diff --git a/crates/ui-egui/src/field_tests.rs b/crates/ui-egui/src/field_tests.rs index e1d17c271..566f0828b 100644 --- a/crates/ui-egui/src/field_tests.rs +++ b/crates/ui-egui/src/field_tests.rs @@ -1153,3 +1153,44 @@ fn prepared_save_cannot_enter_a_replacement(route: &str, save_as: bool, discard: assert!(u.app.ui.after_field.is_none()); std::fs::remove_dir_all(&dir).ok(); } + +/// Replacing a painted value with its editor must preserve its baseline, including inside +/// the horizontal rows used by the sidebar. +#[test] +fn sidebar_field_text_stays_in_place_on_focus() { + for numeric in [true, false] { + let ctx = egui::Context::default(); + crate::theme::apply(&ctx, crate::theme::Tokens::light()); + let id = Id::unique("baseline_field"); + let draw = |events| { + let mut out = ctx.run_ui( + egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, egui::vec2(400.0, 200.0))), events, ..Default::default() }, + |ui| { + ui.horizontal(|ui| { + if numeric { + crate::widgets::number_field(ui, id, crate::widgets::Lead::None, Some(123.0), "", 100.0, 1.0); + } else { + crate::widgets::text_field(ui, id, "123", 100.0, ""); + } + }); + }, + ); + out.textures_delta.clear(); + out + }; + draw(vec![]); + let before = draw(vec![]); + let bounds = |out: &egui::FullOutput| painted_texts(&out.shapes).into_iter().find(|(text, _, _)| text == "123").expect("field text").2; + let before = bounds(&before); + let p = before.center(); + for pressed in [true, false] { + draw(vec![ + Event::PointerMoved(p), + Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE }, + ]); + } + assert!(ctx.memory(|m| m.has_focus(id.with("te"))), "editor is focused"); + let after = bounds(&draw(vec![])); + assert!((before.center().y - after.center().y).abs() <= 0.5, "numeric={numeric}: before={before:?}, after={after:?}"); + } +} diff --git a/crates/ui-egui/src/panels/typography/spacing.rs b/crates/ui-egui/src/panels/typography/spacing.rs index c6bc7a51d..54c327dd0 100644 --- a/crates/ui-egui/src/panels/typography/spacing.rs +++ b/crates/ui-egui/src/panels/typography/spacing.rs @@ -88,6 +88,8 @@ pub(super) fn spacing_field( out } +/// A spacing value beside its scrub icon, centered in the same field while idle and editing. +/// Commits typed text on Enter or focus loss; Escape cancels the draft. fn lead_field(ui: &mut Ui, id: Id, glyph: LeadGlyph, value: &str, width: f32, tip: &str) -> Option { let t = Tokens::get(ui.ctx()); let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover()); @@ -112,12 +114,13 @@ fn lead_field(ui: &mut Ui, id: Id, glyph: LeadGlyph, value: &str, width: f32, ti let mut out = None; match editing { Some(mut text) => { - let mut child = ui.new_child(egui::UiBuilder::new().max_rect(text_rect.shrink2(vec2(2.0, 4.0)))); + let mut child = ui.new_child(egui::UiBuilder::new().max_rect(text_rect.shrink2(vec2(2.0, 0.0)))); let r = child.add( egui::TextEdit::singleline(&mut text) .id(id.with("te")) .frame(egui::Frame::NONE) .font(regular(11.0)) + .align(Align2::LEFT_CENTER) .desired_width(text_rect.width() - 6.0), ); crate::a11y::own_focus(&r); diff --git a/crates/ui-egui/src/typography_scrub_tests.rs b/crates/ui-egui/src/typography_scrub_tests.rs index 9b95b8e50..df61936db 100644 --- a/crates/ui-egui/src/typography_scrub_tests.rs +++ b/crates/ui-egui/src/typography_scrub_tests.rs @@ -30,6 +30,10 @@ impl Fields { f } fn frame(&mut self, events: Vec, modifiers: Modifiers) -> egui::CursorIcon { + self.frame_output(events, modifiers).platform_output.cursor_icon + } + /// Draws the real typography controls, retaining their shapes for position assertions. + fn frame_output(&mut self, events: Vec, modifiers: Modifiers) -> egui::FullOutput { self.time += 1.0 / 60.0; let mut events = events; events.insert(0, Event::ModifiersChanged(modifiers)); @@ -42,7 +46,7 @@ impl Fields { crate::typography::typography(&mut self.app, ui, &doc, &sel); }); out.textures_delta.clear(); - out.platform_output.cursor_icon + out } /// Screen position of a typography field's label/lead, used to start a scrub. fn lead(&self, key: &str) -> Pos2 { @@ -159,20 +163,34 @@ fn typography_icon_scrub_changes_only_the_selected_characters_without_ending_tex } /// Asserts typography spacing fields accept typed percent units, commit on Enter, and -/// Escape restores the previous value without adding an undo step. +/// Escape restores the previous value without adding an undo step. Focusing the editor must +/// also leave the painted value at the same vertical position. #[test] fn typography_spacing_fields_still_accept_typed_units_and_escape_cancels() { let mut f = Fields::new(); let p = f.lead("ty_lh") + vec2(36.0, 0.0); + let field = f.ctx.read_response(Id::unique("ty_lh").with("click")).unwrap().rect; + let text_bounds = |out: egui::FullOutput| { + out.shapes + .iter() + .find_map(|shape| match &shape.shape { + egui::Shape::Text(text) if text.galley.text() == "24" && field.contains(text.pos) => Some(text.visual_bounding_rect()), + _ => None, + }) + .expect("the line-height value is painted inside its field") + }; + let before = text_bounds(f.frame_output(vec![], Modifiers::NONE)); for pressed in [true, false] { f.frame( vec![Event::PointerMoved(p), Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE }], Modifiers::NONE, ); } - f.frame(vec![], Modifiers::NONE); - assert!(f.ctx.data(|d| d.get_temp::(Id::unique("ty_lh").with("editing"))).is_some(), "click starts typing"); f.ctx.memory_mut(|m| m.request_focus(Id::unique("ty_lh").with("te"))); + let after = text_bounds(f.frame_output(vec![], Modifiers::NONE)); + assert!((before.top() - after.top()).abs() <= 0.5 && (before.bottom() - after.bottom()).abs() <= 0.5, "before={before:?}, after={after:?}"); + assert!(f.ctx.memory(|m| m.has_focus(Id::unique("ty_lh").with("te"))), "line-height editor is focused"); + assert!(f.ctx.data(|d| d.get_temp::(Id::unique("ty_lh").with("editing"))).is_some(), "click starts typing"); f.frame( vec![Event::Key { key: egui::Key::A, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::COMMAND }], Modifiers::COMMAND, diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index b6b4b7e88..c0f2f814e 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -361,7 +361,9 @@ fn number_field_impl( .font(regular(11.0)) .desired_width(edit_rect.width() - 6.0) .align(egui::Align2::LEFT_CENTER); - let mut child = ui.new_child(egui::UiBuilder::new().max_rect(edit_rect.shrink2(vec2(0.0, 4.0)))); + // TextEdit already reserves the theme's 24 pt interaction height. Insetting its + // top would move its baseline below the centered value painted while idle. + let mut child = ui.new_child(egui::UiBuilder::new().max_rect(edit_rect)); let r = child.add(te); a11y::own_focus(&r); keep_focus(&r, started); @@ -628,12 +630,13 @@ fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str /// its own (the field draws it), showing `hint` while empty; `password` shows dots. fn field_editor(ui: &mut Ui, rect: Rect, id: Id, text: &mut String, hint: &str, password: bool) -> Response { let t = Tokens::get(ui.ctx()); - let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect.shrink2(vec2(8.0, 4.0)))); + let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect.shrink2(vec2(8.0, 0.0)))); let r = child.add( egui::TextEdit::singleline(text) .id(id) .frame(egui::Frame::NONE) .password(password) + .align(Align2::LEFT_CENTER) .font(regular(11.0)) .hint_text(egui::RichText::new(hint).font(regular(11.0)).color(t.text_tertiary)) .desired_width(rect.width() - 16.0), diff --git a/docs/ui-parity.md b/docs/ui-parity.md index b46288714..7b35ffa24 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); colour picker review fixes for text undo, fitted sidebars, moving controls and gradient variables; Corner smoothing: an original icon and the shared number-field drag gesture, preserving fractional baselines; guides snap to layers when dragged; Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); the Design panel's sections on one grid — 16 / 8 margins, 88 + 8 + 88 + 8 + 24 columns, 24 pt rows, 4 pt between a row's buttons (#858); the toolbar on one 32 pt grid, without its Resources button (#1210); colour picker LCH and OKLCH models; an XML (Android Vector Drawable) export format; Context menus: the Menu key reaches egui 0.37 but is unbound (row done → partial); the Text on a path tool; board tool keys pinned; community fixes: the text box by dragging, the font picker keys, the pen path on leaving Design, a still canvas in vector edit mode, a scrolling pages list, colour emoji, readable new text in frames, system theme on first run; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; a rename selects the whole name, so typing replaces it; Escape or Enter after a pen path stays on the same vector (#760); IME composition kept on through idle frames of a text edit, candidates at the caret (#747); Selection colors for mixed selections and smart-selection spacing (#753); submenus with a hover delay and a safe triangle; the Actions palette's tabs, ranked search, highlight and recents; menu items that ask for a value; editor fixes from the community batches) · **Target:** Figma Design UI3 (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (sidebar number and text fields keep their text vertically centered when focused, including typography spacing; with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); colour picker review fixes for text undo, fitted sidebars, moving controls and gradient variables; Corner smoothing: an original icon and the shared number-field drag gesture, preserving fractional baselines; guides snap to layers when dragged; Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); the Design panel's sections on one grid — 16 / 8 margins, 88 + 8 + 88 + 8 + 24 columns, 24 pt rows, 4 pt between a row's buttons (#858); the toolbar on one 32 pt grid, without its Resources button (#1210); colour picker LCH and OKLCH models; an XML (Android Vector Drawable) export format; Context menus: the Menu key reaches egui 0.37 but is unbound (row done → partial); the Text on a path tool; board tool keys pinned; community fixes: the text box by dragging, the font picker keys, the pen path on leaving Design, a still canvas in vector edit mode, a scrolling pages list, colour emoji, readable new text in frames, system theme on first run; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; a rename selects the whole name, so typing replaces it; Escape or Enter after a pen path stays on the same vector (#760); IME composition kept on through idle frames of a text edit, candidates at the caret (#747); Selection colors for mixed selections and smart-selection spacing (#753); submenus with a hover delay and a safe triangle; the Actions palette's tabs, ranked search, highlight and recents; menu items that ask for a value; editor fixes from the community batches) · **Target:** Figma Design UI3 (desktop app 126.10.7) Fine canvas control is what Figma users feel first: handles, snapping, nudging, modifier keys, drag behaviour, numeric precision. This checklist covers the tools, gestures and panels. Status: @@ -28,7 +28,7 @@ interaction feel not yet compared. | Several windows | partial | File ▸ New window, Open in new window on a tab, a tab dragged off the strip; screen-reader tree in the main window only; positions not restored | | Multiplayer avatars, Share button | missing | waits for multiplayer | | Colour names in hex fields | done | every hex field (fills, strokes, text ranges, effects, layout grids, colour variables, page and prototype backgrounds, the colour picker's Hex) takes a CSS colour name as well: `red` becomes `FF0000`; case, spaces and hyphens are ignored (`Light Blue`); names are opaque and keep the colour's opacity; hex wins where both could match. The picker's CSS field takes every CSS name too (`Color::from_hex_or_name`, `color::named`) | -| Fields select their text on focus | done | a click or Tab into a one-line field selects all of its text, so typing replaces the value: hex and colour fields, number fields, inline fields (a fill's hex, variable values, renames), names, search boxes and dialog fields; a drag from the press selects only what it covers; multi-line fields (descriptions, comments) keep the caret where clicked. The first click into an inline field (a fill's hex) now takes the keyboard; it took a second click. A rename (double-click, F2, File ▸ Rename, a page) selects the whole name before its field first draws, so a key typed in that same frame replaces the name; it was appended (`widgets::select_all_on_focus`, `select_all_tests`, `layers_rename_tests`) | +| Fields select their text on focus | done | Number and text values keep their vertical position when their editor gains focus (including typography spacing; `sidebar_field_text_stays_in_place_on_focus`). a click or Tab into a one-line field selects all of its text, so typing replaces the value: hex and colour fields, number fields, inline fields (a fill's hex, variable values, renames), names, search boxes and dialog fields; a drag from the press selects only what it covers; multi-line fields (descriptions, comments) keep the caret where clicked. The first click into an inline field (a fill's hex) now takes the keyboard; it took a second click. A rename (double-click, F2, File ▸ Rename, a page) selects the whole name before its field first draws, so a key typed in that same frame replaces the name; it was appended (`widgets::select_all_on_focus`, `select_all_tests`, `layers_rename_tests`) | | Colour picker models | done | Hex, RGB, HSL, HSB, LCH, OKLCH and CSS; LCH and OKLCH keep lightness and hue when a colour leaves sRGB (chroma comes down); the values typed stay while the colour is theirs (a hue typed on a grey, a chroma sRGB can't hold); CSS reads `lch()` and `oklch()` with any hue unit and `none` (`figcraft_color::{Lch, Oklch}`). Values are the stored channels read as sRGB | | Colour picker placement and dismissal | done | Opens 8 pt beside the sidebar at the opening control, lifted to fit the screen; outside presses (including the sidebar), Escape and X close it. Reopening resets a dragged position; eyedropper sampling keeps it open. | | Colour picker on a new fill or stroke | done | + opens the picker on the new paint when the selection shares one paint list; on a mixed selection + adds a paint to each layer and no picker opens (#404) | @@ -175,6 +175,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | M1: sidebar number and text fields keep their text vertically centered when focused, including typography spacing. Editors use the full 24 pt field height instead of adding a 4 pt top inset to an editor already at least 24 pt tall. Regression: `field_tests::sidebar_field_text_stays_in_place_on_focus` compares painted text positions before and after focus. Readiness estimates unchanged. | | 2026-10-30 | minor | Pen into vector edit mode (#1430): Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on; Escape keeps the pen. Numbers unchanged. | | 2026-10-30 | minor | M8, #1574: the Actions palette (Ctrl+K) offered engine commands that need parameters it can't give, such as `image.place` ("Place image", needing the image's `data` or `path`), which failed with "Invalid parameters" and sat above the menu's Place image… row with the same label. A search now leaves out a command no menu lists when its params doc names a parameter without `?` (`figcraft_engine::cmd::needs_params`), and a row no menu item gives whose label reads like a menu item's (any case, without the ellipsis). Place image runs File ▸ Place image…, which opens the file dialog. Tests: `palette_tests.rs` (no row needs parameters unless a menu item runs it, no duplicate label across several searches), `unknown_params_tests.rs`. | | 2026-10-30 | minor | Preferences ▸ Scroll wheel zoom (#1390): a mouse wheel zooms about the pointer and Cmd/Ctrl+wheel scrolls; Shift+wheel, trackpads and pinches unchanged. Numbers unchanged. |