From abe89d5176bac389defa16f4887daa60d5fd8d00 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 13 Oct 2026 17:26:22 -0700 Subject: [PATCH] Type: Show Hidden Characters keeps no text nodes while it is off or no document is open Show Hidden Characters keeps each text object's marks together with the node it laid them out from. Only a frame that drew the overlay changed what it kept. After the option was turned off, or the document was closed, those nodes stayed alive until the overlay was next drawn. The canvas now takes the kept marks at the start of each frame and puts them back only when it draws the overlay. A frame that does not draw it, for example with the option off or with no document open, drops the marks and their nodes. A frame that draws the overlay still keeps the marks of the frame before. --- crates/ui-egui/src/canvas.rs | 72 ++++++++++++++++++++++++++++++++++-- crates/ui-egui/src/lib.rs | 4 +- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 2bf7ea77..7aa8ab79 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -153,6 +153,11 @@ pub fn fit(app: &mut VectorcraftApp, how: &str) { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let full = ui.available_rect_before_wrap(); + // `hidden_chars` holds the marks Show Hidden Characters kept and the text nodes they were laid + // out from. Only the assignment right before the overlay call below puts them back. A frame + // that does not draw the overlay drops them, for example with the option off, with the edges + // hidden, on the Home screen or with no document open. + let hidden_chars = std::mem::take(&mut app.canvas.hidden_chars); // A document that opened, closed or became active since Home was chosen replaces it. if app.ui.home.is_some_and(|k| k != crate::menus::home_key(app)) { app.ui.home = None; @@ -438,6 +443,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { thread_overlay(app, &painter, &xf); } if app.ui.view.hidden_chars { + app.canvas.hidden_chars = hidden_chars; hidden_chars_overlay(app, &painter, &xf); } } @@ -1530,8 +1536,8 @@ fn hidden_chars_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: &Xf) { let color = Color32::from_rgb(0x4f, 0x9d, 0xff); let font = egui::FontId::proportional(((10.0 * xf.zoom) as f32).clamp(7.0, 18.0)); let kept = &mut app.canvas.hidden_chars; - // `last` holds the marks of the last frame that showed them. The marks of the objects this - // frame doesn't draw are dropped at the end. + // `last` holds the marks of the last frame. The marks of the objects this frame doesn't draw + // are dropped at the end. let mut last = std::mem::take(&mut kept.marks); // The inputs are read before any layout: when fonts load during this frame, the next frame // lays all type out again. @@ -1569,7 +1575,7 @@ fn hidden_chars_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: &Xf) { }); } -/// The Show Hidden Characters marks of the text objects drawn in the last frame that showed them +/// The Show Hidden Characters marks of the text objects drawn in the last frame /// ([`crate::CanvasCache::hidden_chars`]). Each is kept with the object it was laid out from: an /// edited object is a new `Arc` and is laid out again, and all type is laid out again when the /// fonts or the hyphenation exceptions change. @@ -3763,6 +3769,66 @@ mod tests { assert_ne!(drawn(&mut app, &xf), zoomed, "set in the family that arrived"); } + /// The shapes Type → Show Hidden Characters draws in one canvas frame ([`show`]). + fn hidden_char_marks(app: &mut VectorcraftApp, ctx: &egui::Context) -> Vec { + let blue = Color32::from_rgb(0x4f, 0x9d, 0xff); + shapes(app, ctx) + .into_iter() + .filter(|s| matches!(s, Shape::Circle(c) if c.fill == blue) || matches!(s, Shape::Text(t) if t.fallback_color == blue)) + .collect() + } + + /// While Show Hidden Characters is on, a canvas frame keeps the marks of the frame before. + /// Turning it off drops the marks with their text nodes, and turning it on again draws the + /// same marks. + #[test] + fn show_hidden_characters_off_releases_the_text_nodes() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + let text = json!({"x": 20, "y": 60, "text": "one two\nthree four"}); + let id = vectorcraft_doc::NodeId(app.session.execute("text.create", &text).unwrap()["id"].as_u64().unwrap()); + let ctx = egui::Context::default(); + app.run("type.hiddenCharacters", json!({})).unwrap(); + let on = hidden_char_marks(&mut app, &ctx); + assert!(on.len() > 2, "{on:?}"); + // Tests running alongside add fonts and set hyphenation exceptions, which lays all type out + // again: frames are drawn until neither changed between two of them. + let kept = (0..100).any(|_| { + let (inputs, marks) = (app.canvas.hidden_chars.inputs.clone(), app.canvas.hidden_chars.marks[&id].1.clone()); + hidden_char_marks(&mut app, &ctx); + let after = &app.canvas.hidden_chars; + after.inputs == inputs && std::sync::Arc::ptr_eq(&after.marks[&id].1, &marks) + }); + assert!(kept, "a frame keeps the marks of the frame before"); + app.run("type.hiddenCharacters", json!({})).unwrap(); + assert!(hidden_char_marks(&mut app, &ctx).is_empty(), "off"); + assert!(app.canvas.hidden_chars.marks.is_empty(), "no node is kept"); + app.run("type.hiddenCharacters", json!({})).unwrap(); + assert_eq!(hidden_char_marks(&mut app, &ctx), on, "on again: the same marks"); + } + + /// Closing the document drops the marks Show Hidden Characters kept with its text nodes, and + /// the same type in a new document shows the same marks. + #[test] + fn show_hidden_characters_releases_the_text_nodes_of_a_closed_document() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + app.run("type.hiddenCharacters", json!({})).unwrap(); + let open = |app: &mut VectorcraftApp| { + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + app.session.execute("text.create", &json!({"x": 20, "y": 60, "text": "one two\nthree four"})).unwrap(); + hidden_char_marks(app, &ctx) + }; + let first = open(&mut app); + assert!(first.len() > 2, "{first:?}"); + assert_eq!(app.canvas.hidden_chars.marks.len(), 1, "the marks are kept with the text object's node"); + app.session.execute("file.close", &json!({})).unwrap(); + assert!(hidden_char_marks(&mut app, &ctx).is_empty(), "the Home screen"); + assert!(app.canvas.hidden_chars.marks.is_empty(), "no node of the closed document is kept"); + assert_eq!(open(&mut app), first, "the same type in a new document"); + } + /// General › Zoom with Mouse Wheel (#394), through the control channel's `ui.wheel`: off, /// the wheel scrolls and Cmd-wheel zooms; on, the wheel zooms about the pointer, Shift-wheel /// scrolls up and down and Cmd/Ctrl-wheel sideways. Alt-wheel (Option on the Mac) and a diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 012decbf..821c4e16 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -284,8 +284,8 @@ pub struct CanvasCache { pub slices: Option, /// The print tiling's pages for (document uid, revision) (View → Show Print Tiling). pub print_tiling: Option, - /// The marks Type → Show Hidden Characters drew on each text object in the last frame that - /// showed them. + /// The marks Type → Show Hidden Characters drew on each text object in the last frame. A frame + /// that does not draw them drops them ([`canvas::show`]). pub hidden_chars: canvas::HiddenChars, /// [`VectorcraftApp::selection_box`] for (document uid, revision, Use Preview Bounds). pub selection_box: Option<((u64, u64, bool), Option)>,