From 8148d024eaf13f20f6363f5fe8b690de3e41c929 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 16 Oct 2026 14:27:41 +0200 Subject: [PATCH] Transform: a percentage typed alone in W or H scales the size it shows (fixes #2401) --- crates/ui-egui/src/panels/tests_constrain.rs | 24 +++++++++++++++ crates/ui-egui/src/widgets.rs | 31 ++++++++++++++++++-- docs/target-app-parity.md | 2 +- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/panels/tests_constrain.rs b/crates/ui-egui/src/panels/tests_constrain.rs index ad0e7976e..1ccf8ac01 100644 --- a/crates/ui-egui/src/panels/tests_constrain.rs +++ b/crates/ui-egui/src/panels/tests_constrain.rs @@ -216,6 +216,30 @@ fn ctrl_cmd_enter_scales_the_artboard_proportionally() { assert_eq!(board(End::Enter(Modifiers::COMMAND)), (400.0, 300.0)); } +/// A percentage typed alone in W or H (`25%`) is a share of the size the field shows, as in +/// Illustrator: with the link off only that side changes, with it on the other side follows, and +/// Ctrl/Cmd+Enter keeps the proportions as with a length. +#[test] +fn a_percentage_in_a_size_field_scales_the_size_it_shows() { + let surfaces: [(&str, Draw); 3] = + [("Transform panel", transform::show), ("Properties", properties::transform_section), ("Control bar", crate::chrome::control_bar)]; + for (name, draw) in surfaces { + let mut app = rect_app(); + set_field(&mut app, draw, "100 pt", "25%", End::Enter(Modifiers::NONE)); + assert_eq!(size(&app), (25.0, 50.0), "{name}, W"); + let mut app = rect_app(); + set_field(&mut app, draw, "50 pt", "150 %", End::Enter(Modifiers::NONE)); + assert_eq!(size(&app), (100.0, 75.0), "{name}, H"); + let mut app = rect_app(); + set_field(&mut app, draw, "100 pt", "200%", End::Enter(Modifiers::COMMAND)); + assert_eq!(size(&app), (200.0, 100.0), "{name}, Ctrl/Cmd+Enter"); + let mut app = rect_app(); + run(&mut app, "prefs.set", json!({"key": "constrainProportions", "value": true})); + set_field(&mut app, draw, "50 pt", "300%", End::Enter(Modifiers::NONE)); + assert_eq!(size(&app), (300.0, 150.0), "{name}, linked"); + } +} + /// A length field of the New Document dialog keeps the focus on Ctrl/Cmd+Enter, and the dialog /// creates no document. #[test] diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 0dd7347e8..3c5a81d20 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -212,6 +212,19 @@ pub(crate) fn typed_unit(ctx: &egui::Context, unit: Unit) -> Unit { /// without a unit is in `unit` ([`typed_unit`]: in points in a picas field with Numbers Without /// Units Are Points on). pub fn num_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, value: Option, unit: Unit, width: f32) -> Option { + unit_field(ui, id, value, unit, width, false) +} + +/// [`num_field`]; with `share_of_value` a percentage typed alone is that share of `value` +/// ([`share_of`]). +fn unit_field( + ui: &mut Ui, + id: impl std::hash::Hash + std::fmt::Debug, + value: Option, + unit: Unit, + width: f32, + share_of_value: bool, +) -> Option { let id = ui.id().with(id); let shown = value.map(|v| unit.format(v)).unwrap_or_default(); take_dialog_focus(ui, id, &shown); @@ -223,7 +236,18 @@ pub fn num_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, value: let scrubbed = scrub::field(ui, id, rect, &buf, value.map(|v| unit.from_pt(v)), STEP_DECIMALS); let commit = resp.lost_focus() && buf != shown; ui.data_mut(|d| d.insert_temp(id, buf.clone())); - if commit { read.parse(&buf) } else { stepped.or(scrubbed).map(|v| unit.to_pt(v)) } + if commit { + read.parse(&buf).or_else(|| if share_of_value { share_of(&buf, value?) } else { None }) + } else { + stepped.or(scrubbed).map(|v| unit.to_pt(v)) + } +} + +/// A percentage typed alone (`50%`, `150 %`, `100/3%`) as that share of `value`; `None` for any +/// other text. `Unit::parse` reads a bare percentage as nothing, having no value to take it of. +fn share_of(text: &str, value: f64) -> Option { + let percent = vectorcraft_doc::parse_number(text.trim().strip_suffix('%')?)?; + Some(value * percent / 100.0) } /// The places a stepped value in a unit field keeps (no 12.300000001). @@ -333,7 +357,8 @@ pub(crate) fn enter_modifiers(ui: &Ui) -> Option { } /// A [`num_field`] for a width or height (Transform panel, Properties panel, Control bar): Ctrl/Cmd+Enter -/// also commits it and ends its editing, as Enter does, and [`enter_modifiers`] reports Command. +/// also commits it and ends its editing, as Enter does, and [`enter_modifiers`] reports Command. A +/// percentage typed alone (`50%`) is that share of the size shown, as in Illustrator (#2401). pub fn size_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, value: Option, unit: Unit, width: f32) -> Option { use egui::{Event, Key}; // The text box does not end its editing on an Enter with Ctrl/Cmd: a plain Enter goes in right @@ -348,7 +373,7 @@ pub fn size_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, value } }); } - num_field(ui, id, value, unit, width) + unit_field(ui, id, value, unit, width, true) } /// Select all of `text` in text field `id`. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 6aa604036..3f46f4019 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -455,7 +455,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Dialogs move by dragging their heading (the band down from the top edge, as a title bar), stay on screen and keep where they were moved for the session; the form dialogs' choices are dropdowns (Offset Path's Joins, Average's and Shear's and Reflect's axis), and Offset Path lists Offset, Joins, Miter Limit and previews (#423). - Dialogs open with their first field focused and its value selected: type a value and press Enter (Move, Rotate, Scale…). A dialog opened from a menu takes the keyboard focus every time it opens, not only the first (#1367), and one whose first field starts greyed out (Blend Options on Smooth Color, Layers Panel Options) gives the focus to that field once it is enabled (#1395). Number fields in panels, the Control bar and dialogs (preferences too) step with Up/Down, or with the mouse wheel over the focused field (#485: a notch a step; the panel under it then stays put, and an unfocused field leaves the wheel to scrolling) (Shift: ten, Ctrl/Cmd: a tenth of their unit), applied at once. - Scrubby labels (#400, beyond Illustrator, as in Photoshop): dragging a numeric field's label left or right (the ↔ cursor shows over it) steps the value one unit every 2 points (Shift: ten, Ctrl/Cmd: a tenth; counts stay whole), in the field's unit and within its limits, applied live as the art follows; the whole drag is one undo step and Escape puts the value back. A click on the label focuses the field; the text box itself never scrubs, and a field with no label doesn't. Text labels before a field (Transform, Properties, Control bar, Stroke, Transparency, form rows in dialogs) and the Character and Paragraph panels' icons act as labels. Preferences › General › Scrub Numeric Fields by Dragging (on by default) turns it off. - - In the Transform panel, the Properties panel's Transform section and the Control bar, Alt+Enter (Option+Return) applies a value to a copy of the selection and selects the copy, in one undo step. With anchor points selected with Direct Selection it applies the value to them, as Enter does. Ctrl/Cmd+Enter in W or H applies the value and scales the selection proportionally with the link off. With the Artboard tool, it scales the active artboard proportionally. + - In the Transform panel, the Properties panel's Transform section and the Control bar, Alt+Enter (Option+Return) applies a value to a copy of the selection and selects the copy, in one undo step. With anchor points selected with Direct Selection it applies the value to them, as Enter does. Ctrl/Cmd+Enter in W or H applies the value and scales the selection proportionally with the link off. With the Artboard tool, it scales the active artboard proportionally. A percentage typed alone in W or H (`50%`) is that share of the size the field shows, and the link and Ctrl/Cmd+Enter keep the proportions as with a length (#2401). - The Control bar's underlined Transform link opens the Transform panel in a popover under it (reference point, X/Y/W/H, Rotate and Shear with their icons and angle presets, flips, shape properties, Scale Corners and Scale Strokes & Effects), as its Stroke link does the Stroke panel; X/Y/W/H also show inline while the bar has room. The reference point locator is nine squares, the outer eight joined around the box. - In isolation mode the Control bar has an Exit Isolation Mode button (`object.exitIsolation`), except while a pattern is edited. Outside isolation mode, with one group, compound path, path, image, mesh or placed file selected, it has an Isolate Selected Object button (`object.isolate`), which also isolates a path inside a group that Direct Selection selected. - Tab shows and hides the panels (in the Type tool it types a tab) without moving the keyboard focus onto a field, so the next shortcut or letter still works; in dialogs and panel fields it moves between fields.