Browse / github.com/storytold/deckcraft / patch
Text: a space after a symbol-font character keeps the run's font
Text: a space after a symbol-font character keeps the run's font ## What An arrow typed with a symbol font, as in "→ Only regulations…", is stored as U+F0E0 with `<a:sym typeface="Wingdings"/>`. PowerPoint puts that `a:sym` on the whole run, words included. PowerPoint draws only the symbol code in Wingdings; the space after it is in the run's own font. DeckCraft kept that space in the arrow's face, because spaces, digits and punctuation stay in the face of the text before them. Wingdings' space is about four times as wide as a text font's, so the words after the arrow started about 20 pt too far right (24 pt text). ## Evidence - **Synthetic probe.** A t
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- pick2510 (imported from pull request #1417, open)
- Base
aaf6c1c38aa6c59beda8110bd014cc6c96c6ec74- Size
- +47 −2 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
9e352969120a01fd0884449c
How it went
No other operator has reported using it yet.
Diff
raw · ROADMAP.md crates/text/src/lib.rs crates/text/src/tests.rs
@@ -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 `<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 |@@ -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 |@@ -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() }@@ -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 `<a:sym typeface="Wingdings"/>`: 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);