From 2395a390b5ebf90f2668b40f2d3a151af74e7200 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 08:26:01 -0800 Subject: [PATCH] Control bar: the inline X, Y, W and H fields show only when they fit The bar decided to show the inline fields against a fixed 440-point width, smaller than the room the fields take, so just above that width the H field ran past the bar's right margin and was clipped. The bar now adds up the room from the values that draw the fields: the label widths in the current language, the four 80-point fields, the W/H link and the item spacing ahead of each. With less room it shows the Transform link alone, as before. --- crates/ui-egui/src/chrome/control_bar.rs | 39 +++++++++++++------ crates/ui-egui/src/chrome/tests.rs | 48 ++++++++++++++++++++++++ crates/ui-egui/src/panels/transform.rs | 6 ++- docs/target-app-parity.md | 1 + 4 files changed, 81 insertions(+), 13 deletions(-) diff --git a/crates/ui-egui/src/chrome/control_bar.rs b/crates/ui-egui/src/chrome/control_bar.rs index 58e203b75..df58c1be7 100644 --- a/crates/ui-egui/src/chrome/control_bar.rs +++ b/crates/ui-egui/src/chrome/control_bar.rs @@ -157,8 +157,27 @@ fn global_edit_options(app: &mut VectorcraftApp, ui: &mut Ui) { .show(|ui| crate::panels::properties::global_edit_options(app, ui)); } -/// The room the Control bar's inline X/Y/W/H fields need; with less, only the Transform link shows. -const INLINE_TRANSFORM_WIDTH: f32 = 440.0; +/// The width of each of the Control bar's inline X/Y/W/H fields. +const INLINE_FIELD_WIDTH: f32 = 80.0; + +/// The keys and labels of the Control bar's inline X/Y/W/H fields, in their order. +const INLINE_FIELDS: [(&str, &str); 4] = [("x", "X:"), ("y", "Y:"), ("width", "W:"), ("height", "H:")]; + +/// The label of an inline X/Y/W/H field. +fn inline_label(t: &Tokens, lbl: &str) -> egui::RichText { + egui::RichText::new(tl!(lbl)).size(12.0).color(t.text_dim) +} + +/// The room the Control bar's inline X/Y/W/H fields take: their labels as drawn in the current +/// language, the four fields, the W/H link and the item spacing ahead of each of them. With less, +/// only the Transform link shows. +fn inline_transform_width(ui: &Ui, t: &Tokens) -> f32 { + let measure = |text: egui::WidgetText| text.into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, egui::TextStyle::Body).size().x; + let labels: f32 = INLINE_FIELDS.iter().map(|(_, lbl)| measure(inline_label(t, lbl).into())).sum(); + let fields = INLINE_FIELD_WIDTH * INLINE_FIELDS.len() as f32; + let items = 2 * INLINE_FIELDS.len() + 1; + labels + fields + crate::panels::transform::CONSTRAIN_LINK_SIZE + ui.spacing().item_spacing.x * items as f32 +} /// The Control bar (Window → Control), context-sensitive like Illustrator's. pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { @@ -290,28 +309,24 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { crate::panels::transform::link(app, ui); // The bounding box, rotated with rotated objects: its centre and its own sides. if let Some(b) = app.selection_box() - && ui.available_width() >= INLINE_TRANSFORM_WIDTH + && ui.available_width() >= inline_transform_width(ui, &t) { let c = b.center(); let origin = app.ruler_origin(); let link = crate::panels::transform::proportional(app, ui); let copy = crate::panels::transform::alt_enter_copies(app, ui); - for (k, lbl, v, offset) in [ - ("x", "X:", c.x, origin.x), - ("y", "Y:", c.y, origin.y), - ("width", "W:", b.rect.width(), 0.0), - ("height", "H:", b.rect.height(), 0.0), - ] { + let values = [(c.x, origin.x), (c.y, origin.y), (b.rect.width(), 0.0), (b.rect.height(), 0.0)]; + for ((k, lbl), (v, offset)) in INLINE_FIELDS.into_iter().zip(values) { // The W/H link sits between W and H. if k == "height" { crate::panels::transform::constrain_link(app, ui); } - widgets::field_label(ui, egui::RichText::new(tl!(lbl)).size(12.0).color(t.text_dim)); + widgets::field_label(ui, inline_label(&t, lbl)); let (id, shown) = (("cb", k), Some(v - offset)); let set = if matches!(k, "width" | "height") { - widgets::size_field(ui, id, shown, units, 80.0) + widgets::size_field(ui, id, shown, units, INLINE_FIELD_WIDTH) } else { - widgets::num_field(ui, id, shown, units, 80.0) + widgets::num_field(ui, id, shown, units, INLINE_FIELD_WIDTH) }; if let Some(nv) = set { app.run("object.setBounds", json!({k: nv + offset, "reference": 4, "proportional": link, "copy": copy})).ok(); diff --git a/crates/ui-egui/src/chrome/tests.rs b/crates/ui-egui/src/chrome/tests.rs index 5c49b153d..c9109a17f 100644 --- a/crates/ui-egui/src/chrome/tests.rs +++ b/crates/ui-egui/src/chrome/tests.rs @@ -1886,3 +1886,51 @@ fn the_control_bar_shows_the_global_edit_options() { } assert!(seen, "a wide bar has room for it"); } + +/// At any width the Control bar shows the inline X, Y, W and H fields whole inside its right margin, +/// or leaves all four out and keeps the Transform link; a wider bar never leaves them out once a +/// narrower one showed them. +#[test] +fn the_control_bar_shows_the_inline_transform_fields_only_when_they_fit() { + crate::i18n::set_current(crate::i18n::Lang::EN); + for count in [1, 2] { + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + let ids: Vec<_> = (0..count) + .map(|i| app.run("shape.rectangle", json!({"x": 20 + 100 * i, "y": 20, "width": 40, "height": 30})).unwrap()["id"].clone()) + .collect(); + app.run("select.set", json!({ "ids": ids })).unwrap(); + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + crate::theme::install_fonts(&ctx); + let mut seen = false; + for width in (600..=2400).step_by(10) { + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(width as f32, 300.0)); + let mut nodes = Vec::new(); + for _ in 0..2 { + let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| super::control_bar(&mut app, ui)); + out.textures_delta.clear(); + nodes = out.platform_output.accesskit_update.map(|u| u.nodes).unwrap_or_default(); + } + let spacing = ctx.global_style().spacing.item_spacing.x; + // The right edge of the box of the field its label names. The text box sits inside the + // field's box with the same inset on both sides, and one item spacing after the label. + let right = |name: &str| { + let (id, label) = nodes.iter().find(|(_, n)| n.role() == egui::accesskit::Role::Label && n.value() == Some(name))?; + let input = nodes.iter().find(|(_, n)| n.labelled_by().contains(id)).and_then(|(_, n)| n.bounds())?; + let inset = input.x0 - label.bounds()?.x1 - f64::from(spacing); + Some(input.x1 + inset) + }; + let fields = ["X:", "Y:", "W:", "H:"].map(right); + assert!(fields.iter().all(Option::is_some) || fields.iter().all(Option::is_none), "{count} objects, {width}: {fields:?}"); + assert!(nodes.iter().any(|(_, n)| n.label() == Some("Transform")), "{count} objects, {width}: the Transform link shows"); + // The bar's content ends at its 10-point inner margin. + if let Some(h) = fields[3] { + assert!(h <= f64::from(width) - 10.0, "{count} objects, {width}: H ends at {h}"); + } + assert!(!seen || fields[3].is_some(), "{count} objects, {width}: left out after a narrower bar showed them"); + seen |= fields[3].is_some(); + } + assert!(seen, "{count} objects: a wide bar has room for the fields"); + } +} diff --git a/crates/ui-egui/src/panels/transform.rs b/crates/ui-egui/src/panels/transform.rs index df8683a01..0f1cf3db5 100644 --- a/crates/ui-egui/src/panels/transform.rs +++ b/crates/ui-egui/src/panels/transform.rs @@ -469,11 +469,15 @@ pub fn link(app: &mut VectorcraftApp, ui: &mut Ui) { }); } +/// The width and height of [`constrain_link`]'s button. +pub const CONSTRAIN_LINK_SIZE: f32 = 22.0; + /// The link between W and H (Transform panel, Properties panel, Control bar): one toggle, the /// `constrainProportions` preference, which the size fields pass on as `proportional`. pub fn constrain_link(app: &mut VectorcraftApp, ui: &mut Ui) { let on = app.session.prefs.constrain_proportions; - if widgets::icon_button(ui, if on { "link" } else { "link-2-off" }, tl!("Constrain Width and Height Proportions"), on, 22.0).clicked() { + let tip = tl!("Constrain Width and Height Proportions"); + if widgets::icon_button(ui, if on { "link" } else { "link-2-off" }, tip, on, CONSTRAIN_LINK_SIZE).clicked() { set_pref(app, "constrainProportions", !on); } } diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 44aede640..208b5e1ca 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -502,6 +502,7 @@ since 2026-10-01. - **Escape in Selection tool drags:** Escape during a Selection tool move or Alt-drag copy, a bounding-box scale or rotation, a marquee, or a corner widget, pie widget or bracket drag cancels it: the art returns to its state at the press, no copy is left, no undo step is added, the release does nothing, and the tool stays chosen. This is VectorCraft's own behavior. Regression tests: `escape_cancels_*` in `select/tests.rs` and `pie.rs`, and `escape_cancels_a_selection_tool_move_copy_scale_or_rotation` in `tests/selectescape.rs`. - **Global Edit: Shift-click leaves a match out:** while Global Edit is on, a Shift-click with the Selection tool on a highlighted match, or on an object inside a matched group, leaves that match out (`select.globalEditExclude`): it is no longer highlighted and later edits leave it as it is; the object the edit started from stays, and Global Edit ends when fewer than two matches are left ([Update assets quickly with Global Edit](https://helpx.adobe.com/ph_fil/illustrator/how-to/global-editing-sensei.html)). Regression tests: `a_shift_click_*`, `an_object_left_out_*`, `a_group_left_out_*` and `leaving_out_all_but_one_*` in `crates/engine/src/tests/globaledit.rs`. - **Global Edit Options in the Control bar:** while Select › Start Global Edit can run, the Control bar shows a Global Edit Options dropdown that opens the same options as the Properties panel's (Match: Appearance, Size; On Artboards; Include Objects On Canvas; Result(s)); both set the same options. The dropdown is off while Global Edit is on; it sits after Recolor Artwork, ahead of the X/Y/W/H fields ([Edit similar objects globally](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-similar-objects.html)). Regression test: `the_control_bar_shows_the_global_edit_options` in `crates/ui-egui/src/chrome/tests.rs`. +- **Control bar's inline X, Y, W and H fields fit the bar:** the Control bar shows the inline X, Y, W and H fields only when all four, their labels as drawn in the current language and the W/H link fit inside the bar's margin; otherwise it shows the Transform link alone. This is VectorCraft's own layout rule. Regression test: `the_control_bar_shows_the_inline_transform_fields_only_when_they_fit` in `crates/ui-egui/src/chrome/tests.rs`. - **Pattern-only backquote drags:** With Selection move, bounding-box scale or rotate, or the Rotate, Reflect, Scale or Shear tool, backquote held at mouse release applies the drag matrix to selected pattern paint while object geometry stays in place. The key may be pressed after mouse press; if released before the mouse button, the drag commits as an ordinary transform. Objects without pattern paint stay still; stroke-only patterns and patterned objects in mixed selections take the same matrix. Regression tests: `backquote_move_uses_release_state_and_one_commit` and `backquote_box_scale_and_rotate_use_pattern_matrix_at_release` in `select/tests.rs`, `backquote_drag_previews_and_commits_pattern_only_for_each_transform_tool` in `xform/transform.rs`, `selection_backquote_drag_changes_pattern_fill_and_stroke_without_moving_mixed_art` and `selection_backquote_drag_changes_stroke_only_pattern` in `tests/pattern.rs`, `egui_backtick_drag_uses_key_state_at_release` in `canvas/tests.rs`, and `synthetic_pointer_backquote_keeps_solid_object_in_place` in `control/tests.rs`. Scores and hours unchanged. - **Existing-guide Escape cancellation (G1):** Escape clears a pending guide press or drag through Selection and Direct/Group Selection, cancelling a begun preview. Eight app-input regressions cover both axes, move/copy, temporary selection-tool borrowing from idle Pen, preselected-guide document/interaction-start selection and Undo/Redo/saved-state preservation, later on-canvas motion/release, Shift-press cancellation, and ordinary-release/Enter/idle controls. Historical focused checks on `8ab574b7` recorded five expected RED failures with three passing controls, then all eight GREEN tests and the Direct Selection Escape control passed. All nine also passed in current full CI. A full CI retry on the five-unit combined source at `a0671777` passed all seven stages with 6,613 test outcomes on 2026-10-14. An earlier full-CI run on the identical source panicked in the randomized `ai_mutated_save_never_panics` test; the cause remains unresolved. This five-fix batch does not claim to repair that failure. The original 1280×960 headless app capture from `b076025a` was reviewed for one vertical Selection guide after Escape; source review found its scene unchanged on `a0671777`. Scores and hours unchanged. - **Escape in more tool drags:** Escape during an Anchor Point drag, a Mesh tool drag, a Direct Selection drag of a mesh point, a corner widget or the brackets of type on a path, or a Shape Builder drag cancels it: the art goes back to how the press found it, no undo step is added, the release does nothing, and the tool stays chosen. Escape during a Measure tool drag brings back the measurement before it. Regression tests: `escape_cancels_*` in `draw2/anchor/tests.rs`, `meshedit.rs`, `direct/tests.rs`, `builder/tests.rs` and `xform/measure.rs`, and `escape_cancels_an_anchor_point_mesh_shape_builder_or_measure_drag` in `tests/toolescape.rs`.