From a54be8298fa4883ff080feae85a62cf091a23d54 Mon Sep 17 00:00:00 2001 From: theironchef <6278036+theironchef@users.noreply.github.com> Date: Wed, 7 Oct 2026 08:48:29 -0700 Subject: [PATCH] Fix #349: clicking an artboard in the Artboards panel makes it active The panel kept its own selection (a pstate nothing else read), so a click only highlighted the row: the status bar navigator, Fit Artboard in Window and Actual Size stayed on the previous artboard. The panel's selection is now the view's active artboard (View::artboard): a click, Move Up/Down and New Artboard set it, and the highlight follows the navigator, per document, as in the reference app. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/artboards.rs | 65 ++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/crates/ui-egui/src/panels/artboards.rs b/crates/ui-egui/src/panels/artboards.rs index 69dee346..eaa74c91 100644 --- a/crates/ui-egui/src/panels/artboards.rs +++ b/crates/ui-egui/src/panels/artboards.rs @@ -1,4 +1,6 @@ //! Artboards panel: numbered list with inline rename (double-click), move up / down, new, delete. +//! The highlighted row is the active artboard, the one the status bar's navigator shows and Fit +//! Artboard in Window fits: clicking a row makes it active. use egui::{Sense, Ui, pos2, vec2}; use serde_json::json; @@ -8,8 +10,16 @@ use crate::theme::Tokens; use crate::widgets::{self, menu_item}; use crate::{VectorcraftApp, icons}; -fn selected(ui: &Ui, n: usize) -> usize { - pstate::(ui.ctx(), "ab-sel").min(n.saturating_sub(1)) +/// The active artboard (of `n`). +fn selected(app: &VectorcraftApp, n: usize) -> usize { + app.view().map_or(0, |v| v.artboard).min(n.saturating_sub(1)) +} + +/// Make artboard `i` the active one, leaving the view where it is. +fn select(app: &mut VectorcraftApp, i: usize) { + if let Some(v) = app.view_mut() { + v.artboard = i; + } } pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { @@ -19,7 +29,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { super::empty_state(ui, "dc-artboards", tl!("No document"), tl!("Open a document to see its artboards.")); return; } - let sel = selected(ui, abs.len()); + let sel = selected(app, abs.len()); let editing: Option = pstate(ui.ctx(), "ab-edit"); widgets::list_box(ui, |ui| { ui.set_min_height(110.0); @@ -62,11 +72,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let oresp = ui.interact(opt, ui.id().with(("ab-opt", i)), Sense::click()); icons::paint(ui, "dc-artboard-options", opt, if oresp.hovered() { t.text_strong } else { t.icon }); if oresp.on_hover_text(tl!("Artboard Options: edit with the Artboard tool")).clicked() { - set_pstate(ui.ctx(), "ab-sel", i); + select(app, i); app.select_tool("artboard"); } if resp.clicked() { - set_pstate(ui.ctx(), "ab-sel", i); + select(app, i); } if resp.double_clicked() { set_pstate(ui.ctx(), "ab-edit", Some(i)); @@ -81,15 +91,15 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if widgets::icon_button_enabled(ui, "dc-arrow-up", tl!("Move Up"), false, sel > 0, 24.0).clicked() && app.run("artboard.reorder", json!({"index": sel, "to": sel - 1})).is_ok() { - set_pstate(ui.ctx(), "ab-sel", sel - 1); + select(app, sel - 1); } if widgets::icon_button_enabled(ui, "dc-arrow-down", tl!("Move Down"), false, sel + 1 < n, 24.0).clicked() && app.run("artboard.reorder", json!({"index": sel, "to": sel + 1})).is_ok() { - set_pstate(ui.ctx(), "ab-sel", sel + 1); + select(app, sel + 1); } if widgets::icon_button(ui, "dc-new-item", tl!("New Artboard"), false, 24.0).clicked() && app.run("artboard.new", json!({})).is_ok() { - set_pstate(ui.ctx(), "ab-sel", n); + select(app, n); } if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Artboard"), false, n > 1, 24.0).clicked() { app.run("artboard.delete", json!({"index": sel})).ok(); @@ -99,7 +109,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let n = app.session.active().map(|d| d.doc.artboards.len()).unwrap_or(0); - let sel = selected(ui, n); + let sel = selected(app, n); if menu_item(ui, tl!("New Artboard"), n > 0, false) { app.run("artboard.new", json!({})).ok(); } @@ -124,3 +134,40 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { app.run("artboard.fitToArt", json!({"index": sel})).ok(); } } + +#[cfg(test)] +mod tests { + use super::*; + use vectorcraft_engine::Session; + + /// Clicking a row makes its artboard the active one (the navigator's, Fit Artboard in Window's). + #[test] + fn clicking_a_row_makes_its_artboard_active() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 150})).unwrap(); + app.session.execute("artboard.new", &json!({"x": 400, "y": 300, "width": 200, "height": 150})).unwrap(); + app.view_mut().unwrap().artboard = 0; + let ctx = egui::Context::default(); + let frame = |app: &mut VectorcraftApp, events: Vec| { + let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui)); + out.textures_delta.clear(); + out.shapes + }; + // Where the second row's name is painted. + let at = frame(&mut app, vec![]) + .iter() + .find_map(|c| match &c.shape { + egui::Shape::Text(t) if t.galley.text() == "Artboard 2" => Some(t.pos + vec2(4.0, 4.0)), + _ => None, + }) + .unwrap(); + let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + for e in [egui::Event::PointerMoved(at), button(true), button(false)] { + frame(&mut app, vec![e]); + } + assert_eq!(app.view().unwrap().artboard, 1, "Artboard 2 is active"); + // The navigator moves it on; the panel follows. + crate::menus::invoke(&mut app, "view.goToArtboard", json!({"index": "previous"})); + assert_eq!(selected(&app, 2), 0, "and the panel follows the navigator"); + } +}