diff --git a/ROADMAP.md b/ROADMAP.md index 7dfa96f8..37e6cde0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,6 +2,7 @@ **Stage: alpha** · next: beta, ~20 points and ~120–210 h away +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-16 · **Change:** minor (Symbol-font characters (U+F0xx) remap to the installed Symbol face's real Unicode on macOS, so they draw from Symbol instead of a look-alike; previous: major, community PR wave 2026-10-14) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** major (community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) DeckCraft aims at full PowerPoint parity, and to be better: faster, open (a documented zip+JSON @@ -131,6 +132,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-16 | Symbol-font characters: a run's U+F0xx symbol code (from ``) now draws from the installed Symbol face even when it has no private-use mapping — Apple's Symbol on macOS maps the glyphs to real Unicode, so the code is remapped to that character (the Adobe Symbol encoding) instead of falling to a look-alike. Bullets and metafile text take the same remap | | 2026-10-15 | CLI: .txt and .md export includes table cell text, which it used to skip silently (#814) | | 2026-10-15 | PDF export and rendering: `anchorCtr` centres the drawn text block — right-aligned text no longer ran past the box edge (#736) | | 2026-10-14 | connected MCP rejects unrelated, malformed, incomplete and oversized replies without replay; buffered unsolicited bytes force a fresh connection before sending (#718) | @@ -227,6 +229,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | Change | Summary | |---|---|---| +| 2026-10-16 | minor | Symbol-font U+F0xx codes remap to the installed Symbol face's real Unicode (Apple Symbol on macOS) | | 2026-10-14 | trivial | remote MCP checks reply IDs, envelopes and bounded newline framing (#718) | | 2026-10-14 | minor | Gray list markers on empty paragraphs while editing; caret keeps the list indent before typing | | 2026-10-14 | minor | Community PR wave folded: headline breadth 84% over 191 features (P0 92%, P1 82%, P2 55%) and the feature-area breadths from `cargo xtask parity`; one row per area; Upcoming item 2 merged; per-PR count notes dropped from the progress log. Ready stays ~55%, stage alpha | diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 9acc26c0..7963cbbb 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -865,13 +865,15 @@ fn shape_cells( let neutral = cluster.iter().all(|c| script::script_of(*c).is_none()) && !deckcraft_fonts::is_emoji_cluster(cluster) && !cluster.contains(&'\u{FE0E}'); // A symbol-font character (U+F0xx) with an `a:sym` font draws in that font when it's there. + // Some installed Symbol faces (Apple's on macOS) have no private-use mapping: they map the + // glyphs to real Unicode, so the PUA code is remapped to that character when shaping. let sym_face = st .sym .as_deref() .filter(|_| cluster.first().is_some_and(|c| ('\u{F000}'..='\u{F0FF}').contains(c))) .filter(|f| db.has_family(f)) .map(|f| db.face(f, st.style_name)) - .filter(|f| deckcraft_fonts::covers_cluster(f, cluster)); + .filter(|f| covers_symbol(f, cluster.first().copied().unwrap_or(' '))); let face = match (sym_face, prev.as_ref()) { (Some(f), _) => f, (None, Some(p)) @@ -1055,7 +1057,11 @@ fn shape_item( let (Some(&b0), Some(&b1)) = (byte_of.get(range.start), byte_of.get(range.end)) else { return }; let features = shaping_features(cells.get(range.clone()).unwrap_or(&[]), styles, script); let params = ShapeParams { features: &features, rtl, script, language: lang }; - let glyphs = deckcraft_fonts::shape_range(face, text, b0..b1, ¶ms, |ch| if upper { caps_char(ch) } else { ch }); + let sym = styles.get(first.style).and_then(|st| st.sym.as_deref()); + let glyphs = deckcraft_fonts::shape_range(face, text, b0..b1, ¶ms, |ch| { + let ch = if upper { caps_char(ch) } else { ch }; + symbol_map_char(face, ch, sym).unwrap_or(ch) + }); let upem = face.upem.max(1.0); let scale_of = |cell: &Cell| { let st = styles.get(cell.style); @@ -1137,6 +1143,199 @@ fn is_symbol_font(family: &str) -> bool { f.starts_with("wingdings") || f == "symbol" || f.starts_with("webdings") || f.starts_with("marlett") } +/// Does `face` draw the symbol-font character `c` (a U+F0xx private-use code)? Either directly at +/// the PUA, or, when the face has no private-use mapping (Apple's "Symbol" maps its glyphs to real +/// Unicode), at the Adobe Symbol encoding's real character. +fn covers_symbol(face: &FontFace, c: char) -> bool { + let code = (c as u32) & 0xFF; + face.covers(c) || symbol_unicode(code).is_some_and(|u| char::from_u32(u).is_some_and(|r| face.covers(r))) +} + +/// The character `ch` is drawn as when the item's face is the `a:sym` symbol font `sym`: a U+F0xx +/// private-use code becomes the face's glyph — at the PUA, or at the real Unicode the Symbol font +/// maps it to (Apple's "Symbol"). `None` when `ch` is not such a character or the face is not that +/// font, so the run's other characters pass through unchanged. +fn symbol_map_char(face: &FontFace, ch: char, sym: Option<&str>) -> Option { + let code = ch as u32; + if !(0xF000..=0xF0FF).contains(&code) { + return None; + } + let sym = sym?; + if !is_symbol_font(sym) || !face.family.eq_ignore_ascii_case(sym) { + return None; + } + symbol_font_draw(face, code) +} + +/// The real-Unicode character the Symbol font's code `code` (0x00–0xFF) maps to, in the Adobe +/// Symbol encoding (Unicode, Inc.'s `MAPPINGS/VENDORS/ADOBE/symbol.txt`; Apple's Mac OS Symbol is +/// laid out identically): the codes a document stores as U+F000 + code draw the same glyphs at +/// these Unicode points. Apple's "Symbol" on macOS has no private-use mapping at all — it maps the +/// glyphs to these real characters instead — so a document's U+F0xx symbol characters must be +/// remapped to them there (PowerPoint does the same against whatever the installed Symbol font +/// covers). Only the non-ASCII, non-CUS entries are listed: printable ASCII passes through as its +/// own code, and the CUS (Corporate Use Subarea) fragment glyphs have no standard Unicode point. +const fn symbol_unicode(code: u32) -> Option { + Some(match code { + // 0x20–0x7E: the few mathematical ones among the ASCII-range characters. + 0x22 => 0x2200, + 0x24 => 0x2203, + 0x27 => 0x220B, + 0x2A => 0x2217, + 0x2D => 0x2212, + 0x3C => 0x2264, + 0x3D => 0x2265, + 0x3E => 0x221E, + 0x3F => 0x2234, + 0x40 => 0x2245, + 0x5C => 0x2234, + 0x5E => 0x22A5, + 0x7E => 0x223C, + // 0x41–0x5A: Greek uppercase. + 0x41 => 0x0391, + 0x42 => 0x0392, + 0x43 => 0x03A7, + 0x44 => 0x0394, + 0x45 => 0x0395, + 0x46 => 0x03A6, + 0x47 => 0x0393, + 0x48 => 0x0397, + 0x49 => 0x0399, + 0x4A => 0x03D1, + 0x4B => 0x039A, + 0x4C => 0x039B, + 0x4D => 0x039C, + 0x4E => 0x039D, + 0x4F => 0x039F, + 0x50 => 0x03A0, + 0x51 => 0x0398, + 0x52 => 0x03A1, + 0x53 => 0x03A3, + 0x54 => 0x03A4, + 0x55 => 0x03A5, + 0x56 => 0x03C2, + 0x57 => 0x03A9, + 0x58 => 0x039E, + 0x59 => 0x03A8, + 0x5A => 0x0396, + // 0x61–0x7A: Greek lowercase (0x6A is the phi symbol, 0x6D the mu). + 0x61 => 0x03B1, + 0x62 => 0x03B2, + 0x63 => 0x03C7, + 0x64 => 0x03B4, + 0x65 => 0x03B5, + 0x66 => 0x03C6, + 0x67 => 0x03B3, + 0x68 => 0x03B7, + 0x69 => 0x03B9, + 0x6A => 0x03D5, + 0x6B => 0x03BA, + 0x6C => 0x03BB, + 0x6D => 0x03BC, + 0x6E => 0x03BD, + 0x6F => 0x03BF, + 0x70 => 0x03C0, + 0x71 => 0x03B8, + 0x72 => 0x03C1, + 0x73 => 0x03C3, + 0x74 => 0x03C4, + 0x75 => 0x03C5, + 0x76 => 0x03D6, + 0x77 => 0x03C9, + 0x78 => 0x03BE, + 0x79 => 0x03C8, + 0x7A => 0x03B6, + // 0xA1–0xAF: Greek upsilon with hook, primes, math, suits and arrows. + 0xA1 => 0x03D2, + 0xA2 => 0x2032, + 0xA3 => 0x2264, + 0xA4 => 0x2215, + 0xA5 => 0x221E, + 0xA6 => 0x0192, + 0xA7 => 0x2663, + 0xA8 => 0x2666, + 0xA9 => 0x2665, + 0xAA => 0x2660, + 0xAB => 0x2194, + 0xAC => 0x2190, + 0xAD => 0x2191, + 0xAE => 0x2192, + 0xAF => 0x2193, + // 0xB0–0xBC: degree, plus-minus, double prime, math and ellipsis. + 0xB0 => 0x00B0, + 0xB1 => 0x00B1, + 0xB2 => 0x2033, + 0xB3 => 0x2265, + 0xB4 => 0x00D7, + 0xB5 => 0x221D, + 0xB6 => 0x2202, + 0xB7 => 0x2022, + 0xB8 => 0x00F7, + 0xB9 => 0x2260, + 0xBA => 0x2261, + 0xBB => 0x2248, + 0xBC => 0x2026, + 0xBF => 0x21B5, + // 0xC0–0xCF: letterlike symbols, set relations. + 0xC0 => 0x2135, + 0xC1 => 0x2111, + 0xC2 => 0x211C, + 0xC3 => 0x2118, + 0xC4 => 0x2297, + 0xC5 => 0x2295, + 0xC6 => 0x2205, + 0xC7 => 0x2229, + 0xC8 => 0x222A, + 0xC9 => 0x2283, + 0xCA => 0x2287, + 0xCB => 0x2284, + 0xCC => 0x2282, + 0xCD => 0x2286, + 0xCE => 0x2208, + 0xCF => 0x2209, + // 0xD0–0xDF: angle, nabla, product, radical, not, ands, ors, double arrows. + 0xD0 => 0x2220, + 0xD1 => 0x2207, + 0xD5 => 0x220F, + 0xD6 => 0x221A, + 0xD7 => 0x22C5, + 0xD8 => 0x00AC, + 0xD9 => 0x2227, + 0xDA => 0x2228, + 0xDB => 0x21D4, + 0xDC => 0x21D0, + 0xDD => 0x21D1, + 0xDE => 0x21D2, + 0xDF => 0x21D3, + // 0xE0–0xF5: lozenge, angle brackets, summation, integral. + 0xE0 => 0x25CA, + 0xE1 => 0x2329, + 0xE5 => 0x2211, + 0xF1 => 0x232A, + 0xF2 => 0x222B, + 0xF3 => 0x2320, + 0xF5 => 0x2321, + _ => return None, + }) +} + +/// The character the symbol font draws the code `code` at, when the installed `face` is that font: +/// its private-use code U+F000 + code, or, when the face has no private-use mapping (Apple's +/// "Symbol" maps its glyphs to real Unicode), the Adobe Symbol encoding's real character. +fn symbol_font_draw(face: &FontFace, code: u32) -> Option { + let pua = char::from_u32(0xF000 | (code & 0xFF))?; + if face.covers(pua) { + return Some(pua); + } + let real = symbol_unicode(code & 0xFF)?; + let real = char::from_u32(real)?; + if face.covers(real) { + Some(real) + } else { + None + } +} + /// The character to draw bullet `c` with the installed symbol font `face`, when `font` names a /// symbol font that `face` is (not a substitute) and it has the glyph. fn symbol_font_char(face: &FontFace, c: &str, font: Option<&str>) -> Option { @@ -1145,14 +1344,8 @@ fn symbol_font_char(face: &FontFace, c: &str, font: Option<&str>) -> Option`: @@ -955,10 +976,11 @@ fn symbol_font_characters_draw_in_the_sym_font() { // whichever installed font happens to map U+F0DE (Wingdings draws an arrow at another angle). let symbol = "Symbol"; let db = FontDb::global(); - // Some installed Symbol faces use Unicode mappings only, without the legacy U+F0xx cmap. - // This integration test needs a face that can draw the symbol code used by the document. - if !db.has_family(symbol) || !db.face(symbol, "Regular").covers('\u{F0DE}') { - eprintln!("skipped: no installed Symbol face covers U+F0DE"); + // The installed Symbol face draws the code either at the private-use U+F0DE or, when it has no + // private-use mapping (Apple's Symbol on macOS maps glyphs to real Unicode), at the real + // character the Symbol font stores the code at. Skip only if neither is covered. + if !db.has_family(symbol) || !covers_symbol(&db.face(symbol, "Regular"), '\u{F0DE}') { + eprintln!("skipped: no installed Symbol face draws U+F0DE"); return; } let (p, mut sh) = setup("x\u{F0DE}y", 600.0, 300.0);