From 3f12cfdbbf1e16f05b1ff562cb3d2a221e0a7a4d Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 18:49:43 -0800 Subject: [PATCH 1/4] Control bar: idle frames read the selected object without copying it The Control bar and the contextual task bar copied the first selected object every frame (the task bar twice), which on a path of 200,000 anchors costs about 1.4 ms per copy. Both bars now borrow it; the task bar copies it only when it is a basic shape, for Shape Properties. With one selected open path of 200,000 anchors, an idle frame with both bars shown takes 3.8 ms instead of 8.0 ms in a release build. --- crates/ui-egui/src/canvas/task_bar.rs | 18 ++++++++++-------- crates/ui-egui/src/chrome/control_bar.rs | 7 ++++--- crates/ui-egui/src/tests/place.rs | 23 +++++++++++++++++++++++ docs/target-app-parity.md | 2 ++ 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/canvas/task_bar.rs b/crates/ui-egui/src/canvas/task_bar.rs index e6579d8f3..b5f8dac66 100644 --- a/crates/ui-egui/src/canvas/task_bar.rs +++ b/crates/ui-egui/src/canvas/task_bar.rs @@ -48,7 +48,7 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { let Some(b) = (if on_artboards { picked_artboards_bounds(app) } else { app.selection_bounds() }) else { return }; let Some(st) = app.session.active() else { return }; let n = st.selection.len(); - let first = st.selection.objects.first().and_then(|id| st.doc.node(*id)).cloned(); + let first = st.selection.objects.first().and_then(|id| st.doc.node(*id)); let is_group = first.as_ref().is_some_and(|f| matches!(f.kind, NodeKind::Group { .. })); let is_image = first.as_ref().is_some_and(|f| matches!(f.kind, NodeKind::Image(_))); let is_clip_group = first.as_ref().is_some_and(|f| matches!(f.kind, NodeKind::Group { clip: true, .. })); @@ -72,13 +72,15 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { // Selection tool: its properties take the place of Lock, as in the reference app // (). use vectorcraft_doc::LiveShape; - let shape = first.clone().filter(|f| { - n == 1 - && st.selection.anchors.is_empty() - && app.session.tool_id() == "selection" - && matches!(&f.kind, NodeKind::Path { live: Some(l), .. } - if matches!(**l, LiveShape::Rectangle { .. } | LiveShape::Ellipse { .. } | LiveShape::Polygon { .. })) - }); + let shape = first + .filter(|f| { + n == 1 + && st.selection.anchors.is_empty() + && app.session.tool_id() == "selection" + && matches!(&f.kind, NodeKind::Path { live: Some(l), .. } + if matches!(**l, LiveShape::Rectangle { .. } | LiveShape::Ellipse { .. } | LiveShape::Polygon { .. })) + }) + .cloned(); let mut items: Vec<(&str, &str, &str, serde_json::Value)> = vec![]; // (label, icon, command, params) let none = || json!({}); if on_artboards { diff --git a/crates/ui-egui/src/chrome/control_bar.rs b/crates/ui-egui/src/chrome/control_bar.rs index df58c1be7..018a4e2d9 100644 --- a/crates/ui-egui/src/chrome/control_bar.rs +++ b/crates/ui-egui/src/chrome/control_bar.rs @@ -203,9 +203,9 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let Some(st) = app.session.active() else { return }; let sel = st.selection.objects.clone(); let units = app.session.general_unit(); - let first = sel.first().and_then(|id| st.doc.node(*id)).cloned(); + let first = sel.first().and_then(|id| st.doc.node(*id)); let anchors = anchor_controls(app); - let label = match &first { + let label = match first { _ if app.session.tool_id() == "artboard" => tl!("Artboard"), Some(_) if sel.len() == 1 && anchors != AnchorControls::None => { tl!("Anchor Point") @@ -222,6 +222,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { } _ => tl!(crate::panels::appearance::object_label(app)), }; + let has_first = first.is_some(); ui.label(egui::RichText::new(label).font(theme::semibold(12.0)).color(t.text)); // With the Artboard tool in use, the bar shows the active artboard's controls in place // of the object controls. @@ -238,7 +239,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { crate::toolbar::control_bar_options(app, ui); crate::dialogs::envelope::control_bar(app, ui); crate::panels::symbols::control_bar(app, ui); - let opacity = if first.is_some() { crate::panels::current_transparency(app).map_or(1.0, |t| t.0) } else { 1.0 }; + let opacity = if has_first { crate::panels::current_transparency(app).map_or(1.0, |t| t.0) } else { 1.0 }; if !image { let shown_stroke = crate::panels::current_stroke(app); let mixed = crate::panels::stroke_mixed(app, ui.ctx()); diff --git a/crates/ui-egui/src/tests/place.rs b/crates/ui-egui/src/tests/place.rs index e4c9d47ef..9406ba2c9 100644 --- a/crates/ui-egui/src/tests/place.rs +++ b/crates/ui-egui/src/tests/place.rs @@ -488,6 +488,29 @@ fn the_control_bar_shows_the_image_file_link_colour_mode_and_ppi() { assert!(text.contains("Embedded"), "{text}"); } +/// The Control bar's label is read from the first selected object each frame, so it follows an +/// edit, Undo, a switch to another document and back, and a selection change. +#[test] +fn the_control_bar_label_follows_edits_undo_documents_and_selection() { + let mut app = app(); + let path = temp_file("label.png", &png(30, 30, 72.0)); + app.run("file.place", json!({"path": path})).unwrap(); + let text = |app: &mut VectorcraftApp| crate::tests_labels::painted_text(app, crate::chrome::control_bar); + assert!(text(&mut app).contains("Linked File")); + app.run("links.embed", json!({})).unwrap(); + let embedded = text(&mut app); + assert!(embedded.contains("Embedded") && !embedded.contains("Linked File"), "{embedded}"); + app.run("edit.undo", json!({})).unwrap(); + assert!(text(&mut app).contains("Linked File")); + app.run("file.new", json!({"width": 100, "height": 100})).unwrap(); + assert!(!text(&mut app).contains("Linked File"), "the new document has nothing selected"); + app.session.set_active(0); + assert!(text(&mut app).contains("Linked File")); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 30})).unwrap(); + let rect = text(&mut app); + assert!(!rect.contains("Linked File") && !rect.contains("Embedded"), "{rect}"); +} + /// Click the Control bar's `label`. fn click_label(app: &mut VectorcraftApp, ctx: &egui::Context, label: &str) { use crate::tests_removeanchors::{at, click_control, control_frame}; diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 3763b806a..15ab8bed6 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1314,6 +1314,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Live objects in documents with many symbols:** a live object, or an object with effects, that shows symbols or patterns is not walked again in a later frame to find them while the document's symbols and patterns stay the same. One that shows a placed file is walked in every frame. At 1000 × 1000 pixels, a page of 2,000 envelopes around instances of the last 2,000 of 10,000 symbols takes 30 ms a frame, and 83 ms when every frame walks them again, with the same pixels. +- **Control bar and task bar on large paths:** the Control bar and the contextual task bar read the first selected object in place each frame instead of copying it; the task bar copies the object only when it is a basic shape, for its Shape Properties. With one selected open path of 200,000 anchors, an idle frame with both bars shown takes 3.8 ms in a release build instead of 8.0 ms (medians of five interleaved runs). Test: `the_control_bar_label_follows_edits_undo_documents_and_selection`. + - **Type drawn as glyph outlines:** under a geometry effect, in EMF and WMF export, in DXF export when it is written as outlines, flattened with Convert All Text to Outlines, and inside an envelope, type keeps its underline and strikethrough, the weight and dashes of its character strokes when it is scaled, and the placement of its gradients and patterns, as the canvas draws it. A group's or layer's own fills and strokes also paint the underline and strikethrough of the type in it. - **Texture redraws (#1257):** Craquelure, Mosaic Tiles and Stained Glass compare squared distances when finding their nearest cells; rounding/tie boundaries use the original distance ordering. Brute-force candidate tests and three 1800×1500 viewport comparisons preserve the old pixels. A CMYK rectangle move at 190.07%/DPI 1.5 is reproducible with `cargo test -p vectorcraft-render texture_drag_perf -- --ignored --nocapture`; idle-machine Windows interaction is still unverified. From dba21932342b5c0b4e40924ab9b14937be8dc0ec Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 19:07:21 -0800 Subject: [PATCH 2/4] Tests: the task bar follows the active document after a switch A test selects an open path in one document and a rectangle in a new one, then switches back and forth with set_active: the task bar offers Join only for the open path and Shape Properties only for the rectangle. --- crates/ui-egui/src/tests/taskbar.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/ui-egui/src/tests/taskbar.rs b/crates/ui-egui/src/tests/taskbar.rs index a1374bd37..f52235297 100644 --- a/crates/ui-egui/src/tests/taskbar.rs +++ b/crates/ui-egui/src/tests/taskbar.rs @@ -418,3 +418,29 @@ fn a_compound_path_under_direct_selection_drops_edit_path() { let (_, texts) = bar(&mut app); assert!(has(&texts, "Release Compound Path") && !has(&texts, "Edit Path"), "{texts:?}"); } + +/// After a switch to another document and back, the task bar shows the actions for the active +/// document's selection: an open path's Join in the first, a rectangle's Shape Properties in the +/// second. +#[test] +fn the_task_bar_follows_the_active_document() { + let mut app = app(); + app.run("tool.select", json!({"tool": "selection"})).unwrap(); + let line = id(app.run("shape.line", json!({"x1": 50, "y1": 50, "x2": 200, "y2": 120})).unwrap()); + select(&mut app, &[line]); + assert!(has(&bar(&mut app).1, "Join")); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + let rect = id(app.run("shape.rectangle", json!({"x": 40, "y": 40, "width": 80, "height": 60})).unwrap()); + select(&mut app, &[rect]); + let (_, texts) = bar(&mut app); + assert!(!has(&texts, "Join") && has(&texts, "Duplicate"), "{texts:?}"); + app.session.set_active(0); + let (_, texts) = bar(&mut app); + assert!(has(&texts, "Join"), "{texts:?}"); + app.session.set_active(1); + let (_, texts) = bar(&mut app); + assert!(!has(&texts, "Join"), "{texts:?}"); + let (_, texts) = click_lock_slot(&mut app); + assert!(has(&texts, "Rectangle Properties:"), "{texts:?}"); + assert!(!locked(&app, rect), "the button doesn't lock the shape"); +} From c23612970265ba1a420e07e44883ffb859433720 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 19:10:12 -0800 Subject: [PATCH 3/4] Docs: name the task bar test in the Control bar and task bar line --- docs/target-app-parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 15ab8bed6..3b3b6e46d 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1314,7 +1314,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Live objects in documents with many symbols:** a live object, or an object with effects, that shows symbols or patterns is not walked again in a later frame to find them while the document's symbols and patterns stay the same. One that shows a placed file is walked in every frame. At 1000 × 1000 pixels, a page of 2,000 envelopes around instances of the last 2,000 of 10,000 symbols takes 30 ms a frame, and 83 ms when every frame walks them again, with the same pixels. -- **Control bar and task bar on large paths:** the Control bar and the contextual task bar read the first selected object in place each frame instead of copying it; the task bar copies the object only when it is a basic shape, for its Shape Properties. With one selected open path of 200,000 anchors, an idle frame with both bars shown takes 3.8 ms in a release build instead of 8.0 ms (medians of five interleaved runs). Test: `the_control_bar_label_follows_edits_undo_documents_and_selection`. +- **Control bar and task bar on large paths:** the Control bar and the contextual task bar read the first selected object in place each frame instead of copying it; the task bar copies the object only when it is a basic shape, for its Shape Properties. With one selected open path of 200,000 anchors, an idle frame with both bars shown takes 3.8 ms in a release build instead of 8.0 ms (medians of five interleaved runs). Tests: `the_control_bar_label_follows_edits_undo_documents_and_selection`, `the_task_bar_follows_the_active_document`. - **Type drawn as glyph outlines:** under a geometry effect, in EMF and WMF export, in DXF export when it is written as outlines, flattened with Convert All Text to Outlines, and inside an envelope, type keeps its underline and strikethrough, the weight and dashes of its character strokes when it is scaled, and the placement of its gradients and patterns, as the canvas draws it. A group's or layer's own fills and strokes also paint the underline and strikethrough of the type in it. From 8a5ec6a25fff090843adac5569f20e467f0021fb Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 19:35:44 -0800 Subject: [PATCH 4/4] Tests: the task bar follows an edit and Undo The task bar drops Join once the selected open path is closed and offers it again after Undo, and Shape Properties shows a rectangle's corner radius as it is after object.setLiveShape and after Undo. The progress line names the new test. --- crates/ui-egui/src/tests/taskbar.rs | 30 +++++++++++++++++++++++++++++ docs/target-app-parity.md | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/tests/taskbar.rs b/crates/ui-egui/src/tests/taskbar.rs index f52235297..96e453344 100644 --- a/crates/ui-egui/src/tests/taskbar.rs +++ b/crates/ui-egui/src/tests/taskbar.rs @@ -444,3 +444,33 @@ fn the_task_bar_follows_the_active_document() { assert!(has(&texts, "Rectangle Properties:"), "{texts:?}"); assert!(!locked(&app, rect), "the button doesn't lock the shape"); } + +/// The task bar shows the selection as it is after an edit and after Undo: Join leaves once the +/// open path is closed and comes back with Undo, and Shape Properties shows a rectangle's new +/// corner radius and then its old one. +#[test] +fn the_task_bar_follows_edits_and_undo() { + let mut app = app(); + app.run("tool.select", json!({"tool": "selection"})).unwrap(); + let line = id(app.run("shape.line", json!({"x1": 50, "y1": 50, "x2": 200, "y2": 120})).unwrap()); + select(&mut app, &[line]); + assert!(has(&bar(&mut app).1, "Join")); + app.run("path.join", json!({})).unwrap(); + let (_, texts) = bar(&mut app); + assert!(!has(&texts, "Join"), "{texts:?}"); + app.run("edit.undo", json!({})).unwrap(); + let (_, texts) = bar(&mut app); + assert!(has(&texts, "Join"), "{texts:?}"); + + let rect = id(app.run("shape.rectangle", json!({"x": 40, "y": 40, "width": 80, "height": 60, "radius": 10})).unwrap()); + select(&mut app, &[rect]); + let (_, texts) = click_lock_slot(&mut app); + assert!(has(&texts, "10 pt") && !has(&texts, "20 pt"), "{texts:?}"); + app.run("object.setLiveShape", json!({"radius": 20})).unwrap(); + let (_, texts) = click_lock_slot(&mut app); + assert!(has(&texts, "20 pt") && !has(&texts, "10 pt"), "{texts:?}"); + app.run("edit.undo", json!({})).unwrap(); + let (_, texts) = click_lock_slot(&mut app); + assert!(has(&texts, "10 pt") && !has(&texts, "20 pt"), "{texts:?}"); + assert!(!locked(&app, rect), "the button doesn't lock the shape"); +} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 3b3b6e46d..a8ee089e5 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1314,7 +1314,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Live objects in documents with many symbols:** a live object, or an object with effects, that shows symbols or patterns is not walked again in a later frame to find them while the document's symbols and patterns stay the same. One that shows a placed file is walked in every frame. At 1000 × 1000 pixels, a page of 2,000 envelopes around instances of the last 2,000 of 10,000 symbols takes 30 ms a frame, and 83 ms when every frame walks them again, with the same pixels. -- **Control bar and task bar on large paths:** the Control bar and the contextual task bar read the first selected object in place each frame instead of copying it; the task bar copies the object only when it is a basic shape, for its Shape Properties. With one selected open path of 200,000 anchors, an idle frame with both bars shown takes 3.8 ms in a release build instead of 8.0 ms (medians of five interleaved runs). Tests: `the_control_bar_label_follows_edits_undo_documents_and_selection`, `the_task_bar_follows_the_active_document`. +- **Control bar and task bar on large paths:** the Control bar and the contextual task bar read the first selected object in place each frame instead of copying it; the task bar copies the object only when it is a basic shape, for its Shape Properties. With one selected open path of 200,000 anchors, an idle frame with both bars shown takes 3.8 ms in a release build instead of 8.0 ms (medians of five interleaved runs). Tests: `the_control_bar_label_follows_edits_undo_documents_and_selection`, `the_task_bar_follows_the_active_document`, `the_task_bar_follows_edits_and_undo`. - **Type drawn as glyph outlines:** under a geometry effect, in EMF and WMF export, in DXF export when it is written as outlines, flattened with Convert All Text to Outlines, and inside an envelope, type keeps its underline and strikethrough, the weight and dashes of its character strokes when it is scaled, and the placement of its gradients and patterns, as the canvas draws it. A group's or layer's own fills and strokes also paint the underline and strikethrough of the type in it.