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.