Merge pull request #2404 from marcoarnulfo/fix/transform-percent-size

Transform: a percentage typed alone in W or H scales the size it shows
This commit is contained in:
BFlat
2026-10-17 14:39:18 +07:00
committed by GitHub
3 changed files with 53 additions and 4 deletions
@@ -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]
+28 -3
View File
@@ -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<f64>, unit: Unit, width: f32) -> Option<f64> {
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<f64>,
unit: Unit,
width: f32,
share_of_value: bool,
) -> Option<f64> {
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<f64> {
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<egui::Modifiers> {
}
/// 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<f64>, unit: Unit, width: f32) -> Option<f64> {
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`.
+1 -1
View File
@@ -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.