diff --git a/ROADMAP.md b/ROADMAP.md index bc7d88d3..8b045b73 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -151,6 +151,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-28 | CLI `run --cmd-file FILE` reads UTF-8 commands in order alongside `--cmd`, skips blank/comment lines, and reports file/line errors before final save/export (#1098) | | 2026-10-28 | MCP accepts repeated `--automation-read-root` and `--automation-write-root` flags for access to multiple folders (#1154) | | 2026-10-26 | Slide reorder: dragging one of several selected slides in the thumbnail pane or the Slide Sorter moves the whole selection, as one run in deck order and one undo step; the slides stay selected and join the section they land in. `slide.move {ids, to}` does the same from the control channel and MCP; dragging an unselected slide still moves only that slide | +| 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-29 | Picture Format: the Picture Styles row shows as many styles as leave room for the groups after it, and its arrow opens all 24. With every style in the row, Crop and Size sat past the ribbon's edge in any window narrower than about 2500 px, so Crop could not be seen without scrolling the ribbon (#1232). In the default 1440 px window Crop, Height and Width are now in view, and from 1600 px the whole tab fits | | 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 | @@ -335,6 +336,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/shape.rs b/crates/text/src/shape.rs index ea3c1beb..2be8424f 100644 --- a/crates/text/src/shape.rs +++ b/crates/text/src/shape.rs @@ -1,7 +1,7 @@ use super::autonum::autonum; use super::run::{RunEffects, TextShadow}; use super::style::{Cell, Opts, ParaShaped, Style, face_chain, make_style}; -use super::symbol::{covers_symbol, map_symbol_bullet, symbol_font_char, symbol_map_char}; +use super::symbol::{covers_symbol, is_symbol_font, map_symbol_bullet, symbol_font_char, symbol_map_char}; use super::{ Arc, Bullet, Caps, Ctx, Fill, FontDb, FontFace, FontSlot, HashMap, ParaProps, RunKind, RunProps, ScriptTag, Shape, ShapeParams, TextBody, UnicodeSegmentation, bidi, resolve, script, @@ -185,7 +185,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}'); @@ -202,7 +204,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 35663cd5..f9f3f310 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -1276,6 +1276,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);