diff --git a/crates/ui-egui/src/idle_tests.rs b/crates/ui-egui/src/idle_tests.rs index 2d4e5757..3c71146d 100644 --- a/crates/ui-egui/src/idle_tests.rs +++ b/crates/ui-egui/src/idle_tests.rs @@ -288,3 +288,23 @@ fn an_animated_gif_wakes_only_the_presenter_and_only_at_its_frames() { h.app.ui.present = None; h.assert_idle("the presenter closed"); } + +#[test] +fn cjk_names_are_looked_for_in_edits_not_on_every_pan() { + let mut s = Session::new(); + let a = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].clone(); + let b = s.execute("shape.rectangle", &json!({"x": 20, "y": 0, "width": 10, "height": 10})).unwrap()["id"].clone(); + s.execute("node.set", &json!({"id": a, "name": "中文"})).unwrap(); + let mut h = Harness::new(s); + let ctx = h.ctx.clone(); + assert_eq!(h.app.cjk_needed(&ctx, 0), crate::cjk::HAN, "the first look reads every name"); + // A pan or zoom changes the revision but not the document: its hundreds of thousands of + // names aren't read again (that took 70 ms a frame in a 900 000-layer file). + let revision = h.app.session.ds().unwrap().revision; + h.run("view.set", json!({"zoom": 2, "cx": 300, "cy": 200})); + assert_ne!(h.app.session.ds().unwrap().revision, revision); + assert_eq!(h.app.cjk_needed(&ctx, 0), 0); + // An edit is read: the layers it changed. + h.run("node.set", json!({"id": b, "name": "한국어"})); + assert_eq!(h.app.cjk_needed(&ctx, 0), crate::cjk::HANGUL); +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 613f18f9..059ca364 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -156,6 +156,7 @@ mod windows_tests; use std::collections::BTreeSet; use std::path::PathBuf; +use std::sync::Arc; use figcraft_doc::NodeId; use figcraft_engine::Session; @@ -241,6 +242,9 @@ pub enum TitleBar { /// How often open documents get an autosave checkpoint (seconds). pub const AUTOSAVE_SECS: f64 = 30.0 * 60.0; +/// Most changed layers whose names are checked for CJK scripts one by one (beyond: all of them). +const CJK_CHANGES: usize = 10_000; + /// How long a toast shows (seconds). const TOAST_SECS: f64 = 3.0; @@ -483,8 +487,8 @@ pub struct FigApp { /// System faces for scripts the built-in fonts lack were added (after the font scan), with /// faces for these CJK scripts ([`cjk`]) in this UI language's order. script_fonts: Option<(u8, &'static str)>, - /// The tab (uid) and revision whose names were last checked for CJK scripts. - cjk_checked: Option<(u64, u64)>, + /// The tab (uid) and document version whose names were last checked for CJK scripts. + cjk_checked: Option<(u64, Arc)>, /// The language preference the UI strings follow (applied when it changes). language: Option, /// A field had the keyboard when the last frame ended. @@ -856,11 +860,24 @@ impl FigApp { }) }); let dragging = ctx.input(|i| i.pointer.any_down()); - if let Some(d) = self.session.docs.get(self.session.active).filter(|d| !dragging && self.cjk_checked != Some((d.uid, d.revision))) { - self.cjk_checked = Some((d.uid, d.revision)); - let doc = &d.doc; - let comments = doc.comments.iter().flat_map(|c| std::iter::once(&c.first).chain(&c.replies)).flat_map(|m| [&m.author, &m.text]); - need |= std::iter::once(&doc.name).chain(doc.nodes().map(|n| &n.name)).chain(comments).fold(0, |m, s| m | cjk::scripts(s)); + // Pans and zooms change the revision too: only a new version of the document is looked + // at, and only the layers it changed (a large file has hundreds of thousands of names). + let current = self.session.docs.get(self.session.active).filter(|_| !dragging).map(|d| (d.uid, d.doc.clone())); + if let Some((uid, doc)) = current { + let before = self.cjk_checked.take().filter(|(u, _)| *u == uid).map(|(_, b)| b); + if !before.as_ref().is_some_and(|b| Arc::ptr_eq(b, &doc)) { + let mut scan = |s: &str| need |= cjk::scripts(s); + scan(&doc.name); + match before.as_ref().and_then(|b| doc.changed_ids(b, CJK_CHANGES)) { + Some(ids) => ids.into_iter().filter_map(|id| doc.node(id)).for_each(|n| scan(&n.name)), + None => doc.nodes().for_each(|n| scan(&n.name)), + } + doc.comments.iter().flat_map(|c| std::iter::once(&c.first).chain(&c.replies)).for_each(|m| { + scan(&m.author); + scan(&m.text); + }); + } + self.cjk_checked = Some((uid, doc)); } need }