From 1dc8b72bbf9f0384b272cc3414d7b4c73e440edb Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 18:34:42 -0800 Subject: [PATCH] Type: hyphenated type lays out again when the hyphenation exceptions change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changing Preferences › Hyphenation › Exceptions bumped each open document's revision, but the caches of laid-out type outside the text crate were keyed on the font generation alone. The renderer kept the glyph geometry, raster-effect pixels, live-effect and envelope art, pattern tiles and placed symbol art of unchanged type objects, so hyphenated area type kept its old line breaks on the canvas. The hit-test art of blends and envelopes, the Fit to Artwork Bounds answer, the layout caches of the Type and Touch Type tools and the out port's overflow mark were keyed the same way. vectorcraft_text::layout_inputs() returns a number that changes with the fonts and with the hyphenation exceptions. Every one of those caches now keys on it instead of FontDb::generation(), and envelope ink uses it in place of the private envelope_ink_inputs(). The hit-test key hashes it together with the plug-in revision. Tests: a renderer that drew hyphenated area type, alone, with a drop shadow, in a group with a Transform effect, in a symbol instance and in an envelope, draws it as a new renderer does after the exceptions change; the out port of selected area type shows the overflow that the new exceptions cause. The render test helpers that run a step again when the fonts change during it now also run it again when the exceptions change. --- crates/engine/src/doc_state.rs | 9 +-- crates/engine/src/session/documents.rs | 6 +- crates/render/src/frame.rs | 4 +- crates/render/src/fx/pixels.rs | 4 +- crates/render/src/lib.rs | 4 +- crates/render/src/live.rs | 7 +- crates/render/src/pattern.rs | 7 +- crates/render/src/renderer/text.rs | 10 +-- crates/render/src/tests/fontchange.rs | 89 ++++++++++++++++++++++---- crates/render/src/tests/memory.rs | 8 +-- crates/render/src/tests/typeedges.rs | 2 +- crates/text/src/hyphen.rs | 10 +-- crates/text/src/layout.rs | 4 +- crates/text/src/layout/driver.rs | 9 +-- crates/text/src/lib.rs | 2 +- crates/tools/src/text.rs | 6 +- crates/tools/src/text/ports.rs | 5 +- crates/tools/src/text/tests.rs | 30 +++++++++ crates/tools/src/touchtype.rs | 7 +- 19 files changed, 163 insertions(+), 60 deletions(-) diff --git a/crates/engine/src/doc_state.rs b/crates/engine/src/doc_state.rs index c3722f6ad2..780616c07a 100644 --- a/crates/engine/src/doc_state.rs +++ b/crates/engine/src/doc_state.rs @@ -369,12 +369,13 @@ impl DocState { /// menu asks on every frame it is open, and measuring builds the solids of 3D effects, so the /// answer is kept for this version of the document and those artboards, and a change of the /// selection or the view keeps it. It is measured again when what the measure reads outside the - /// document changes: the installed plug-ins, whose effects draw art, and the installed fonts, - /// which set the bounds of all type. + /// document changes: the installed plug-ins, whose effects draw art, and the installed fonts and + /// the hyphenation exceptions, which set the bounds of all type. pub fn any_art_to_fit(&self, indices: &[usize]) -> bool { - self.any_art_to_fit_with(indices, (vectorcraft_plugins::registry::revision(), vectorcraft_text::FontDb::global().generation())) + self.any_art_to_fit_with(indices, (vectorcraft_plugins::registry::revision(), vectorcraft_text::layout_inputs())) } - /// [`Self::any_art_to_fit`], with `outside` the generations of the plug-ins and the fonts. + /// [`Self::any_art_to_fit`], with `outside` the generations of the plug-ins and the layout inputs + /// ([`vectorcraft_text::layout_inputs`]). pub(crate) fn any_art_to_fit_with(&self, indices: &[usize], outside: (u64, u64)) -> bool { // Measuring runs renderer and plug-in code; should it ever ask again while measuring, it // is measured without being kept rather than borrowed twice. diff --git a/crates/engine/src/session/documents.rs b/crates/engine/src/session/documents.rs index 5ceaf86aaf..a87b47be0e 100644 --- a/crates/engine/src/session/documents.rs +++ b/crates/engine/src/session/documents.rs @@ -25,9 +25,11 @@ impl Session { // Clicks find an image's own fills and strokes where their effects draw them. vectorcraft_doc::hit::set_image_paint(vectorcraft_render::effects::image_paint); // Clicks find a blend's steps and an envelope's distorted art where the canvas draws them, - // evaluated again when the fonts or the plug-ins change. + // evaluated again when the fonts, the hyphenation exceptions or the plug-ins change. vectorcraft_doc::hit::set_live_art(vectorcraft_render::effects::expand_live, || { - vectorcraft_text::FontDb::global().generation().rotate_left(32) ^ vectorcraft_render::effects::plugin_revision() + let mut h = std::hash::DefaultHasher::new(); + std::hash::Hash::hash(&(vectorcraft_text::layout_inputs(), vectorcraft_render::effects::plugin_revision()), &mut h); + std::hash::Hasher::finish(&h) }); // Envelopes map type from the bounds of its glyph outlines, underlines and strikethroughs. vectorcraft_text::register_envelope_ink(); diff --git a/crates/render/src/frame.rs b/crates/render/src/frame.rs index e16f134aca..7d000aa64f 100644 --- a/crates/render/src/frame.rs +++ b/crates/render/src/frame.rs @@ -110,8 +110,8 @@ pub(crate) struct PlacedEntry { pub(crate) art: std::sync::Weak, /// The art placed, or `None` for art placed again in every frame ([`depends_on_document`]). pub(crate) placed: Option>, - /// The font generation ([`vectorcraft_text::FontDb::generation`]) the art was placed in, when - /// it holds type: the art is placed again when the fonts change. + /// The layout inputs ([`vectorcraft_text::layout_inputs`]) the art was placed with, when it + /// holds type: the art is placed again when the fonts or the hyphenation exceptions change. pub(crate) fonts: Option, /// The 9-slice map the art was placed through ([`Renderer::sliced_art`]), or `None` for art /// placed by the transform alone ([`Renderer::placed_art`]). An entry is used only by the diff --git a/crates/render/src/fx/pixels.rs b/crates/render/src/fx/pixels.rs index 72731f7330..cdf817f1e4 100644 --- a/crates/render/src/fx/pixels.rs +++ b/crates/render/src/fx/pixels.rs @@ -412,7 +412,7 @@ impl Renderer { /// brush library it reads; for images and placed documents their pixels and files, the /// layer's Dim Images and the options they are drawn with (with /// [`crate::RenderOptions::kept_images_only`], also which pixels and undecodable notes the - /// image store holds); for type the fonts, whether overprinting shows + /// image store holds); for type the fonts and the hyphenation exceptions, whether overprinting shows /// ([`crate::proof::overprints`]) and Document Setup's substitution highlights; and the /// plug-ins installed, with the names of the document's swatches and patterns and its pattern /// definitions (a plug-in's painted pieces can name them), when an object in it has a plug-in @@ -457,7 +457,7 @@ impl Renderer { f.doc.setup.highlight_substituted_glyphs, o.highlight_alternate_glyphs, )); - (vectorcraft_text::FontDb::global().generation(), crate::proof::overprints(f.opts), highlights).hash(&mut h); + (vectorcraft_text::layout_inputs(), crate::proof::overprints(f.opts), highlights).hash(&mut h); } if reads.plugins || (doc.plugins && (reads.symbols || reads.patterns)) { (effects::plugin_revision(), doc.names, doc.patterns).hash(&mut h); diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 141c3524c5..6a92d91e8f 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -65,8 +65,8 @@ pub struct Renderer { /// The latest cached glyph geometry for each text object id. A move changes the node's Arc /// but not its glyphs in text space, so the old geometry can still be used after comparison. text_ids: PtrMap, - /// The [`vectorcraft_text::FontDb::generation`] `texts` was laid out with: type lays out - /// again when fonts are added or rescanned. + /// The [`vectorcraft_text::layout_inputs`] `texts` was laid out with: type lays out again + /// when fonts are added or rescanned and when the hyphenation exceptions change. text_fonts: u64, /// Clip regions per clipping path (see [`Self::clip_of`]). clips: FrameCache, diff --git a/crates/render/src/live.rs b/crates/render/src/live.rs index 4d41e16af8..3777c98948 100644 --- a/crates/render/src/live.rs +++ b/crates/render/src/live.rs @@ -127,8 +127,9 @@ struct Walked { } impl Walked { - /// The [`resources_key`] of the node walked: the walk's hash, the font generation when it found - /// type or a placed file, the plug-in revision and the hash of the document's swatch and pattern + /// The [`resources_key`] of the node walked: the walk's hash, the layout inputs (fonts and + /// hyphenation exceptions, [`vectorcraft_text::layout_inputs`]) when it found type or a placed + /// file, the plug-in revision and the hash of the document's swatch and pattern /// names (`names`) when it found a plug-in effect, and the color settings `cms`. Whether a /// plug-in paints its pieces depends on the plug-ins installed, so the names count for every /// plug-in effect. @@ -136,7 +137,7 @@ impl Walked { let mut h = DefaultHasher::new(); self.hash.hash(&mut h); if self.text { - vectorcraft_text::FontDb::global().generation().hash(&mut h); + vectorcraft_text::layout_inputs().hash(&mut h); } if self.plugins { crate::effects::plugin_revision().hash(&mut h); diff --git a/crates/render/src/pattern.rs b/crates/render/src/pattern.rs index 2372abdbf2..317a423681 100644 --- a/crates/render/src/pattern.rs +++ b/crates/render/src/pattern.rs @@ -39,7 +39,8 @@ type TileKey = (String, u64, Ink, bool); /// What a tile was drawn from besides its pattern's own art, for art that shows more than itself: /// the art of every pattern and symbol and the pixels of every image in the document, the -/// document's other data (which holds its brushes), and the fonts. Editing a symbol leaves the art +/// document's other data (which holds its brushes), and the fonts and hyphenation exceptions +/// ([`vectorcraft_text::layout_inputs`]). Editing a symbol leaves the art /// of a pattern that shows it as it was, so the art alone kept the tile from before (#1723). Held /// weakly: a weak reference keeps the allocation, so no other value gets one of these addresses /// while the tile is kept. @@ -66,13 +67,13 @@ impl Deps { art: Self::art(doc).map(Arc::downgrade).collect(), pixels: Self::pixels(doc).map(Arc::downgrade).collect(), unknown: vectorcraft_doc::Shared::downgrade(&doc.unknown), - fonts: vectorcraft_text::FontDb::global().generation(), + fonts: vectorcraft_text::layout_inputs(), } } /// Does `doc` hold what the tile was drawn from? fn current(&self, doc: &Document) -> bool { - self.fonts == vectorcraft_text::FontDb::global().generation() + self.fonts == vectorcraft_text::layout_inputs() && std::ptr::eq(self.unknown.as_ptr(), &*doc.unknown) && self.art.iter().map(Weak::as_ptr).eq(Self::art(doc).map(Arc::as_ptr)) && self.pixels.iter().map(Weak::as_ptr).eq(Self::pixels(doc).map(Arc::as_ptr)) diff --git a/crates/render/src/renderer/text.rs b/crates/render/src/renderer/text.rs index b4a0dd0d2e..945ce5303b 100644 --- a/crates/render/src/renderer/text.rs +++ b/crates/render/src/renderer/text.rs @@ -55,7 +55,7 @@ impl Renderer { /// objects) hit for them. Other art is placed again in every frame, and so is art that holds /// symbol instances, type with inline graphics, images, placed documents or raster effects /// ([`depends_on_document`]), and art that the cache has no room for. Art that holds type is - /// placed again when the fonts change. + /// placed again when the fonts or the hyphenation exceptions change. pub(super) fn placed_art(&mut self, art: &Arc, xf: Affine, scale_strokes: bool, inst: Option<&Node>) -> (Arc, bool) { let kept_kind = matches!(art.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) || vectorcraft_doc::live::is_live(art); // An instance with appearance changes of its own isn't kept: the key doesn't tell them apart. @@ -67,7 +67,7 @@ impl Renderer { let key = (Arc::as_ptr(art) as usize, xf.as_coeffs().map(f64::to_bits), scale_strokes, stain); if let Some(e) = self.placed_arts.get(&key, self.stamp) && std::ptr::eq(e.art.as_ptr(), Arc::as_ptr(art)) - && e.fonts.is_none_or(|g| g == vectorcraft_text::FontDb::global().generation()) + && e.fonts.is_none_or(|g| g == vectorcraft_text::layout_inputs()) && e.slice.is_none() { return match &e.placed { @@ -106,7 +106,7 @@ impl Renderer { if keeps && let Some(e) = self.placed_arts.get(&key, self.stamp) && std::ptr::eq(e.art.as_ptr(), Arc::as_ptr(art)) - && e.fonts.is_none_or(|g| g == vectorcraft_text::FontDb::global().generation()) + && e.fonts.is_none_or(|g| g == vectorcraft_text::layout_inputs()) && e.slice.as_ref() == Some(map) { match e.placed.clone() { @@ -134,7 +134,7 @@ impl Renderer { fn keep_placed(&mut self, key: PlacedKey, art: &Arc, placed: &Arc, slice: Option) -> bool { let kept = (!depends_on_document(art)).then(|| placed.clone()); let keeps = kept.is_some(); - let fonts = (keeps && holds_type(art)).then(|| vectorcraft_text::FontDb::global().generation()); + let fonts = (keeps && holds_type(art)).then(vectorcraft_text::layout_inputs); // The bytes count the key and the entry, the art's allocation that the weak reference // keeps, and the kept art. let bytes = size_of::<(PlacedKey, PlacedEntry, Node)>() + kept.as_deref().map_or(0, lru::node_bytes); @@ -150,7 +150,7 @@ impl Renderer { if !matches!(&a.kind, NodeKind::Text(own) if std::ptr::eq(&**own, t)) { return (Arc::new(text_geom(t)), false); } - let fonts = vectorcraft_text::FontDb::global().generation(); + let fonts = vectorcraft_text::layout_inputs(); if fonts != self.text_fonts { self.texts.clear(); self.text_ids.clear(); diff --git a/crates/render/src/tests/fontchange.rs b/crates/render/src/tests/fontchange.rs index bedffb6b4b..15389cd805 100644 --- a/crates/render/src/tests/fontchange.rs +++ b/crates/render/src/tests/fontchange.rs @@ -3,16 +3,16 @@ use std::sync::{PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard}; -use vectorcraft_doc::text::{CharStyle, TextObject}; +use vectorcraft_doc::text::{CharStyle, TextKind, TextObject}; use vectorcraft_doc::{Node, NodeKind}; -use vectorcraft_geom::Point; +use vectorcraft_geom::{PathData, Point}; use crate::*; -/// Taken to add fonts, or to look up an installed one by name (`FontDb::face` loads it, which -/// changes the fonts too). Renderers lay type out again when the fonts change, so tests of what -/// they keep between frames hold it shared ([`fonts_steady`]) and the tests that change the fonts -/// hold it alone ([`adding_fonts`]). +/// Taken to add fonts, to look up an installed one by name (`FontDb::face` loads it, which +/// changes the fonts too) or to change the hyphenation exceptions. Renderers lay type out again +/// when the fonts or the exceptions change, so tests of what they keep between frames hold it +/// shared ([`fonts_steady`]) and the tests that change them hold it alone ([`adding_fonts`]). static FONTS: RwLock<()> = RwLock::new(()); /// No fonts are added by these tests until the guard is dropped. @@ -29,14 +29,12 @@ pub(crate) fn adding_fonts() -> RwLockWriteGuard<'static, ()> { /// What `step` returns from a run in which the fonts stayed the same. The fonts can change while /// [`fonts_steady`] is held: the first lookup by family name catalogs the installed fonts, and /// resolving a cataloged family, or a character no loaded font has, loads a face. `step` runs up -/// to 100 times, until [`vectorcraft_text::FontDb::generation`] is the same after a run as before -/// it. +/// to 100 times, until [`vectorcraft_text::layout_inputs`] is the same after a run as before it. pub(crate) fn with_fonts_unchanged(mut step: impl FnMut() -> T) -> T { - let db = vectorcraft_text::FontDb::global(); for _ in 0..100 { - let before = db.generation(); + let before = vectorcraft_text::layout_inputs(); let out = step(); - if db.generation() == before { + if vectorcraft_text::layout_inputs() == before { return out; } } @@ -466,6 +464,75 @@ fn enveloped_type_under_effects_redraws_when_its_font_becomes_available() { } } +/// Hyphenated area type redraws with its new line breaks when Preferences › Hyphenation › +/// Exceptions change: its glyph geometry, the pixels of its raster effects, the art drawn for +/// geometry effects, the art kept for a symbol instance and an envelope's evaluation are made +/// again. +#[test] +fn hyphenated_type_redraws_when_the_exceptions_change() { + let _adding = adding_fonts(); + struct Clear; + impl Drop for Clear { + fn drop(&mut self) { + vectorcraft_text::set_hyphenation_exceptions(""); + } + } + let _clear = Clear; + use vectorcraft_doc::Effect; + let moved = || Effect { id: "distort.transform".into(), params: serde_json::json!({"moveH": 0.0}), visible: true }; + const WORDS: &str = "Extraordinarily comprehensive typographical considerations notwithstanding everything."; + const NEVER: &str = "extraordinarily, comprehensive, typographical, considerations, notwithstanding"; + let cases = ["the area type", "the type with a drop shadow", "the group with a Transform effect", "the symbol instance", "the envelope"]; + for (i, what) in cases.into_iter().enumerate() { + vectorcraft_text::set_hyphenation_exceptions(""); + let mut d = Document::new(300.0, 150.0); + let mut t = TextObject::point(Point::ZERO, WORDS, CharStyle { size: 14.0, ..Default::default() }); + t.kind = TextKind::Area { frame: PathData::from_bezpath(&Rect::new(10.0, 10.0, 130.0, 145.0).to_path(0.1)) }; + t.xf = Affine::IDENTITY; + t.para.hyphenate = true; + let mut object = Node::new(d.alloc_id(), NodeKind::Text(Box::new(t))); + match i { + 0 => {} + 1 => { + let params = serde_json::json!({"x": 4, "y": 4, "blur": 1, "opacity": 100}); + object.appearance.effects.push(Effect { id: "stylize.dropShadow".into(), params, visible: true }); + } + 2 => { + object = Node::group(d.alloc_id(), vec![Arc::new(object)]); + object.appearance.effects.push(moved()); + } + 3 => { + object.appearance.effects.push(moved()); + let art = Node::group(d.alloc_id(), vec![Arc::new(object)]); + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Words".into(), art: Arc::new(art) }); + object = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Words".into(), xf: Affine::IDENTITY }); + } + _ => { + let kind = vectorcraft_doc::EnvelopeKind::Warp { style: "arc".into(), bend: 30.0, h: 0.0, v: 0.0, horizontal: true, version: 0 }; + let envelope = NodeKind::Envelope { + content: vec![Arc::new(object)], + kind: Box::new(kind), + fidelity: 50.0, + editing: false, + options: Default::default(), + frame: Box::new(Affine::IDENTITY), + }; + object = Node::new(d.alloc_id(), envelope); + } + } + let l = d.layers[0].id; + d.insert(Some(l), 0, object).unwrap(); + let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() }; + let mut r = Renderer::new(); + let before = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels; + vectorcraft_text::set_hyphenation_exceptions(NEVER); + let after = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels; + let fresh = Renderer::new().render(&d, 300, 150, Affine::IDENTITY, &opts).pixels; + assert_ne!(before, fresh, "{what}: the words in the list are not broken"); + assert!(after == fresh, "{what}: drawn as a new renderer draws it"); + } +} + /// Without an engine session, a renderer maps an envelope of type from its glyph outline bounds, /// and measures the type again when its font becomes available. #[test] diff --git a/crates/render/src/tests/memory.rs b/crates/render/src/tests/memory.rs index d286fa7e3c..1f7a49698c 100644 --- a/crates/render/src/tests/memory.rs +++ b/crates/render/src/tests/memory.rs @@ -320,13 +320,13 @@ fn kept_glyphs(r: &mut Renderer, d: &Document) -> Vec>> { .collect() } -/// `f`'s result from a run during which the fonts did not change: other tests add fonts, and the -/// renderer lays all type out again when the fonts change. +/// `f`'s result from a run during which the fonts and the hyphenation exceptions did not change: +/// other tests change them, and the renderer lays all type out again when they change. fn fonts_unchanged(mut f: impl FnMut() -> T) -> T { loop { - let fonts = vectorcraft_text::FontDb::global().generation(); + let inputs = vectorcraft_text::layout_inputs(); let t = f(); - if vectorcraft_text::FontDb::global().generation() == fonts { + if vectorcraft_text::layout_inputs() == inputs { return t; } } diff --git a/crates/render/src/tests/typeedges.rs b/crates/render/src/tests/typeedges.rs index fae0207d0d..3295342ce8 100644 --- a/crates/render/src/tests/typeedges.rs +++ b/crates/render/src/tests/typeedges.rs @@ -859,7 +859,7 @@ fn type_outside_its_frame_is_drawn() { fn a_pan_draws_the_type_from_the_kept_pixels() { let _fonts = crate::tests_fontchange::fonts_steady(); let opts = RenderOptions::default(); - let fonts = || vectorcraft_text::FontDb::global().generation(); + let fonts = vectorcraft_text::layout_inputs; // How many type objects a renderer drew offscreen for `b` in `second` after drawing `a` in // `first`. The second frame's pixels are a new renderer's, each channel within `near`. The // frames are drawn again while the fonts change between them (other tests load fonts, and the diff --git a/crates/text/src/hyphen.rs b/crates/text/src/hyphen.rs index e293346279..c940ed85a4 100644 --- a/crates/text/src/hyphen.rs +++ b/crates/text/src/hyphen.rs @@ -502,16 +502,16 @@ mod tests { } /// Process-wide store used by `prefs.set` / restore. Each change makes envelopes measure type - /// again. + /// again, and caches of layouts lay type out again. #[test] fn set_hyphenation_exceptions_round_trips_the_source_string() { - let inputs = crate::layout::envelope_ink_inputs(); + let inputs = crate::layout_inputs(); set_hyphenation_exceptions("typography, hap-pen"); assert_eq!(hyphenation_exceptions(), "typography, hap-pen"); - let changed = crate::layout::envelope_ink_inputs(); - assert_ne!(changed, inputs, "the envelope inputs change with the exceptions"); + let changed = crate::layout_inputs(); + assert_ne!(changed, inputs, "the layout inputs change with the exceptions"); set_hyphenation_exceptions(""); assert!(hyphenation_exceptions().is_empty()); - assert_ne!(crate::layout::envelope_ink_inputs(), changed, "and again when they are cleared"); + assert_ne!(crate::layout_inputs(), changed, "and again when they are cleared"); } } diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 82a849a3dc..a7d7d1d3f3 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -46,9 +46,7 @@ pub(crate) use driver::{fit_passes, layout_faces_per_call}; pub(crate) use on_path::{layout_trying_each_start, path_arrangements}; use vertical::{TcyStyle, stands_upright, tate_chu_yoko, upright_cell, upright_centre, upright_extent, whole_character_widths}; -#[cfg(test)] -pub(crate) use driver::envelope_ink_inputs; -pub use driver::{envelope_ink, register_envelope_ink}; +pub use driver::{envelope_ink, layout_inputs, register_envelope_ink}; const EPS: f64 = 1e-6; diff --git a/crates/text/src/layout/driver.rs b/crates/text/src/layout/driver.rs index 9108cc171d..59c6ac38bf 100644 --- a/crates/text/src/layout/driver.rs +++ b/crates/text/src/layout/driver.rs @@ -191,12 +191,13 @@ pub fn envelope_ink(t: &TextObject, m: Affine, projection: Option<(vectorcraft_g /// Make envelopes map type from [`envelope_ink`], measured again when the fonts or the hyphenation /// exceptions change (the first registration stays, see `vectorcraft_doc::live::set_type_ink`). pub fn register_envelope_ink() { - vectorcraft_doc::live::set_type_ink(envelope_ink, envelope_ink_inputs); + vectorcraft_doc::live::set_type_ink(envelope_ink, layout_inputs); } -/// A number that changes whenever what [`envelope_ink`] reads besides the type changes: the -/// installed fonts and the hyphenation exceptions. -pub(crate) fn envelope_ink_inputs() -> u64 { +/// A number that changes when the fonts ([`FontDb::generation`]) or the hyphenation exceptions +/// (Preferences › Hyphenation › Exceptions) change. Caches of layouts made with the global font +/// database, and of art made from them, key on it. +pub fn layout_inputs() -> u64 { FontDb::global().generation().rotate_left(32) ^ crate::hyphen::exceptions_generation() } diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 2ff6b9ad5b..6ae85931ca 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -47,7 +47,7 @@ pub use fontdb::{ user_font_dirs, }; pub use hyphen::{hyphenation_exceptions, set_hyphenation_exceptions}; -pub use layout::{envelope_ink, register_envelope_ink}; +pub use layout::{envelope_ink, layout_inputs, register_envelope_ink}; use std::sync::Arc; pub use svg_glyph::{SvgGlyphReader, set_svg_glyph_reader}; diff --git a/crates/tools/src/text.rs b/crates/tools/src/text.rs index bd0c229ddf..763325f90e 100644 --- a/crates/tools/src/text.rs +++ b/crates/tools/src/text.rs @@ -134,7 +134,7 @@ fn text_hash(s: &str) -> u64 { h.finish() } -/// The edited text's layout, keyed by the font database's generation and the text. +/// The edited text's layout, keyed by [`vectorcraft_text::layout_inputs`] and the text. type CachedLayout = ((u64, TextObject), Arc); /// Process-global Type-tool edit-session counter: the IME target's replacement epoch. @@ -281,9 +281,9 @@ impl TypeTool { } /// Layout of `t`, cached by content (overlays run every frame) and by the fonts available (the - /// web build adds fonts after startup). + /// web build adds fonts after startup) and the hyphenation exceptions. fn layout(&self, t: &TextObject) -> Arc { - let fonts = FontDb::global().generation(); + let fonts = vectorcraft_text::layout_inputs(); let mut c = self.cache.lock().unwrap_or_else(|e| e.into_inner()); if let Some(((g, k), l)) = c.as_ref() && *g == fonts diff --git a/crates/tools/src/text/ports.rs b/crates/tools/src/text/ports.rs index 554a1cd611..6928fbe6f0 100644 --- a/crates/tools/src/text/ports.rs +++ b/crates/tools/src/text/ports.rs @@ -167,7 +167,8 @@ pub(crate) fn port_near(cx: &ToolContext, p: Point) -> Option<(NodeId, Port, f64 } /// Which selected text overflows, for [`thread_ports`]: each text object's layout verdict kept -/// with the object and the fonts it was made from, as overlays run every frame. +/// with the object and the layout inputs it was made with (fonts and hyphenation exceptions, +/// [`vectorcraft_text::layout_inputs`]), as overlays run every frame. #[derive(Default)] pub struct PortMarks { seen: std::collections::HashMap, @@ -178,7 +179,7 @@ pub struct PortMarks { /// port whether text is hidden by the size of its frame, whatever tool is active. None when more /// than [`PORTS_MAX`] of them are selected. pub fn thread_ports(marks: &mut PortMarks, cx: &ToolContext) -> Vec { - let fonts = FontDb::global().generation(); + let fonts = vectorcraft_text::layout_inputs(); let mut o = Vec::new(); let mut kept = std::collections::HashMap::new(); if !ports_shown(cx.doc, &cx.selection.objects) { diff --git a/crates/tools/src/text/tests.rs b/crates/tools/src/text/tests.rs index a7f302db65..94063166bc 100644 --- a/crates/tools/src/text/tests.rs +++ b/crates/tools/src/text/tests.rs @@ -1538,6 +1538,36 @@ fn selected_type_shows_its_ports_in_each_state() { assert!(thread_ports(&mut marks, &cx(&d, &sel, &p)).is_empty(), "only selected type shows them"); } +/// The out port shows whether the text fits as the hyphenation exceptions lay it out now: when +/// Preferences › Hyphenation › Exceptions change, the selected type is laid out again. +#[test] +fn the_out_port_follows_the_hyphenation_exceptions() { + struct Clear; + impl Drop for Clear { + fn drop(&mut self) { + vectorcraft_text::set_hyphenation_exceptions(""); + } + } + let _clear = Clear; + const WORDS: &str = "Extraordinarily comprehensive typographical considerations notwithstanding everything."; + const NEVER: &str = "extraordinarily, comprehensive, typographical, considerations, notwithstanding"; + let (mut d, id) = crate::testutil::doc_with_area_type(); + // A 70 by 170 pt frame holds the hyphenated text, and the text without those breaks overflows + // it. + let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) else { panic!("not text") }; + t.runs = vec![TextRun::new(WORDS, CharStyle { size: 14.0, ..Default::default() })]; + t.para.hyphenate = true; + t.kind = TextKind::Area { frame: vectorcraft_geom::PathData::from_bezpath(&Rect::new(0.0, 0.0, 70.0, 170.0).to_path(0.1)) }; + let (mut sel, p) = (Selection::default(), paint()); + sel.set([id]); + let mut marks = PortMarks::default(); + let red = |ov: &[Overlay]| ov.iter().filter(|o| matches!(o, Overlay::Path { color: [230, 30, 30], .. })).count(); + vectorcraft_text::set_hyphenation_exceptions(""); + assert_eq!(red(&thread_ports(&mut marks, &cx(&d, &sel, &p))), 0, "the hyphenated text fits"); + vectorcraft_text::set_hyphenation_exceptions(NEVER); + assert_eq!(red(&thread_ports(&mut marks, &cx(&d, &sel, &p))), 1, "the text without those breaks does not"); +} + /// Past [`PORTS_MAX`] selected frames (select all of an opened PDF) no ports are drawn or clicked: /// they would bury the canvas in boxes; up to it every frame has its two. #[test] diff --git a/crates/tools/src/touchtype.rs b/crates/tools/src/touchtype.rs index 172487c197..018a65aefc 100644 --- a/crates/tools/src/touchtype.rs +++ b/crates/tools/src/touchtype.rs @@ -99,7 +99,7 @@ struct Drag { began: bool, } -/// The type object's layout, keyed by the font database's generation and the object. +/// The type object's layout, keyed by [`vectorcraft_text::layout_inputs`] and the object. type CachedLayout = ((u64, TextObject), Arc); #[derive(Default)] @@ -196,9 +196,10 @@ fn char_box(t: &TextObject, lay: &TextLayout, gi: usize) -> Option { } impl TouchTypeTool { - /// Layout of `t`, cached by content (overlays run every frame) and by the fonts available. + /// Layout of `t`, cached by content (overlays run every frame), by the fonts available and by + /// the hyphenation exceptions. fn layout(&self, t: &TextObject) -> Arc { - let fonts = FontDb::global().generation(); + let fonts = vectorcraft_text::layout_inputs(); let mut c = self.cache.lock().unwrap_or_else(|e| e.into_inner()); if let Some(((g, k), l)) = c.as_ref() && *g == fonts