mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-26 11:36:58 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afb987391d |
@@ -137,7 +137,6 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-25 | Windows installer: the Start menu shortcut takes the icon embedded in `deckcraft.exe` instead of an MSI `<Icon>`, so a taskbar pin keeps its icon after an update (#1114); a pin that already lost it has to be unpinned and pinned again once |
|
||||
| 2026-10-24 | AutoFormat as you type: a paragraph that starts with `*`, `-`, `--`, `>`, `<>`, `1.`, `1)`, `I.`, `i.`, `A.`, `a.` or `a)` and a space or tab becomes a bulleted or numbered list in that style; Backspace right after puts the typed text back, and Tools ▸ AutoCorrect and `text.autocorrect {lists}` turn it off |
|
||||
| 2026-10-24 | Tables: while the caret is in a cell, dragging into another cell selects that range of cells (shaded, merged cells taken in whole), and Shift+arrow grows or shrinks it; formatting applies to the selected cells only, and Delete clears their text and keeps the table |
|
||||
| 2026-10-24 | Preferences ▸ Measurement units: inches or centimetres. The Format pane's Size and Position fields, Shape Format ▸ Size on the ribbon, the Slide Size dialog and the ruler show and take the chosen unit; the deck still stores points (#1326) |
|
||||
@@ -294,7 +293,6 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-25 | trivial | Progress log: Windows shortcut icon survives updates (#1114). Scores and milestone names unchanged |
|
||||
| 2026-10-24 | trivial | Progress log: selecting a range of table cells by dragging. Scores and milestone names unchanged |
|
||||
| 2026-10-24 | trivial | Progress log: measurement units in centimetres (#1326). Scores unchanged |
|
||||
| 2026-10-22 | trivial | Progress log: Animation Pane ▸ Play From. Scores unchanged |
|
||||
|
||||
@@ -430,27 +430,4 @@ mod tests {
|
||||
assert_eq!(mac, win, "macOS and Windows offer different file types");
|
||||
assert_eq!(mac, linux, "macOS and Linux offer different file types");
|
||||
}
|
||||
|
||||
/// #1114: a shortcut that names an MSI `<Icon>` shows a blank icon once an update deletes the
|
||||
/// cached copy of it, and a taskbar pin (a copy of the shortcut) is never repaired. Shortcuts
|
||||
/// take the icon embedded in `deckcraft.exe`; only `ARPPRODUCTICON` uses the MSI `<Icon>`.
|
||||
#[test]
|
||||
fn windows_shortcuts_use_the_icon_embedded_in_the_exe() {
|
||||
let wxs = include_str!("../../../packaging/windows/deckcraft.wxs");
|
||||
let shortcuts: Vec<&str> = wxs.split("<Shortcut").skip(1).filter_map(|s| s.split("/>").next()).collect();
|
||||
assert!(!shortcuts.is_empty(), "deckcraft.wxs has no shortcut");
|
||||
for shortcut in shortcuts {
|
||||
assert!(!shortcut.contains("Icon="), "a shortcut names an MSI <Icon>:{shortcut}");
|
||||
}
|
||||
assert!(wxs.contains("<Property Id=\"ARPPRODUCTICON\""), "Apps and features keeps its icon");
|
||||
}
|
||||
|
||||
/// The shortcuts take the exe's own icon, and `package.ps1 -SkipBuild` packages an existing
|
||||
/// exe without `DECKCRAFT_REQUIRE_WINRES`, so the script has to refuse one with no icon.
|
||||
#[test]
|
||||
fn the_windows_package_script_rejects_an_exe_without_an_icon() {
|
||||
let script = include_str!("../../../packaging/windows/package.ps1");
|
||||
assert!(script.contains("ExtractIconEx"), "package.ps1 does not count the exe's icons");
|
||||
assert!(script.contains("has no embedded icon"), "package.ps1 does not reject an iconless exe");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,3 @@ sync_github_name = true
|
||||
|
||||
[people.Katacc]
|
||||
sync_github_name = true
|
||||
|
||||
[people.SofiaAFigueredo]
|
||||
real_name = "Sofia Alves Figueredo"
|
||||
|
||||
@@ -165,6 +165,15 @@ pub(crate) fn inverse_dct<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
for i in 0..len {
|
||||
t[i] = copy[brev(n, i)];
|
||||
}
|
||||
inverse_dct_stage1(t, n, r);
|
||||
inverse_dct_stage2(t, n, r);
|
||||
inverse_dct_stage3(t, n, r);
|
||||
inverse_dct_stage4(t, n, r);
|
||||
inverse_dct_stage5(t, n, r);
|
||||
inverse_dct_stage6(t, n, r);
|
||||
}
|
||||
|
||||
fn inverse_dct_stage1<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
if n == 6 {
|
||||
for i in 0..16 {
|
||||
bf(t, 32 + i, 63 - i, 63 - 4 * brev(4, i) as i32, false);
|
||||
@@ -190,6 +199,9 @@ pub(crate) fn inverse_dct<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
hd(t, 16 + 2 * i, 17 + 2 * i, i & 1 == 1, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inverse_dct_stage2<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
if n == 6 {
|
||||
for i in 0..4 {
|
||||
for j in 0..2 {
|
||||
@@ -221,6 +233,9 @@ pub(crate) fn inverse_dct<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inverse_dct_stage3<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
for i in 0..2 {
|
||||
bf(t, 2 * i, 2 * i + 1, 32 + 16 * i as i32, i == 0);
|
||||
}
|
||||
@@ -248,6 +263,9 @@ pub(crate) fn inverse_dct<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inverse_dct_stage4<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
for i in 0..2 {
|
||||
hd(t, i, 3 - i, false, r);
|
||||
}
|
||||
@@ -273,6 +291,9 @@ pub(crate) fn inverse_dct<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inverse_dct_stage5<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
if n >= 3 {
|
||||
for i in 0..4 {
|
||||
hd(t, i, 7 - i, false, r);
|
||||
@@ -300,6 +321,9 @@ pub(crate) fn inverse_dct<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
hd(t, i, 15 - i, false, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inverse_dct_stage6<T: Lane>(t: &mut [T], n: u32, r: u32) {
|
||||
if n >= 5 {
|
||||
for i in 0..4 {
|
||||
bf(t, 27 - i, 20 + i, 32, true);
|
||||
@@ -531,10 +555,9 @@ pub(crate) fn inverse_transform_2d(dequant: &[i32], residual: &mut [i32], tx_sz:
|
||||
let col_clamp = (bit_depth + 6).max(16);
|
||||
let rk = row_kind(tx_type);
|
||||
let ck = col_kind(tx_type);
|
||||
let mut t = [0i32; 64];
|
||||
let rect2 = log2w.abs_diff(log2h) == 1;
|
||||
let p = Tx2d { w, h, log2w, log2h, row_shift, col_shift, row_clamp, col_clamp, rk, ck, rect2 };
|
||||
if !lossless && bit_depth <= 10 {
|
||||
let p = Tx2d { w, h, log2w, log2h, row_shift, col_shift, row_clamp, col_clamp, rk, ck, rect2 };
|
||||
if w >= 8 && h >= 8 {
|
||||
transform_lanes::<8>(dequant, residual, &p);
|
||||
} else {
|
||||
@@ -542,6 +565,13 @@ pub(crate) fn inverse_transform_2d(dequant: &[i32], residual: &mut [i32], tx_sz:
|
||||
}
|
||||
return;
|
||||
}
|
||||
scalar_row_pass(dequant, residual, &p, lossless);
|
||||
scalar_col_pass(residual, &p, lossless);
|
||||
}
|
||||
|
||||
fn scalar_row_pass(dequant: &[i32], residual: &mut [i32], p: &Tx2d, lossless: bool) {
|
||||
let (w, h, log2w, row_shift, row_clamp, col_clamp, rk, rect2) = (p.w, p.h, p.log2w, p.row_shift, p.row_clamp, p.col_clamp, p.rk, p.rect2);
|
||||
let mut t = [0i32; 64];
|
||||
let in_w = w.min(32);
|
||||
for i in 0..h {
|
||||
// Every 1D transform maps an all-zero row to zeros (each step is linear with
|
||||
@@ -572,6 +602,11 @@ pub(crate) fn inverse_transform_2d(dequant: &[i32], residual: &mut [i32], tx_sz:
|
||||
residual[i * w + j] = round2(t[j], row_shift).clamp(lo, hi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scalar_col_pass(residual: &mut [i32], p: &Tx2d, lossless: bool) {
|
||||
let (w, h, log2h, col_shift, col_clamp, ck) = (p.w, p.h, p.log2h, p.col_shift, p.col_clamp, p.ck);
|
||||
let mut t = [0i32; 64];
|
||||
for j in 0..w {
|
||||
for i in 0..h {
|
||||
t[i] = residual[i * w + j];
|
||||
@@ -617,11 +652,16 @@ fn transform_1d<T: Lane>(t: &mut [T], kind: Kind1d, n: u32, r: u32) {
|
||||
/// The 2D inverse transform with `L` rows (row pass) or columns (column pass) at a time;
|
||||
/// bit-identical to the one-at-a-time process for bit depths up to 10.
|
||||
fn transform_lanes<const L: usize>(dequant: &[i32], residual: &mut [i32], p: &Tx2d) {
|
||||
let mut t = [[0i32; L]; 64];
|
||||
lanes_row_pass(dequant, residual, p, &mut t);
|
||||
lanes_col_pass(residual, p, &mut t);
|
||||
}
|
||||
|
||||
fn lanes_row_pass<const L: usize>(dequant: &[i32], residual: &mut [i32], p: &Tx2d, t: &mut [[i32; L]; 64]) {
|
||||
let (w, h) = (p.w, p.h);
|
||||
let in_w = w.min(32);
|
||||
let lo = -(1i32 << (p.col_clamp - 1));
|
||||
let hi = (1i32 << (p.col_clamp - 1)) - 1;
|
||||
let mut t = [[0i32; L]; 64];
|
||||
let mut i0 = 0;
|
||||
while i0 < h {
|
||||
// Rows without coefficients (and rows 32..63) transform to zeros.
|
||||
@@ -649,6 +689,10 @@ fn transform_lanes<const L: usize>(dequant: &[i32], residual: &mut [i32], p: &Tx
|
||||
}
|
||||
i0 += L;
|
||||
}
|
||||
}
|
||||
|
||||
fn lanes_col_pass<const L: usize>(residual: &mut [i32], p: &Tx2d, t: &mut [[i32; L]; 64]) {
|
||||
let (w, h) = (p.w, p.h);
|
||||
let mut j0 = 0;
|
||||
while j0 < w {
|
||||
for i in 0..h {
|
||||
|
||||
+140
-201
@@ -126,216 +126,155 @@ fn advance(rtf: &str, i: usize, n: usize) -> usize {
|
||||
if rtf.is_char_boundary(i + n) { i + n } else { i + rtf.get(i..).and_then(|r| r.chars().next()).map_or(1, char::len_utf8) }
|
||||
}
|
||||
|
||||
/// Group state: whether the text is being skipped, and the `\uc` fallback count.
|
||||
#[derive(Clone, Copy)]
|
||||
struct St {
|
||||
skip: bool,
|
||||
uc: usize,
|
||||
}
|
||||
|
||||
/// Parser state for [`rtf_to_outline`]: the cursor, the group stack and the outline so far.
|
||||
struct Outline<'a> {
|
||||
rtf: &'a str,
|
||||
b: &'a [u8],
|
||||
i: usize,
|
||||
stack: Vec<St>,
|
||||
st: St,
|
||||
level: Option<u32>,
|
||||
indent: i64,
|
||||
pending_skip: usize,
|
||||
line: String,
|
||||
out: String,
|
||||
}
|
||||
|
||||
/// Read an RTF outline into tab-indented outline text: a paragraph's outline level (or its left
|
||||
/// indent in 0.25" steps) becomes its number of tabs.
|
||||
pub fn rtf_to_outline(rtf: &str) -> String {
|
||||
let mut p = Outline {
|
||||
rtf,
|
||||
b: rtf.as_bytes(),
|
||||
i: 0,
|
||||
stack: vec![],
|
||||
st: St { skip: false, uc: 1 },
|
||||
level: None,
|
||||
indent: 0,
|
||||
pending_skip: 0,
|
||||
line: String::new(),
|
||||
out: String::new(),
|
||||
#[derive(Clone, Copy)]
|
||||
struct St {
|
||||
skip: bool,
|
||||
uc: usize,
|
||||
}
|
||||
let b = rtf.as_bytes();
|
||||
let mut stack: Vec<St> = vec![];
|
||||
let mut st = St { skip: false, uc: 1 };
|
||||
let (mut level, mut indent): (Option<u32>, i64) = (None, 0);
|
||||
let mut line = String::new();
|
||||
let mut out = String::new();
|
||||
let mut pending_skip = 0usize;
|
||||
let mut i = 0;
|
||||
let flush = |line: &mut String, level: Option<u32>, indent: i64, out: &mut String| {
|
||||
let text = line.trim();
|
||||
if !text.is_empty() {
|
||||
let n = level.map(|l| l as i64).unwrap_or(indent.saturating_add(180) / 360).clamp(0, 9) as usize;
|
||||
out.push_str(&"\t".repeat(n));
|
||||
out.push_str(text);
|
||||
out.push('\n');
|
||||
}
|
||||
line.clear();
|
||||
};
|
||||
p.run();
|
||||
p.flush();
|
||||
finish_surrogates(p.out)
|
||||
}
|
||||
|
||||
impl Outline<'_> {
|
||||
fn run(&mut self) {
|
||||
while self.i < self.b.len() {
|
||||
match self.b[self.i] {
|
||||
b'{' => self.open_group(),
|
||||
b'}' => self.close_group(),
|
||||
b'\\' => {
|
||||
self.i += 1;
|
||||
if !self.escape() {
|
||||
break;
|
||||
while i < b.len() {
|
||||
let c = b[i];
|
||||
match c {
|
||||
b'{' => {
|
||||
stack.push(st);
|
||||
i += 1;
|
||||
// `{\*\dest …}`: an ignorable destination.
|
||||
if b[i..].starts_with(b"\\*") {
|
||||
st.skip = true;
|
||||
}
|
||||
}
|
||||
b'}' => {
|
||||
st = stack.pop().unwrap_or(St { skip: false, uc: 1 });
|
||||
i += 1;
|
||||
}
|
||||
b'\\' => {
|
||||
i += 1;
|
||||
let Some(&n) = b.get(i) else { break };
|
||||
if n.is_ascii_alphabetic() {
|
||||
let s = i;
|
||||
while i < b.len() && b[i].is_ascii_alphabetic() {
|
||||
i += 1;
|
||||
}
|
||||
let word = &rtf[s..i];
|
||||
let ns = i;
|
||||
if i < b.len() && (b[i] == b'-' || b[i].is_ascii_digit()) {
|
||||
i += 1;
|
||||
while i < b.len() && b[i].is_ascii_digit() {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
let arg: Option<i64> = rtf[ns..i].parse().ok();
|
||||
if i < b.len() && b[i] == b' ' {
|
||||
i += 1;
|
||||
}
|
||||
if pending_skip > 0 {
|
||||
pending_skip -= 1;
|
||||
continue;
|
||||
}
|
||||
if SKIP.contains(&word) {
|
||||
st.skip = true;
|
||||
}
|
||||
if st.skip {
|
||||
continue;
|
||||
}
|
||||
match word {
|
||||
"par" => {
|
||||
flush(&mut line, level, indent, &mut out);
|
||||
}
|
||||
"pard" => {
|
||||
level = None;
|
||||
indent = 0;
|
||||
}
|
||||
"outlinelevel" => level = arg.map(|a| a.clamp(0, 9) as u32),
|
||||
"li" => indent = arg.unwrap_or(0),
|
||||
"tab" | "line" => line.push(' '),
|
||||
"uc" => st.uc = arg.unwrap_or(1).clamp(0, 8) as usize,
|
||||
"u" => {
|
||||
let v = arg.unwrap_or(0);
|
||||
// RTF writes UTF-16 units as signed 16-bit values.
|
||||
push_utf16(&mut line, (v & 0xffff) as u32);
|
||||
pending_skip = 0;
|
||||
// Skip the fallback characters.
|
||||
let mut k = st.uc;
|
||||
while k > 0 && i < b.len() {
|
||||
if b[i] == b'\\' && b.get(i + 1) == Some(&b'\'') {
|
||||
i = advance(rtf, i, 4);
|
||||
} else if b[i] == b'{' || b[i] == b'}' || b[i] == b'\\' {
|
||||
break;
|
||||
} else {
|
||||
i = advance(rtf, i, 1);
|
||||
}
|
||||
k -= 1;
|
||||
}
|
||||
}
|
||||
"emdash" => line.push('—'),
|
||||
"endash" => line.push('–'),
|
||||
"bullet" => line.push('•'),
|
||||
"lquote" => line.push('‘'),
|
||||
"rquote" => line.push('’'),
|
||||
"ldblquote" => line.push('“'),
|
||||
"rdblquote" => line.push('”'),
|
||||
_ => {}
|
||||
}
|
||||
} else if n == b'\'' {
|
||||
let hex = rtf.get(i + 1..i + 3).filter(|h| h.bytes().all(|b| b.is_ascii_hexdigit())).and_then(|h| u8::from_str_radix(h, 16).ok());
|
||||
i = advance(rtf, i, 3);
|
||||
if !st.skip
|
||||
&& let Some(h) = hex
|
||||
&& h != 0
|
||||
{
|
||||
line.push(cp1252(h));
|
||||
}
|
||||
} else {
|
||||
i += 1;
|
||||
if st.skip {
|
||||
continue;
|
||||
}
|
||||
match n {
|
||||
b'\\' | b'{' | b'}' => line.push(n as char),
|
||||
b'~' => line.push('\u{a0}'),
|
||||
b'_' => line.push('\u{2011}'),
|
||||
b'\n' | b'\r' => flush(&mut line, level, indent, &mut out),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
b'\r' | b'\n' => self.i += 1,
|
||||
_ => self.text(),
|
||||
}
|
||||
b'\r' | b'\n' => i += 1,
|
||||
_ => {
|
||||
let s = i;
|
||||
while i < b.len() && !matches!(b[i], b'{' | b'}' | b'\\' | b'\r' | b'\n') {
|
||||
i += 1;
|
||||
}
|
||||
if !st.skip
|
||||
&& let Some(t) = rtf.get(s..i)
|
||||
{
|
||||
line.push_str(&t.replace('\u{0}', ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `{`: push the group; `{\*\dest …}` is an ignorable destination.
|
||||
fn open_group(&mut self) {
|
||||
self.stack.push(self.st);
|
||||
self.i += 1;
|
||||
if self.b[self.i..].starts_with(b"\\*") {
|
||||
self.st.skip = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn close_group(&mut self) {
|
||||
self.st = self.stack.pop().unwrap_or(St { skip: false, uc: 1 });
|
||||
self.i += 1;
|
||||
}
|
||||
|
||||
/// The character after a backslash: a control word, a hex escape or a symbol. False at the
|
||||
/// end of the input.
|
||||
fn escape(&mut self) -> bool {
|
||||
let Some(&n) = self.b.get(self.i) else { return false };
|
||||
if n.is_ascii_alphabetic() {
|
||||
self.control_word();
|
||||
} else if n == b'\'' {
|
||||
self.hex_escape();
|
||||
} else {
|
||||
self.symbol(n);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn control_word(&mut self) {
|
||||
let rtf = self.rtf;
|
||||
let s = self.i;
|
||||
while self.i < self.b.len() && self.b[self.i].is_ascii_alphabetic() {
|
||||
self.i += 1;
|
||||
}
|
||||
let word = &rtf[s..self.i];
|
||||
let ns = self.i;
|
||||
if self.i < self.b.len() && (self.b[self.i] == b'-' || self.b[self.i].is_ascii_digit()) {
|
||||
self.i += 1;
|
||||
while self.i < self.b.len() && self.b[self.i].is_ascii_digit() {
|
||||
self.i += 1;
|
||||
}
|
||||
}
|
||||
let arg: Option<i64> = rtf[ns..self.i].parse().ok();
|
||||
if self.i < self.b.len() && self.b[self.i] == b' ' {
|
||||
self.i += 1;
|
||||
}
|
||||
if self.pending_skip > 0 {
|
||||
self.pending_skip -= 1;
|
||||
return;
|
||||
}
|
||||
if SKIP.contains(&word) {
|
||||
self.st.skip = true;
|
||||
}
|
||||
if !self.st.skip {
|
||||
self.apply(word, arg);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(&mut self, word: &str, arg: Option<i64>) {
|
||||
match word {
|
||||
"par" => self.flush(),
|
||||
"pard" => {
|
||||
self.level = None;
|
||||
self.indent = 0;
|
||||
}
|
||||
"outlinelevel" => self.level = arg.map(|a| a.clamp(0, 9) as u32),
|
||||
"li" => self.indent = arg.unwrap_or(0),
|
||||
"tab" | "line" => self.line.push(' '),
|
||||
"uc" => self.st.uc = arg.unwrap_or(1).clamp(0, 8) as usize,
|
||||
"u" => self.unicode(arg.unwrap_or(0)),
|
||||
"emdash" => self.line.push('—'),
|
||||
"endash" => self.line.push('–'),
|
||||
"bullet" => self.line.push('•'),
|
||||
"lquote" => self.line.push('‘'),
|
||||
"rquote" => self.line.push('’'),
|
||||
"ldblquote" => self.line.push('“'),
|
||||
"rdblquote" => self.line.push('”'),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// `\uN`: one UTF-16 unit, then the `\uc` fallback characters, which are skipped.
|
||||
fn unicode(&mut self, v: i64) {
|
||||
// RTF writes UTF-16 units as signed 16-bit values.
|
||||
push_utf16(&mut self.line, (v & 0xffff) as u32);
|
||||
self.pending_skip = 0;
|
||||
let mut k = self.st.uc;
|
||||
while k > 0 && self.i < self.b.len() {
|
||||
if self.b[self.i] == b'\\' && self.b.get(self.i + 1) == Some(&b'\'') {
|
||||
self.i = advance(self.rtf, self.i, 4);
|
||||
} else if matches!(self.b[self.i], b'{' | b'}' | b'\\') {
|
||||
break;
|
||||
} else {
|
||||
self.i = advance(self.rtf, self.i, 1);
|
||||
}
|
||||
k -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// `\'hh`: one cp1252 byte.
|
||||
fn hex_escape(&mut self) {
|
||||
let hex =
|
||||
self.rtf.get(self.i + 1..self.i + 3).filter(|h| h.bytes().all(|b| b.is_ascii_hexdigit())).and_then(|h| u8::from_str_radix(h, 16).ok());
|
||||
self.i = advance(self.rtf, self.i, 3);
|
||||
if !self.st.skip
|
||||
&& let Some(h) = hex
|
||||
&& h != 0
|
||||
{
|
||||
self.line.push(cp1252(h));
|
||||
}
|
||||
}
|
||||
|
||||
/// A one-character control symbol (`\\`, `\{`, `\~`, ...).
|
||||
fn symbol(&mut self, n: u8) {
|
||||
self.i += 1;
|
||||
if self.st.skip {
|
||||
return;
|
||||
}
|
||||
match n {
|
||||
b'\\' | b'{' | b'}' => self.line.push(n as char),
|
||||
b'~' => self.line.push('\u{a0}'),
|
||||
b'_' => self.line.push('\u{2011}'),
|
||||
b'\n' | b'\r' => self.flush(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Plain text up to the next special character.
|
||||
fn text(&mut self) {
|
||||
let s = self.i;
|
||||
while self.i < self.b.len() && !matches!(self.b[self.i], b'{' | b'}' | b'\\' | b'\r' | b'\n') {
|
||||
self.i += 1;
|
||||
}
|
||||
if !self.st.skip
|
||||
&& let Some(t) = self.rtf.get(s..self.i)
|
||||
{
|
||||
self.line.push_str(&t.replace('\u{0}', ""));
|
||||
}
|
||||
}
|
||||
|
||||
/// Ends the current line: a non-empty line becomes one outline entry, indented by its level.
|
||||
fn flush(&mut self) {
|
||||
let text = self.line.trim();
|
||||
if !text.is_empty() {
|
||||
let n = self.level.map(|l| l as i64).unwrap_or(self.indent.saturating_add(180) / 360).clamp(0, 9) as usize;
|
||||
self.out.push_str(&"\t".repeat(n));
|
||||
self.out.push_str(text);
|
||||
self.out.push('\n');
|
||||
}
|
||||
self.line.clear();
|
||||
}
|
||||
flush(&mut line, level, indent, &mut out);
|
||||
finish_surrogates(out)
|
||||
}
|
||||
|
||||
/// Push a UTF-16 unit; surrogate halves are parked as private markers (NUL plus four hex digits)
|
||||
|
||||
@@ -58,15 +58,9 @@
|
||||
<Component Id="DeckcraftApp" Guid="D87EA444-41A4-4C19-922E-66D1A29461FF">
|
||||
<File Id="DeckcraftNotice" Source="$(var.BinDir)\NOTICE" KeyPath="no" />
|
||||
<File Id="DeckcraftExe" Source="$(var.BinDir)\deckcraft.exe" KeyPath="yes">
|
||||
<!-- No Icon here, so the shortcut shows the icon embedded in deckcraft.exe (build.rs). An
|
||||
MSI Icon is cached under %APPDATA%\Microsoft\Installer\{ProductCode}\, and every
|
||||
update is a major upgrade with a new ProductCode that deletes the old folder. A
|
||||
taskbar pin is a copy of this shortcut that the update never touches, so it kept
|
||||
pointing at the deleted file and lost its icon (#1114). The exe's path survives
|
||||
updates; the shortcut is not advertised, so it targets that path and its icon is the
|
||||
exe's own. The Icon above stays for ARPPRODUCTICON (Apps and features). -->
|
||||
<Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder" Name="DeckCraft"
|
||||
Description="DeckCraft presentations and slide shows" WorkingDirectory="INSTALLFOLDER" Advertise="no" />
|
||||
Description="DeckCraft presentations and slide shows" WorkingDirectory="INSTALLFOLDER" Advertise="yes"
|
||||
Icon="DeckcraftIcon.ico" IconIndex="0" />
|
||||
</File>
|
||||
|
||||
<!-- .deckcraft: DeckCraft's own format. -->
|
||||
|
||||
@@ -83,15 +83,6 @@ foreach ($check in @(@('deckcraft.exe', 2), @('deckcraft-cli.exe', 3))) {
|
||||
if ($h.Subsystem -ne $check[1]) { throw "$($check[0]) has PE subsystem $($h.Subsystem), expected $($check[1])" }
|
||||
Write-Output "ok $($check[0]): $Arch, PE subsystem $($h.Subsystem)"
|
||||
}
|
||||
|
||||
# The Start menu shortcut is not advertised and names no MSI icon, so it shows the icon embedded in
|
||||
# deckcraft.exe (#1114). -SkipBuild takes whatever binary is already there, which may come from a
|
||||
# build without DECKCRAFT_REQUIRE_WINRES=1 whose resource compilation failed: refuse an exe with no
|
||||
# icon instead of shipping shortcuts with a generic one. ExtractIconEx with index -1 counts them.
|
||||
Add-Type -Namespace DeckCraft -Name Shell -MemberDefinition '[DllImport("shell32.dll", CharSet = CharSet.Unicode)] public static extern uint ExtractIconEx(string file, int index, IntPtr[] large, IntPtr[] small, uint icons);'
|
||||
$Icons = [DeckCraft.Shell]::ExtractIconEx((Join-Path $Bin 'deckcraft.exe'), -1, $null, $null, 0)
|
||||
if ($Icons -eq 0) { throw 'deckcraft.exe has no embedded icon: rebuild it with DECKCRAFT_REQUIRE_WINRES=1 (omit -SkipBuild)' }
|
||||
Write-Output "ok deckcraft.exe: $Icons embedded icon group(s)"
|
||||
$Stage = Join-Path $TargetDir "windows-package\$Arch"
|
||||
Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $Stage | Out-Null
|
||||
|
||||
Reference in New Issue
Block a user