From a8fbae1b1f3f1009b5e44c7019a427184e08c389 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 16 Oct 2026 20:04:15 -0700 Subject: [PATCH] Graphic Styles: Shift-click selects a range of styles In the Graphic Styles panel, Shift-click and Cmd/Ctrl-click both added or removed the one style clicked. Shift-click now adds to the selection every style from the last one clicked without Shift through the clicked one, in the order the panel lists them, and Cmd/Ctrl-click still adds or removes one. A plain click applies the style and selects it alone, Alt-click still adds it on top, and the Control bar's Style picker still applies on every click. The panel uses swatches::click_selection, as the Swatches panel and the library panel do. Docs: docs/target-app-parity.md. --- crates/ui-egui/src/panels/graphic_styles.rs | 90 +++++++++++++++------ docs/target-app-parity.md | 1 + 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/crates/ui-egui/src/panels/graphic_styles.rs b/crates/ui-egui/src/panels/graphic_styles.rs index dcb0b6bb9..6e488ca65 100644 --- a/crates/ui-egui/src/panels/graphic_styles.rs +++ b/crates/ui-egui/src/panels/graphic_styles.rs @@ -1,11 +1,12 @@ //! Graphic Styles panel: the document's styles as rendered thumbnails (Thumbnail View) or rows //! (Small and Large List View), shown on a square or on type. Clicking a style applies it to the -//! selection (Alt-click adds it on top), Shift/Cmd-click selects several, double-click opens -//! Graphic Style Options. Dragging a style moves it in the list, or applies it to the art it is -//! dropped on (`canvas::panel_drop`); art dragged off the canvas or the Appearance panel's -//! thumbnail dropped on the panel becomes a new style. Holding the right button on a style shows it -//! large, on the selected object when there is one. The Control bar's Style picker lists the same -//! tiles ([`picker`]). Graphic style libraries open in the library panel +//! selection (Alt-click adds it on top); Shift-click adds to the panel's selection every style from +//! the last one clicked without Shift through this one; Cmd/Ctrl-click adds or removes one; +//! double-click opens Graphic Style Options. Dragging a style moves it in the list, or applies it +//! to the art it is dropped on (`canvas::panel_drop`); art dragged off the canvas or the Appearance +//! panel's thumbnail dropped on the panel becomes a new style. Holding the right button on a style +//! shows it large, on the selected object when there is one. The Control bar's Style picker lists +//! the same tiles ([`picker`]). Graphic style libraries open in the library panel //! ([`GraphicStyleLibraries`]). use std::cell::RefCell; @@ -270,8 +271,8 @@ fn large_preview(app: &VectorcraftApp, ctx: &egui::Context, s: &Shown, i: usize) /// What a click on a style does. enum Click { - Apply(String), - Toggle(String), + /// A click with the modifiers held. + Pick(String, egui::Modifiers), Options(String), } @@ -294,8 +295,7 @@ fn clicked(ui: &Ui, resp: &Response, name: &str) -> Option { if resp.double_clicked() { Some(Click::Options(name.to_string())) } else if resp.clicked() { - let m = ui.input(|i| i.modifiers); - Some(if m.shift || m.command { Click::Toggle(name.to_string()) } else { Click::Apply(name.to_string()) }) + Some(Click::Pick(name.to_string(), ui.input(|i| i.modifiers))) } else { None } @@ -419,24 +419,19 @@ fn act(app: &mut VectorcraftApp, ui: &Ui, s: &Shown, ev: Events, picker: bool) { run(app, "graphicStyle.new", json!({ "id": id.0 })); } match ev.click { - Some(Click::Apply(name) | Click::Toggle(name)) if picker => { + Some(Click::Pick(name, _)) if picker => { app.run("graphicStyle.apply", json!({"name": name, "add": alt_held(ui)})).ok(); ui.close(); } - Some(Click::Apply(name)) => { - // With nothing selected the next object drawn takes the style. - app.run("graphicStyle.apply", json!({"name": name, "add": alt_held(ui)})).ok(); - set_pstate(ui.ctx(), "gs-sel", vec![name]); - } - Some(Click::Toggle(name)) => { - let mut sel = selected(ui.ctx(), s.d); - match sel.iter().position(|n| *n == name) { - Some(i) => { - sel.remove(i); - } - None => sel.push(name), - } + Some(Click::Pick(name, m)) => { + let order: Vec<&str> = s.d.graphic_styles.iter().map(|g| g.name.as_str()).collect(); + let sel = super::swatches::click_selection(ui, "gs-anchor", selected(ui.ctx(), s.d), &order, &name, m); set_pstate(ui.ctx(), "gs-sel", sel); + // A plain click also applies the style (with nothing selected the next object drawn + // takes it); Shift and Cmd/Ctrl clicks only select. + if !(m.shift || m.command) { + app.run("graphicStyle.apply", json!({"name": name, "add": alt_held(ui)})).ok(); + } } Some(Click::Options(name)) => { app.run("ui.graphicStyleOptions", json!({ "name": name })).ok(); @@ -938,6 +933,53 @@ mod tests { assert_eq!(d.node(rect).unwrap().extras().graphic_style, d.graphic_styles.last().map(|g| g.id)); } + #[test] + fn shift_click_selects_a_range_and_cmd_click_adds_or_removes_one_style() { + let mut app = app(); + app.session.execute("graphicStyle.new", &json!({"name": "Mine"})).unwrap(); + let n = names(&app); + assert_eq!(n.len(), 5); + let ctx = egui::Context::default(); + set_pstate(&ctx, "gs-view", View::SmallList); + frame(&ctx, |ui| show(&mut app, ui)); + // A click on row `i` (Small List View: rows of 18 points, 3 apart) with `modifiers` held, + // a second after the last one so that two clicks on a row are not a double-click. + let mut time = 0.0; + let mut click = |app: &mut VectorcraftApp, i: usize, modifiers: egui::Modifiers| { + let at = pos2(60.0, 10.0 + 21.0 * i as f32); + time += 1.0; + for pressed in [true, false] { + let button = egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers }; + let raw = egui::RawInput { + screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(260.0, 600.0))), + events: vec![egui::Event::ModifiersChanged(modifiers), egui::Event::PointerMoved(at), button], + time: Some(time + if pressed { 0.0 } else { 0.05 }), + ..Default::default() + }; + ctx.run_ui(raw, |ui| show(app, ui)).textures_delta.clear(); + } + selected(&ctx, &app.session.active().unwrap().doc.clone()) + }; + let linked = |app: &VectorcraftApp| app.session.selection_graphic_style().map(|(g, _)| g.name.clone()); + // A plain click applies the style and selects it alone. + assert_eq!(click(&mut app, 0, egui::Modifiers::NONE), [n[0].clone()]); + assert_eq!(linked(&app).as_ref(), Some(&n[0])); + // Shift-click selects from that style to the clicked one, and applies nothing. + assert_eq!(click(&mut app, 3, egui::Modifiers::SHIFT), n[..4]); + assert_eq!(linked(&app).as_ref(), Some(&n[0])); + // Cmd/Ctrl-click removes a selected style and adds it back. + assert_eq!(click(&mut app, 2, egui::Modifiers::COMMAND), [n[0].clone(), n[1].clone(), n[3].clone()]); + assert_eq!(click(&mut app, 2, egui::Modifiers::COMMAND), [n[0].clone(), n[1].clone(), n[3].clone(), n[2].clone()]); + // Shift-click extends from the style Cmd/Ctrl-clicked last. + click(&mut app, 0, egui::Modifiers::NONE); + click(&mut app, 2, egui::Modifiers::COMMAND); + assert_eq!(click(&mut app, 4, egui::Modifiers::SHIFT), [n[0].clone(), n[2].clone(), n[3].clone(), n[4].clone()]); + assert_eq!(linked(&app).as_ref(), Some(&n[0])); + // A plain click applies the next style. + assert_eq!(click(&mut app, 4, egui::Modifiers::NONE), [n[4].clone()]); + assert_eq!(linked(&app).as_ref(), Some(&n[4])); + } + #[test] fn the_right_button_shows_a_large_preview_and_the_picker_applies_styles() { let mut app = app(); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f2043138b..6515232eb 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -586,6 +586,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur On the canvas, the renderer no longer draws the picture of the art below back as an image each time it copies the backdrop of a non-isolated group that blends: it keeps that picture and draws the art that follows over it. At 2880 × 1800, 50 translucent groups with Multiply members take 65 ms a frame instead of 299 ms on 8 threads (`vectorcraft-cli bench`), and the frames have the same pixels. Type inside a group, layer, symbol instance, blend or repeat keeps its opacity, blend mode, opacity mask, Opacity & Mask Define Knockout Shape, own effects, perspective and visibility when the effects on the container outline it. Hidden type with a geometry effect of its own is left out of exports. - **Graphic styles:** styles keep opacity and blend mode, capture groups and type, apply on top with Alt, and link to the objects using them; Redefine, Break Link, Graphic Style Options, Select All Unused, Sort by Name, and Select > Same Graphic Style / Appearance Attribute. The panel has list and thumbnail views with rendered previews, merge, reorder and drag and drop, plus a Control bar Style picker; six generated style libraries save and load as `.vcstyles`. + In the Graphic Styles panel, Shift-click selects a range of styles and Ctrl/Cmd-click adds or removes one. - **Neutral wording:** labels, MCP tool text, docs and packaging use neutral names, and `cargo xtask brands` (part of `cargo xtask ci`) fails on vendor names. - **Colour, type and file workflows:** Recolor Artwork (dialog with harmonies), Edit Colors, Find & Replace, Change Case, Smart Punctuation, Guides, Lock/Hide Above, Transform Each, Rasterize. Object › Lock › All Artwork Above and Object › Hide › All Artwork Above deselect the selected objects they lock or hide, the key object included.