mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Merge pull request #2739 from guillehr2/fix/1876-artboard-options-xy
Fix #1876: Artboard Options has X and Y with a reference point
This commit is contained in:
@@ -1,13 +1,77 @@
|
||||
//! Artboard Options (double-click with the Artboard tool, double-click the tool, or the Artboards
|
||||
//! panel's options icon or menu): the artboard's fields, sizes parsed with units.
|
||||
//! panel's options icon or menu): the artboard's fields, sizes parsed with units, and its position
|
||||
//! from a reference point.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::Unit;
|
||||
use vectorcraft_geom::{Point, Rect};
|
||||
|
||||
use super::{DialogSpec, run_and_close};
|
||||
use super::{DialogSpec, form, run_and_close};
|
||||
use crate::VectorcraftApp;
|
||||
use crate::panels::transform::{artboard_rect, reference_index, reference_locator};
|
||||
use crate::state::Dialog;
|
||||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Artboard Options").into(), confirm, ..DialogSpec::FORM };
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Artboard Options").into(), body, confirm, ..DialogSpec::FORM };
|
||||
|
||||
/// Where the fields put the artboard: `x` and `y` are its top-left corner in document points, as
|
||||
/// `artboard.setProps` and `artboard.new` take them.
|
||||
fn placed(d: &Dialog) -> Rect {
|
||||
let (x, y) = (d.f64("x", 0.0), d.f64("y", 0.0));
|
||||
Rect::new(x, y, x + d.f64("width", 0.0), y + d.f64("height", 0.0))
|
||||
}
|
||||
|
||||
/// The form's Name, Width and Height, then X and Y in the same grid, the reference point locator
|
||||
/// before X (#1876). The locator is the one the Transform panel, the Control bar and Properties
|
||||
/// share, and X and Y are where its point lies on the rulers, counted from the ruler origin (#1212),
|
||||
/// as the Transform panel and the Control bar show an artboard's. A new Width or Height keeps the
|
||||
/// reference point where it was, as their sizes do.
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let (unit, origin, refi) = (app.session.general_unit(), app.ruler_origin(), reference_index(ui));
|
||||
let (was, w0, h0) = (placed(d), d.f64("width", 0.0), d.f64("height", 0.0));
|
||||
form::grid_then(ui, d, unit, |ui, d| {
|
||||
let (w, h) = (d.f64("width", 0.0), d.f64("height", 0.0));
|
||||
let n = artboard_rect(was, refi, None, None, Some(w), Some(h), false);
|
||||
// Only the side that changed moves, so an untouched coordinate stays exactly as it was.
|
||||
for (key, changed, at) in [("x", w != w0, n.x0), ("y", h != h0, n.y0)] {
|
||||
if changed {
|
||||
d.fields.insert(key.into(), json!(at));
|
||||
}
|
||||
}
|
||||
position_rows(ui, d, refi, unit, origin);
|
||||
});
|
||||
false
|
||||
}
|
||||
|
||||
/// The X and Y rows: where reference point `refi` lies, counted from the ruler origin `origin`,
|
||||
/// with the locator before X's label. A typed X or Y moves the artboard so that its reference point
|
||||
/// lands there.
|
||||
fn position_rows(ui: &mut egui::Ui, d: &mut Dialog, refi: usize, unit: Unit, origin: Point) {
|
||||
let r = placed(d);
|
||||
let rp = vectorcraft_geom::reference_point(r, refi);
|
||||
let mut indent = 0.0;
|
||||
for (key, label, at, zero) in [("x", "X:", rp.x, origin.x), ("y", "Y:", rp.y, origin.y)] {
|
||||
ui.horizontal(|ui| {
|
||||
// Y's label lines up with X's, after the locator.
|
||||
if key == "x" {
|
||||
let left = ui.cursor().min.x;
|
||||
reference_locator(ui, refi);
|
||||
indent = ui.min_rect().right() - left;
|
||||
} else {
|
||||
ui.add_space(indent);
|
||||
}
|
||||
form::row_label(ui, label);
|
||||
});
|
||||
if let Some(v) = crate::widgets::num_field(ui, ("dlg-ab-xy", key), Some(at - zero), unit, form::FIELD_W) {
|
||||
let n = if key == "x" {
|
||||
artboard_rect(r, refi, Some(v + zero), None, None, None, false).x0
|
||||
} else {
|
||||
artboard_rect(r, refi, None, Some(v + zero), None, None, false).y0
|
||||
};
|
||||
d.fields.insert(key.into(), json!(n));
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
|
||||
/// The dialog kind of Artboard Options.
|
||||
pub const KIND: &str = "artboardOptions";
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::state::Dialog;
|
||||
use crate::theme::Tokens;
|
||||
|
||||
/// Width of a dialog's value fields ([`text`] and [`length`], frame included).
|
||||
const FIELD_W: f32 = 132.0;
|
||||
pub(super) const FIELD_W: f32 = 132.0;
|
||||
|
||||
/// A labelled text field bound to `d.fields[key]` (one grid row).
|
||||
pub(super) fn field(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str) {
|
||||
@@ -193,6 +193,12 @@ fn dropdown(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32, options: &
|
||||
/// hidden) in the kind's [`order`], the distances in `unit`, the [`choices`] as dropdowns and the
|
||||
/// [`skew_ends`] fields as sliders.
|
||||
pub(super) fn grid(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) {
|
||||
grid_then(ui, d, unit, |_, _| {});
|
||||
}
|
||||
|
||||
/// [`grid`], then the rows `more` draws in the same grid, so that their labels and fields line up
|
||||
/// with the generic ones.
|
||||
pub(super) fn grid_then(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit, more: impl FnOnce(&mut egui::Ui, &mut Dialog)) {
|
||||
let rail = Tokens::get(ui.ctx()).input_border;
|
||||
let (lengths, first) = (lengths(&d.kind), order(&d.kind));
|
||||
let mut keys: Vec<String> = d
|
||||
@@ -237,6 +243,7 @@ pub(super) fn grid(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) {
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
more(ui, d);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! The dialog registry and the shared frame, drawn headlessly.
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::Unit;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::*;
|
||||
@@ -1018,6 +1019,111 @@ fn artboard_options_position_follows_the_tool_option_with_another_tool() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The strings a frame painted, each with its rectangle.
|
||||
fn painted(out: &egui::FullOutput) -> Vec<(String, egui::Rect)> {
|
||||
fn walk(s: &egui::Shape, v: &mut Vec<(String, egui::Rect)>) {
|
||||
match s {
|
||||
egui::Shape::Text(t) => v.push((t.galley.text().to_string(), egui::Rect::from_min_size(t.pos, t.galley.size()))),
|
||||
egui::Shape::Vec(s) => s.iter().for_each(|s| walk(s, v)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut v = vec![];
|
||||
out.shapes.iter().for_each(|c| walk(&c.shape, &mut v));
|
||||
v
|
||||
}
|
||||
|
||||
/// Where to click in the field after the label `label` among the `shown` strings.
|
||||
fn field_after(shown: &[(String, egui::Rect)], label: &str) -> egui::Pos2 {
|
||||
let r = shown.iter().find(|(t, _)| t == label).unwrap_or_else(|| panic!("no {label:?} in {shown:?}")).1;
|
||||
egui::pos2(r.right() + 40.0, r.center().y)
|
||||
}
|
||||
|
||||
/// A 400 × 300 pt artboard at the page's origin with the ruler origin at (120, 50), and its
|
||||
/// Artboard Options open.
|
||||
fn artboard_options_from_a_ruler_origin() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
app.run("document.setRulerOrigin", json!({"x": 120, "y": 50})).unwrap();
|
||||
assert_eq!(app.ruler_origin(), vectorcraft_geom::Point::new(120.0, 50.0));
|
||||
app.run("tool.options", json!({"tool": "artboard"})).unwrap();
|
||||
app
|
||||
}
|
||||
|
||||
/// #1876: Artboard Options has X and Y beside the reference point locator, counted from the ruler
|
||||
/// origin (#1212) as the Transform panel's are. The artboard moves to a typed X and Y, its reference
|
||||
/// point landing there, in one undo step. The shape dialogs still show no X and Y (theirs are where
|
||||
/// the click was).
|
||||
#[test]
|
||||
fn artboard_options_x_and_y_count_from_the_ruler_origin_at_the_reference_point() {
|
||||
let mut app = artboard_options_from_a_ruler_origin();
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
let mut f = Frames::new();
|
||||
let has = |shown: &[(String, egui::Rect)], s: &str| shown.iter().any(|(t, _)| t == s);
|
||||
// The center by default: (200, 150) on the page, 80 and 100 from the ruler origin.
|
||||
let shown = f.texts(&mut app);
|
||||
for s in ["X:".to_string(), "Y:".to_string(), Unit::Points.format(80.0), Unit::Points.format(100.0)] {
|
||||
assert!(has(&shown, &s), "{s} in {shown:?}");
|
||||
}
|
||||
// The bottom-right corner: (400, 300) on the page, 280 and 250 from the ruler origin.
|
||||
let corner = f.control(&mut app, "Bottom Right", egui::accesskit::Role::RadioButton);
|
||||
f.press(&mut app, corner, true);
|
||||
f.press(&mut app, corner, false);
|
||||
let shown = f.texts(&mut app);
|
||||
for v in [280.0, 250.0] {
|
||||
assert!(has(&shown, &Unit::Points.format(v)), "{v} in {shown:?}");
|
||||
}
|
||||
// Typing X 300 and Y 0 puts that corner at (420, 50) on the page.
|
||||
for (label, typed) in [("X:", "300"), ("Y:", "0")] {
|
||||
let at = field_after(&shown, label);
|
||||
f.press(&mut app, at, true);
|
||||
f.press(&mut app, at, false);
|
||||
f.run(&mut app, vec![egui::Event::Text(typed.into())]);
|
||||
}
|
||||
f.key(&mut app, egui::Key::Enter, egui::Modifiers::NONE);
|
||||
assert!(app.ui.dialog.is_none(), "Enter applies the dialog");
|
||||
assert_eq!(app.session.doc().unwrap().doc.artboards[0].rect, vectorcraft_geom::Rect::new(20.0, -250.0, 420.0, 50.0));
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
open(&mut app, "rectangle", json!({"x": 10, "y": 20, "width": 30, "height": 40}));
|
||||
let shown = f.texts(&mut app);
|
||||
assert!(has(&shown, "Width:") && !has(&shown, "X:") && !has(&shown, "Y:"), "{shown:?}");
|
||||
}
|
||||
|
||||
/// #1876: a new Width or Height in Artboard Options keeps the reference point where it was, as the
|
||||
/// Transform panel's does: about the center by default, so X and Y still read the same, and from
|
||||
/// the top-left corner with that reference point.
|
||||
#[test]
|
||||
fn artboard_options_size_keeps_the_reference_point() {
|
||||
for (point, want) in
|
||||
[(None, vectorcraft_geom::Rect::new(-50.0, 0.0, 450.0, 300.0)), (Some("Top Left"), vectorcraft_geom::Rect::new(0.0, 0.0, 500.0, 300.0))]
|
||||
{
|
||||
let mut app = artboard_options_from_a_ruler_origin();
|
||||
let mut f = Frames::new();
|
||||
if let Some(point) = point {
|
||||
let at = f.control(&mut app, point, egui::accesskit::Role::RadioButton);
|
||||
f.press(&mut app, at, true);
|
||||
f.press(&mut app, at, false);
|
||||
}
|
||||
let shown = f.texts(&mut app);
|
||||
let (width, height) = (field_after(&shown, "Width:"), field_after(&shown, "Height:"));
|
||||
f.press(&mut app, width, true);
|
||||
f.press(&mut app, width, false);
|
||||
f.run(&mut app, vec![egui::Event::Text("500".into())]);
|
||||
// A click on Height ends the Width field's editing; X reads as before.
|
||||
f.press(&mut app, height, true);
|
||||
f.press(&mut app, height, false);
|
||||
let shown = f.texts(&mut app);
|
||||
let x = Unit::Points.format(if point.is_some() { -120.0 } else { 80.0 });
|
||||
assert!(shown.iter().any(|(t, _)| *t == x), "{x} in {shown:?}");
|
||||
f.key(&mut app, egui::Key::Enter, egui::Modifiers::NONE);
|
||||
assert!(app.ui.dialog.is_none(), "Enter applies the dialog");
|
||||
assert_eq!(app.session.doc().unwrap().doc.artboards[0].rect, want, "{point:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Zig Zag's dialog has Size and Ridges per segment sliders, and Relative / Absolute and Smooth /
|
||||
/// Corner as radio buttons, not dropdowns (#1133).
|
||||
#[test]
|
||||
@@ -1058,13 +1164,24 @@ impl Frames {
|
||||
out
|
||||
}
|
||||
|
||||
/// The strings the dialog layer paints once it has settled, each with its rectangle.
|
||||
fn texts(&mut self, app: &mut VectorcraftApp) -> Vec<(String, egui::Rect)> {
|
||||
self.run(app, vec![]);
|
||||
painted(&self.run(app, vec![]))
|
||||
}
|
||||
|
||||
/// The center of the button that screen readers name `label`.
|
||||
fn button(&mut self, app: &mut VectorcraftApp, label: &str) -> egui::Pos2 {
|
||||
self.control(app, label, egui::accesskit::Role::Button)
|
||||
}
|
||||
|
||||
/// The center of the control of `role` that screen readers name `label`.
|
||||
fn control(&mut self, app: &mut VectorcraftApp, label: &str, role: egui::accesskit::Role) -> egui::Pos2 {
|
||||
self.run(app, vec![]);
|
||||
let out = self.run(app, vec![]);
|
||||
let nodes = &out.platform_output.accesskit_update.as_ref().expect("an accessibility tree").nodes;
|
||||
let node = nodes.iter().find(|(_, n)| n.label() == Some(label) && n.role() == egui::accesskit::Role::Button);
|
||||
let b = node.unwrap_or_else(|| panic!("no button named {label:?}")).1.bounds().expect("bounds");
|
||||
let node = nodes.iter().find(|(_, n)| n.label() == Some(label) && n.role() == role);
|
||||
let b = node.unwrap_or_else(|| panic!("no {role:?} named {label:?}")).1.bounds().expect("bounds");
|
||||
egui::pos2((b.x0 + b.x1) as f32 * 0.5, (b.y0 + b.y1) as f32 * 0.5)
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -3858,7 +3858,9 @@ desktop app. The Flare tool's are its Flare Tool Options (dialog `flareOptions`)
|
||||
`direction` (°), and `raysOn` and `ringsOn` (the Rays and Rings checkboxes; off, the flare has none). OK keeps them
|
||||
(`tool.setOption {tool: "flare", values}`) and the next flare dragged out uses them; a click with the tool opens the
|
||||
same dialog, and its OK also draws a flare there. The Artboard tool's are the active artboard's Artboard Options
|
||||
(dialog `artboardOptions`; the Artboard tool's active artboard while it is in use, else the window's), a graph tool's
|
||||
(dialog `artboardOptions`; the Artboard tool's active artboard while it is in use, else the window's; its `x` and `y`
|
||||
are the artboard's top-left corner in document points, while the dialog shows X and Y at the reference point the
|
||||
Transform panel uses, counted from the ruler origin), a graph tool's
|
||||
the selected graph's Graph Type (an error without one), a Symbolism tool's the Symbolism Tools Options (dialog
|
||||
`symbolismOptions`: `diameter` in points, `intensity` and `density` 1–10, one brush for the eight tools) and the
|
||||
Magic Wand tool's its panel. `ui.newArtboard` (Alt-click on the Artboards panel's New Artboard) opens Artboard Options
|
||||
|
||||
File diff suppressed because one or more lines are too long
+3
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user