diff --git a/crates/engine/src/tools.rs b/crates/engine/src/tools.rs index dbda1894b..dba3eaf7e 100644 --- a/crates/engine/src/tools.rs +++ b/crates/engine/src/tools.rs @@ -1875,6 +1875,17 @@ impl Session { self.tool.overlay.marquee = None; self.cancel_interaction()?; } + // A frame's or section's name label: rename it there (the canvas puts a field over it). + if matches!(self.tool.tool, ToolId::Move | ToolId::Scale) + && !(ev.shift || ev.cmd || ev.alt) + && let Some(id) = label_at(&doc, page, &self.tool.labels, ev.p()) + { + if sel != [id] { + self.set_selection(vec![id]); + } + self.request_ui("canvas.renameLabel", serde_json::json!({"id": id})); + return Ok(()); + } let Some(leaf) = self.hit(&doc, page, ev.p(), ev.tol() * 2.0) else { return Ok(()) }; let target = select_target(&doc, leaf, &sel, true); // Text: edit at the click. diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index bc1842704..4c2b66ef0 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -242,6 +242,7 @@ pub fn show(app: &mut FigApp, ui: &mut Ui, worker: &mut Worker) { painter.image(tex.id(), image_rect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); } app.session.tool.labels = overlays(app, &painter, &t); + crate::frame_labels::rename_field(app, ui); // The zoom tool's area. if let (Some(start), Some(p)) = (app.canvas.zoom_drag, ctx.input(|i| i.pointer.hover_pos())) { let area = Rect::from_two_pos(start, p); diff --git a/crates/ui-egui/src/frame_label_tests.rs b/crates/ui-egui/src/frame_label_tests.rs index 38ebef0a1..d7f2371ab 100644 --- a/crates/ui-egui/src/frame_label_tests.rs +++ b/crates/ui-egui/src/frame_label_tests.rs @@ -79,3 +79,58 @@ fn pressing_a_rotated_label_selects_and_drags_the_frame() { assert!(after.x0 > before.x0 + 1.0 && after.y0 > before.y0 + 1.0, "{before:?} → {after:?}"); assert_eq!(u.app.session.ds().unwrap().history.undo.len(), undo + 1); } + +fn key(k: egui::Key) -> Vec { + vec![ + Event::Key { key: k, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }, + Event::Key { key: k, physical_key: None, pressed: false, repeat: false, modifiers: Default::default() }, + ] +} + +/// Double-click `f`'s label and wait for the field to take the keyboard. +fn double_click_label(u: &mut Ui, f: NodeId) { + let at = mid(&label(u, f)); + for _ in 0..2 { + u.button(at, true); + u.button(at, false); + } + u.frame(vec![]); + u.frame(vec![]); +} + +fn name(u: &Ui, f: NodeId) -> String { + u.app.session.doc().unwrap().node(f).unwrap().name.clone() +} + +#[test] +fn double_clicking_a_label_renames_the_frame_there() { + let (mut u, a, b) = setup(); + let before = u.app.session.doc().unwrap().world_bounds(a).unwrap(); + double_click_label(&mut u, a); + assert_eq!(u.app.session.selection(), vec![a]); + assert_eq!(u.app.ui.rename_label.map(|r| r.0), Some(a), "a field over the label"); + // The name starts selected: typing replaces it, Enter keeps it. + u.frame(vec![Event::Text("Sign in".into())]); + u.frame(key(egui::Key::Enter)); + u.frame(vec![]); + assert_eq!(name(&u, a), "Sign in"); + assert!(u.app.ui.rename_label.is_none()); + // The double-click didn't move the frame or rename the other one. + assert_eq!(u.app.session.doc().unwrap().world_bounds(a).unwrap(), before); + assert_eq!(name(&u, b), "Turned"); + // One undo step puts the old name back. + u.app.session.undo().unwrap(); + assert_eq!(name(&u, a), "Upright"); +} + +#[test] +fn escape_leaves_a_label_rename_with_the_old_name() { + let (mut u, _, b) = setup(); + double_click_label(&mut u, b); + assert_eq!(u.app.ui.rename_label.map(|r| r.0), Some(b), "rotated labels rename too"); + u.frame(vec![Event::Text("Nope".into())]); + u.frame(key(egui::Key::Escape)); + u.frame(vec![]); + assert_eq!(name(&u, b), "Turned"); + assert!(u.app.ui.rename_label.is_none()); +} diff --git a/crates/ui-egui/src/frame_labels.rs b/crates/ui-egui/src/frame_labels.rs index 2718fddd1..43dd712db 100644 --- a/crates/ui-egui/src/frame_labels.rs +++ b/crates/ui-egui/src/frame_labels.rs @@ -151,6 +151,37 @@ pub fn draw(app: &FigApp, painter: &Painter, t: &Tokens, doc: &Document, id: Nod Some([area.left_top(), area.right_top(), area.right_bottom(), area.left_bottom()].map(|p| tl + rot * (p - tl))) } +/// Rename `id`'s name label on the canvas: a field over the label from the next frame on. +pub fn start_rename(app: &mut FigApp, id: NodeId) { + // The field takes the focus once it's drawn (as the pages list does). + app.ui.rename_label = Some((id, true)); +} + +/// The field over a label being renamed (see [`start_rename`]): Enter or a click elsewhere +/// renames the layer, Escape keeps the name. +pub fn rename_field(app: &mut FigApp, ui: &mut egui::Ui) { + let Some((id, start)) = app.ui.rename_label else { return }; + let quad = app.session.tool.labels.iter().find(|(i, _)| *i == id).map(|(_, q)| q.map(|p| app.to_screen(p.x, p.y))); + let name = app.session.doc().ok().and_then(|d| d.node(id)).map(|n| n.name.clone()); + let (Some(quad), Some(name)) = (quad, name) else { + app.ui.rename_label = None; + return; + }; + let area = Rect::from_points(&quad); + let font = regular(11.0); + let text_w = ui.painter().layout_no_wrap(name.clone(), font.clone(), Color32::WHITE).size().x; + let rect = Rect::from_min_size(pos2(area.min.x, area.max.y - 16.0), vec2((text_w + 24.0).max(area.width()).clamp(80.0, 480.0), 16.0)); + let (done, open) = crate::widgets::rename_field(ui, egui::Id::new(("frame_label_rename", id)), &name, rect, font, start); + if let Some(new) = done { + app.ui.rename_label = None; + app.run("object.rename", serde_json::json!({"ids": [id], "name": new})); + } else if !open && !start { + app.ui.rename_label = None; + } else { + app.ui.rename_label = Some((id, false)); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index e6a14ede4..6141b3dd8 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -430,6 +430,9 @@ pub struct UiState { /// The layer being renamed in the layers panel. pub rename: Option, pub rename_page: Option<(NodeId, String)>, + /// The frame or section whose name label on the canvas is being renamed (a double-click on + /// it), and whether the rename just started (its field takes the keyboard when next drawn). + pub rename_label: Option<(NodeId, bool)>, pub main_menu: bool, pub palette: Option, /// The Actions palette's tab. @@ -587,6 +590,7 @@ impl Default for UiState { expanded: BTreeSet::new(), rename: None, rename_page: None, + rename_label: None, main_menu: false, palette: None, palette_tab: menus::PaletteTab::All, @@ -961,6 +965,11 @@ impl FigApp { "layers.collapseAll" => layers::collapse_all(self), "contextMenu" => self.ui.key_menu = true, "selectArea" => self.start_key_box(), + "canvas.renameLabel" => { + if let Some(id) = r.data.get("id").and_then(figcraft_engine::params::id_of) { + frame_labels::start_rename(self, id); + } + } "layers.rename" => { let ids: Vec = r.data.get("ids").and_then(Value::as_array).into_iter().flatten().filter_map(figcraft_engine::params::id_of).collect(); diff --git a/docs/roadmap.md b/docs/roadmap.md index 8d63a4692..b89f18423 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -73,6 +73,7 @@ blockers. | Date | Change | Summary | |---|---|---| +| 2026-10-17 | minor | M1: double-clicking a frame's or section's name on the canvas renames it in place (#757; Enter commits, Escape cancels, one undo step). Numbers unchanged | | 2026-10-17 | minor | Community batch #753 (10 PRs): Selection colors for mixed selections (#403); smart-selection spacing fields and canvas handles (#402); a Present tab with device and scale controls (#537); adding auto layout to an empty frame keeps its size (#369); `node.set` applies auto layout keys to frames only and warns about font styles a family lacks (#603); canvas images on whole device pixels (#359); Adwaita window controls on Wayland without server decorations (#592, not run on GNOME or KDE by the maintainers); the keyboard shortcuts panel closes on the home screen (#548); macOS paste modifiers in tests (#750); credits column tests (#347). Numbers unchanged. | | 2026-10-17 | minor | Community fixes from #524 up (batches #727, #728, #733, #735, #736 and #729; 55 PRs) and a crash guard. `.fig` import: explicit instance identity, overrides by key, text style precedence, grid track sizes, slot content, connector labels, skip ink, paragraph options and hanging lists, list start numbers, vertical trim, per-point stroke joins, text on a path, image adjustments and embedded images; large files open within budgets from the file's own size; an import report after opening; imported bytes stay out of the command journal. Files and paste: save a copy as `.fig`; Cmd/Ctrl+V pastes images copied in other apps and layers copied from another design app (`edit.pasteDesignLayers`); paste over and paste properties; clipboard reads on Linux and the BSDs time out after 2 s. Editor: live font preview on hover; Shift+I and Resources open Assets; Shift-drag moves the selection; thin strokes take a nearby double-click; dropped points merge; Flatten keeps open paths; the Actions palette gets tabs, ranked search, a keyboard highlight and recents; the colour picker opens on a new fill or stroke; text strokes follow their alignment; selection highlights after line breaks; a draggable pages/layers divider; variant properties reorder by drag; submenus keep their hover with a safe triangle; menu actions that need a value ask for it; File ▸ Rename edits the name in place. Engine: bad paint indexes, non-finite geometry and pages in copy/cut are refused; a param naming a layer says so when it isn't one; a CLI script is one undo step and resolves `$step`. Server and collaboration: message limits follow `max_file_mb` (client capped at 1 GiB); a damaged generation field no longer drops flushed edits; the web build joins a session from a `#join=` link. Also Swedish command labels, Nix CI through the shared workflow, paint bounds once per render, and a guard that turns a panic inside the boolean-op library into bad input (the stroke draws unclipped). Numbers unchanged. | | 2026-10-16 | minor | Community fixes from #478 up (batches #591, #598, #602; 69 PRs) and a control-channel fix: a line that isn't JSON now ends a `--control` connection, so a web page can't drive the app through a browser request (#589). Highlights: page copies keep navigation, flows, prototype settings and guides; Find and Replace keeps line endings and case-insensitive matches; text hyperlinks survive import and export; recent colours record only what was picked; MCP screenshots respect `maxSize`; Swedish catalog (UI strings), Czech plurals; bounded canvas overscan while panning; Korean fallback at the run's weight; many vector-edit, guide, page and .fig round-trip fixes. Numbers unchanged | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 0cccf008c..d2b3c9e2a 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-17 · **Change:** minor (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; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; 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) · **Target:** Figma Design UI3 (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (renaming a frame or section from its name on the canvas (#757); 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; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; 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) · **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: @@ -98,7 +98,7 @@ interaction feel not yet compared. |---|---|---| | Pan/zoom: wheel, pinch, Space-drag, Shift+0/1/2, Cmd+0, `+`/`-` | done | Shift-wheel pans sideways, pinch and the zoom tool keep the point under the pointer; zoom steps, limits (2%–25,600%) and frame order pinned where the documentation is silent (`view_behaviour_tests`) | | Rulers and guides (drag out, move, remove; rotated frames; frame guides) | done | guides belong to the page or a top-level frame (a frame moved into another keeps its guides but they stop applying); a click selects one for Delete, the arrows or the context menu's Remove guide; Alt-drag drops a copy; Alt while dragging measures to the layers in the frame it crosses; ruler numbering checked by eye only | -| Frame labels (rotated, right-to-left), size badge | done | | +| Frame labels (rotated, right-to-left), size badge | done | double-click a frame's or section's name to rename it in place: Enter or a click elsewhere keeps the name, Escape restores it, one undo step (#757, `frame_label_tests`) | | Outline mode | done | Outlines, outlines for the selection, mask outlines, frame outlines; shapes are clicked on their outlines, reaching layers behind; View ▸ Outlines ▸ Include hidden layers (`view.outlinesHiddenLayers`) outlines hidden layers too and Include object bounds (`view.outlinesObjectBounds`) draws the box of every layer whose outline isn't its box (groups, text, ellipses, vectors); what either draws can be clicked there (a hidden layer on its outline, a group on its box) and nothing else can; both are view settings like the other outline flags (per file, off in a new one, checked in the menu); the lines are light on a dark page (`outline_options_tests`). The main menu now shows a check mark beside every View toggle that is on | | Slices shown on the canvas (View ▸ Slices) | done | dashed rectangles | | Dragging a layer inside a group keeps it in the group | done | frames and auto layout take other drops | @@ -172,6 +172,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look | Date | Change | Summary | |---|---|---| +| 2026-10-17 | minor | Frame and section names on the canvas rename on a double-click (#757): a field over the label with the name selected; Enter or a click elsewhere commits (`object.rename`, one undo step), Escape cancels. Rotated labels too. Not compared side by side with Figma | | 2026-10-17 | minor | The community batches #733, #735, #736, #729 and #753: submenus open after a short hover, keep their place along a safe triangle and reopen closed (main and context menus); the Actions palette's tabs, ranked search, keyboard highlight and recents; menu items that need a value ask for it, File ▸ Rename edits in place; Shift+I and Resources open Assets; a draggable Pages/Layers divider; Shift-drag moves the selection; a double-click near a thin stroke, merging dropped points and Flatten keeping open paths in vector editing; live font preview and the selection highlight after line breaks; the colour picker on a new fill or stroke; Paste over and Paste properties from the keyboard; Selection colors (partial: no library styles or variables there) and smart-selection spacing fields and canvas handles (partial: no reorder rings), both also checked by their author on macOS; the keyboard shortcuts panel closes on the home screen; Adwaita window controls on Wayland compositors without server decorations (checked by its author on GNOME; KDE not run). Checked in the app on Windows (submenus by their author), not side by side with Figma. Summary and hours unchanged | | 2026-10-15 | minor | Hex fields take CSS colour names (`red` → `FF0000`): fills, strokes, effects, grids, variables, backgrounds and the colour picker | | 2026-10-15 | minor | Typography: drag the line-height, letter-spacing and paragraph-spacing icons to scrub their values; shared numeric-field rates, resize cursor and one Undo step per gesture (#373). |