diff --git a/ROADMAP.md b/ROADMAP.md index b04c4b69..9ef69991 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -137,6 +137,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-26 | A space, digit or punctuation after a symbol-font character (`a:sym`, e.g. a Wingdings arrow typed in a run) draws in the run's own font, as PowerPoint does; it stayed in the symbol font, whose space is about four times as wide (synthetic probe) | | 2026-10-25 | Windows installer: the Start menu shortcut takes the icon embedded in `deckcraft.exe` instead of an MSI ``, 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 | @@ -312,6 +313,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-15 | trivial | Progress log: PPTX trigger click-step round trip. Feature scores and milestone names unchanged | | 2026-10-16 | trivial | Progress log: PNG export with a transparent background (#784) | | 2026-10-16 | trivial | Progress log: a `.ppsx` from the system starts the slide show (#862) | +| 2026-10-26 | trivial | Progress log: spaces after symbol-font characters | | 2026-10-24 | trivial | Progress log: picture and embedded object fills | | 2026-10-22 | trivial | Progress log: table text takes the master's other-text style | | 2026-10-16 | trivial | Progress log: responsive mouse-wheel navigation on the fitted canvas | diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index e70c3304..2468877e 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -867,7 +867,9 @@ fn shape_cells( let Some(st) = styles.get(si) else { continue }; let (sc, slot) = scripts.get(a).copied().unwrap_or((None, FontSlot::Latin)); // Spaces, digits and punctuation stay in the face of the text before them when it can - // draw them, so a phrase isn't cut into items at every space. + // draw them, so a phrase isn't cut into items at every space; not in a symbol font + // (`a:sym`), which PowerPoint uses for the symbol codes alone (Wingdings' space is about + // four times a text font's). // Emoji, and pictographs asked to look like text (U+FE0E), pick their own face. let neutral = cluster.iter().all(|c| script::script_of(*c).is_none()) && !deckcraft_fonts::is_emoji_cluster(cluster) && !cluster.contains(&'\u{FE0E}'); @@ -884,7 +886,11 @@ fn shape_cells( let face = match (sym_face, prev.as_ref()) { (Some(f), _) => f, (None, Some(p)) - if neutral && !p.is_color() && deckcraft_fonts::covers_cluster(p, cluster) && char_style.get(a.wrapping_sub(1)) == Some(&si) => + if neutral + && !p.is_color() + && !is_symbol_font(&p.family) + && deckcraft_fonts::covers_cluster(p, cluster) + && char_style.get(a.wrapping_sub(1)) == Some(&si) => { p.clone() } diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index fd02e821..72a3fd2e 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -1255,6 +1255,43 @@ fn symbol_font_characters_draw_in_the_sym_font() { assert!(families.iter().all(|(f, c)| c.0 == 1 || !f.eq_ignore_ascii_case(symbol)), "{families:?}"); } +#[test] +fn a_space_after_a_symbol_font_character_keeps_the_runs_font() { + // PowerPoint stores "→ Only" typed in a Wingdings run as U+F0E0, a space and the words, all + // with ``: only the symbol code draws in Wingdings. The space + // stayed in the arrow's face, and Wingdings' space is about four times as wide. Skipped + // without Wingdings. + let db = FontDb::global(); + if !db.has_family("Wingdings") || !covers_symbol(&db.face("Wingdings", "Regular"), '\u{F0E0}') { + eprintln!("skipped: no installed Wingdings face draws U+F0E0"); + return; + } + let laid = |split: bool| { + let (p, mut sh) = setup("\u{F0E0} Only", 900.0, 300.0); + let para = &mut sh.text.as_mut().unwrap().paragraphs[0]; + para.props.bullet = Some(Bullet::None); + para.runs[0].props.font = Some("Arial".into()); + para.runs[0].props.font_sym = Some("Wingdings".into()); + if split { + // The arrow alone carries the symbol font. + let mut rest = para.runs[0].clone(); + rest.text = " Only".into(); + rest.props.font_sym = None; + para.runs[0].text = "\u{F0E0}".into(); + para.runs.push(rest); + } + lay(&p, &sh, 900.0, 300.0) + }; + let whole = laid(false); + let families: Vec<(String, (usize, usize))> = whole.runs.iter().map(|r| (r.face.family.clone(), r.chars)).collect(); + assert!(families.iter().any(|(f, c)| f.eq_ignore_ascii_case("Wingdings") && c.0 == 0), "the arrow: {families:?}"); + let in_wingdings = |i: usize| families.iter().any(|(f, c)| (c.0..c.1).contains(&i) && f.eq_ignore_ascii_case("Wingdings")); + assert!(!in_wingdings(1) && !in_wingdings(2), "the space and words: {families:?}"); + // "Only" starts where it does when the arrow alone is in Wingdings. + let o = |l: &TextLayout| l.lines[0].caret_x[2]; + assert!((o(&whole) - o(&laid(true))).abs() < 0.01, "{} vs {}", o(&whole), o(&laid(true))); +} + #[test] fn small_capitals_draw_smaller_than_the_capitals_beside_them() { let (p, mut sh) = setup("HxHx", 2000.0, 300.0);