mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Fix #1876: Artboard Options has X and Y with a reference point
Artboard Options showed Name, Width and Height only: the generic form hides x and y, which in the shape dialogs are the click position. The artboard's position could be dragged but not typed. The dialog now draws its own rows after the form's, in the same grid: the reference point locator the Transform panel, the Control bar and Properties share, then X and Y. They show where the reference point lies, counted from the ruler origin (#1212), as the Transform panel and the Control bar show an artboard's position. A typed X or Y moves the artboard so that its reference point lands there, and a new Width or Height keeps the reference point in place. The fields still hold the top-left corner in document points, so OK runs artboard.setProps as before, in one undo step. The shape dialogs keep their x and y hidden. form::grid_then lets a dialog add rows to the generic grid.
This commit is contained in:
committed by
bflatastic
parent
6190525482
commit
2a5dfa6a94
@@ -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