From a7db76bdfb16d2d3a7eb38eef99fe1b3caed5459 Mon Sep 17 00:00:00 2001 From: nevarem <349680237+nevarem@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:11:38 +0800 Subject: [PATCH] Properties: selected ruler guides show their position in a Transform section With ruler guides selected on their own (a click on one), the Properties panel showed the Document sections, so a guide's position could be typed only in the Transform panel. The panel now has a Guides heading and a Transform section with the Transform panel's guide fields (#1597): X moves the vertical guides and Y the horizontal ones, together. With art selected too, it shows the art's sections as before. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EjfPR4a1jPek7r3nyFFNYv --- crates/ui-egui/src/panels/properties.rs | 36 ++++++++++---- crates/ui-egui/src/panels/properties/tests.rs | 47 +++++++++++++++++++ crates/ui-egui/src/panels/transform.rs | 13 ++--- 3 files changed, 82 insertions(+), 14 deletions(-) diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index 1d939d635..fdbcc2d89 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -24,13 +24,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let first = first_selected(app); // Edit Artboards (the Artboard tool): the active artboard, whatever is selected. let editing_artboards = app.session.tool_id() == "artboard"; - let label = match (&first, n_sel) { - _ if editing_artboards => tl!("Artboard").to_string(), - (None, _) => tl!("Document").to_string(), - (_, n) if n > 1 => crate::i18n::tn(n as u64, "{n} Object", "{n} Objects"), - (Some(n), _) if crate::panels::image_trace::is_trace(n) => tl!("Image Tracing").to_string(), - (Some(n), _) => tl!(n.kind_label()).to_string(), - }; + // Ruler guides selected on their own (a click on one): their position. + let guides = super::transform::selected_guides(app); + let label = heading(first.as_ref(), n_sel, editing_artboards, guides.is_some()); ui.label(egui::RichText::new(label).size(11.5).color(t.text_dim)); ui.add_space(4.0); if editing_artboards { @@ -38,7 +34,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { return; } if first.is_none() { - document_sections(app, ui); + match guides { + Some(guides) => guide_sections(app, ui, guides), + None => document_sections(app, ui), + } return; } if n_sel == 1 @@ -592,6 +591,27 @@ fn artboard_fill_row(app: &mut VectorcraftApp, ui: &mut Ui, i: usize, color: Opt } } +/// The panel's heading: what the sections are for (the artboards with the Artboard tool, the +/// selected ruler guides, the document with nothing selected), else the selection's kind. +fn heading(first: Option<&vectorcraft_doc::Node>, n_sel: usize, editing_artboards: bool, guides: bool) -> String { + match (first, n_sel) { + _ if editing_artboards => tl!("Artboard").to_string(), + (None, _) if guides => tl!("Guides").to_string(), + (None, _) => tl!("Document").to_string(), + (_, n) if n > 1 => crate::i18n::tn(n as u64, "{n} Object", "{n} Objects"), + (Some(n), _) if crate::panels::image_trace::is_trace(n) => tl!("Image Tracing").to_string(), + (Some(n), _) => tl!(n.kind_label()).to_string(), + } +} + +/// Ruler guides selected on their own: a Transform section with the X of the vertical ones and +/// the Y of the horizontal ones, which move them, as the Transform panel shows them (#1597). +fn guide_sections(app: &mut VectorcraftApp, ui: &mut Ui, guides: (Option, Option)) { + section_header(ui, tl!("Transform")); + let fw = super::field_width(ui); + super::transform::guide_fields(app, ui, guides, fw); +} + fn document_sections(app: &mut VectorcraftApp, ui: &mut Ui) { let units = app.session.general_unit(); section_header(ui, tl!("Document")); diff --git a/crates/ui-egui/src/panels/properties/tests.rs b/crates/ui-egui/src/panels/properties/tests.rs index b9bbb30e7..1cfa6c380 100644 --- a/crates/ui-egui/src/panels/properties/tests.rs +++ b/crates/ui-egui/src/panels/properties/tests.rs @@ -1600,3 +1600,50 @@ fn a_symbol_instance_turns_about_its_registration_point() { assert!((bx.angle - 45.0).abs() < 1e-6, "turned to {}", bx.angle); assert!(near(bx.reference_point(8), vectorcraft_geom::Point::new(200.0, 160.0)), "the registration point moved to {:?}", bx.reference_point(8)); } + +/// Ruler guides selected on their own (a click on one) show a Guides heading and a Transform +/// section, where X moves the vertical ones and Y the horizontal ones; with art selected too, the +/// panel shows the art's sections. +#[test] +fn selected_guides_show_their_position_in_a_transform_section() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 100})).unwrap(); + let v = app.run("guide.add", json!({"vertical": true, "pos": 30})).unwrap()["index"].as_u64().unwrap(); + let h = app.run("guide.add", json!({"vertical": false, "pos": 20})).unwrap()["index"].as_u64().unwrap(); + let pos = |app: &VectorcraftApp, i: u64| app.session.doc().unwrap().doc.guides[i as usize].pos; + let ctx = egui::Context::default(); + // Steps the field labelled `label` up by one with ↑. + let step = |app: &mut VectorcraftApp, label: &str| { + let texts = frame(app, &ctx, vec![]); + let r = texts.iter().find(|(t, _)| t == label).unwrap_or_else(|| panic!("no {label:?} in {texts:?}")).1; + let at = egui::pos2(r.right() + 40.0, r.center().y); + let press = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() }; + frame(app, &ctx, vec![Event::PointerMoved(at), press(true)]); + frame(app, &ctx, vec![press(false)]); + let up = Event::Key { key: egui::Key::ArrowUp, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }; + frame(app, &ctx, vec![up]); + frame( + app, + &ctx, + vec![Event::PointerButton { pos: egui::pos2(5.0, 690.0), button: PointerButton::Primary, pressed: true, modifiers: Default::default() }], + ); + }; + for (index, label, before) in [(v, "X:", 30.0), (h, "Y:", 20.0)] { + app.run("guide.select", json!({"indexes": [index]})).unwrap(); + frame(&mut app, &ctx, vec![]); + let texts: Vec = frame(&mut app, &ctx, vec![]).into_iter().map(|(t, _)| t).collect(); + for shown in ["Guides", "Transform", "X:", "Y:"] { + assert!(texts.iter().any(|t| t == shown), "{shown:?} in {texts:?}"); + } + assert!(!texts.iter().any(|t| t == "Document"), "{texts:?}"); + step(&mut app, label); + assert_eq!((pos(&app, index) - before).abs(), 1.0, "{label} moves the selected guide"); + } + // A guide and a rectangle: the rectangle's sections. + let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 40})).unwrap()["id"].as_u64().unwrap(); + app.run("select.set", json!({"ids": [id]})).unwrap(); + app.run("guide.select", json!({"indexes": [v], "toggle": true})).unwrap(); + frame(&mut app, &ctx, vec![]); + let texts: Vec = frame(&mut app, &ctx, vec![]).into_iter().map(|(t, _)| t).collect(); + assert!(!texts.iter().any(|t| t == "Guides") && texts.iter().any(|t| t == "W:"), "{texts:?}"); +} diff --git a/crates/ui-egui/src/panels/transform.rs b/crates/ui-egui/src/panels/transform.rs index 8e97b3ab1..95f148840 100644 --- a/crates/ui-egui/src/panels/transform.rs +++ b/crates/ui-egui/src/panels/transform.rs @@ -215,7 +215,7 @@ fn artboard_fields(app: &mut VectorcraftApp, ui: &mut Ui, index: usize, r: Rect) /// With ruler guides selected and no art: the X of the vertical ones and the Y of the horizontal /// ones (the least of each; none where no such guide is selected). -fn selected_guides(app: &VectorcraftApp) -> Option<(Option, Option)> { +pub(crate) fn selected_guides(app: &VectorcraftApp) -> Option<(Option, Option)> { let st = app.session.active()?; if !st.selection.objects.is_empty() || st.selection.guides.is_empty() { return None; @@ -226,16 +226,17 @@ fn selected_guides(app: &VectorcraftApp) -> Option<(Option, Option)> { Some((least(true), least(false))) } -/// The selected ruler guides' view of the panel (#1597): X moves the vertical ones and Y the -/// horizontal ones, together. -fn guide_fields(app: &mut VectorcraftApp, ui: &mut Ui, (x, y): (Option, Option)) { +/// The selected ruler guides' view of the panel (#1597), also the Properties panel's Transform +/// section for them: X moves the vertical ones and Y the horizontal ones, together. The fields +/// are `width` wide. +pub(crate) fn guide_fields(app: &mut VectorcraftApp, ui: &mut Ui, (x, y): (Option, Option), width: f32) { let units = app.session.general_unit(); let origin = app.ruler_origin(); egui::Grid::new("xfp-guide-grid").num_columns(2).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| { for (label, id, pos, origin, by) in [("X:", "xfp-guide-x", x, origin.x, "dx"), ("Y:", "xfp-guide-y", y, origin.y, "dy")] { widgets::dim_label(ui, label); ui.add_enabled_ui(pos.is_some(), |ui| { - if let (Some(v), Some(pos)) = (widgets::num_field_named(ui, id, Some(tl!(label)), pos.map(|p| p - origin), units, 80.0), pos) + if let (Some(v), Some(pos)) = (widgets::num_field_named(ui, id, Some(tl!(label)), pos.map(|p| p - origin), units, width), pos) && let Err(e) = app.run("guide.move", json!({ by: v + origin - pos })) { app.status(e); @@ -257,7 +258,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { return; } if let Some(guides) = selected_guides(app) { - guide_fields(app, ui, guides); + guide_fields(app, ui, guides, 80.0); return; } let units = app.session.general_unit();