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