From 8bc3d3f11099dbb5304761fbd29a9d288397b11a Mon Sep 17 00:00:00 2001 From: Faabyy <68284158+Faabyy@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:00:39 +0000 Subject: [PATCH] Text boxes: a click on the frame selects the box whole (#787) A click on the frame band of an unselected or selected text box or placeholder started text editing, so the box could not be selected, moved, duplicated or deleted from its frame. The frame band now selects the box whole, like PowerPoint; only a click inside takes the caret. Co-Authored-By: Claude Sonnet 5.5 --- ROADMAP.md | 1 + crates/engine/src/tests.rs | 64 ++++++++++++++++++++++++++++++++++++++ crates/engine/src/tools.rs | 7 ++++- docs/ui-parity.md | 4 ++- 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 933bc7d1..e0c51c47 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -142,6 +142,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-15 | A `.pptx` slide that can't be read is reported, not dropped in silence: `lostSlides` from `file.open`, a message and a repaired copy in the app, a warning from `deckcraft-cli` (#203) | | 2026-10-16 | AutoFormat as you type: ordinals (1st, 22nd, 103rd) get superscript suffixes after a space or punctuation; Tools ▸ AutoCorrect and `text.autocorrect {ordinals}` turn it off | | 2026-10-16 | A PowerPoint Show (`.ppsx`) opened from the command line or the file association starts its slide show, as in PowerPoint; File › Open still opens it for editing (#862) | +| 2026-10-15 | Clicking the frame of a text box or placeholder selects the box whole, so it can be moved, duplicated or deleted; only a click inside puts the caret in the text (#787) | | 2026-10-15 | PDF export and rendering: `anchorCtr` centres the drawn text block — right-aligned text no longer ran past the box edge (#736) | | 2026-10-14 | connected MCP rejects unrelated, malformed, incomplete and oversized replies without replay; buffered unsolicited bytes force a fresh connection before sending (#718) | | 2026-10-14 | Empty bulleted and numbered paragraphs show gray markers while editing, including on first click and after Enter. Markers use the list style and stay out of slide shows and exports. | diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index dd64b08c..3885f2f7 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2908,6 +2908,70 @@ fn dragging_the_frame_of_edited_text_moves_the_box() { assert_eq!(text_box_xfrm(&s, id), x); } +/// Press and release at `at` without moving. +fn click_at(s: &mut Session, at: (f64, f64)) { + let ev = |kind| PointerEvent { kind, x: at.0, y: at.1, mods: Mods::default(), tol: 3.0, pressure: None }; + s.pointer(ev(PointerKind::Down)).unwrap(); + s.pointer(ev(PointerKind::Up)).unwrap(); +} + +#[test] +fn clicking_the_frame_of_a_text_box_selects_it_whole() { + let (mut s, id, x) = editing_text_box(0.0); + let sid = deckcraft_model::ShapeId(id as u32); + let undo_before = s.doc().unwrap().history.undo.len(); + // Leave editing and select nothing: a click on empty slide. + click_at(&mut s, (20.0, 20.0)); + assert!(s.doc().unwrap().selection.shapes.is_empty()); + + // Edges and corner bands, inside and just outside the box, select it but don't start typing. + for (lx, ly) in [(x.w / 4.0, 1.0), (x.w / 4.0, -2.0), (x.w * 0.75, x.h - 1.0), (1.0, x.h / 2.0), (x.w + 2.0, x.h / 2.0)] { + click_at(&mut s, (20.0, 20.0)); + click_at(&mut s, on_box(&x, lx, ly)); + let st = s.doc().unwrap(); + assert_eq!(st.selection.shapes, vec![sid], "({lx}, {ly})"); + assert!(st.selection.text.is_none(), "({lx}, {ly}) entered text"); + } + + // Dragging the frame moves the box; the inside of an unselected box still takes the caret. + click_at(&mut s, (20.0, 20.0)); + let from = on_box(&x, x.w / 4.0, 1.0); + drag_gesture(&mut s, from, (from.0 + 30.0, from.1 + 20.0)); + let moved = text_box_xfrm(&s, id); + assert!((moved.x - (x.x + 30.0)).abs() < 1e-9 && (moved.y - (x.y + 20.0)).abs() < 1e-9, "{moved:?}"); + click_at(&mut s, (20.0, 20.0)); + click_at(&mut s, on_box(&moved, moved.w / 4.0, moved.h / 2.0)); + assert!(s.doc().unwrap().selection.text.is_some()); + + // The frame also deletes: select by frame, press Delete. + click_at(&mut s, (20.0, 20.0)); + click_at(&mut s, on_box(&moved, moved.w / 4.0, 1.0)); + s.execute("edit.delete", &json!({})).unwrap(); + assert!(s.doc().unwrap().shape(sid).is_none()); + assert!(s.doc().unwrap().history.undo.len() > undo_before); +} + +#[test] +fn clicking_the_frame_of_a_selected_plain_shape_still_takes_the_caret() { + let mut s = session(); + s.prefs.snap_to_grid = false; + s.prefs.smart_guides = false; + s.execute("slide.new", &json!({"layout": "blank"})).unwrap(); + let id = + s.execute("shape.insert", &json!({"preset": "rect", "rect": [200.0, 150.0, 240.0, 60.0], "text": "Hello"})).unwrap()["id"].as_u64().unwrap(); + let sid = deckcraft_model::ShapeId(id as u32); + let x = text_box_xfrm(&s, id); + // The first click selects an ordinary shape, the second puts the caret, as before; the frame + // rule is for text boxes and placeholders only. + click_at(&mut s, (20.0, 20.0)); + click_at(&mut s, on_box(&x, x.w / 4.0, x.h / 2.0)); + let st = s.doc().unwrap(); + assert_eq!(st.selection.shapes, vec![sid]); + assert!(st.selection.text.is_none()); + click_at(&mut s, on_box(&x, x.w / 4.0, 1.0)); + assert!(s.doc().unwrap().selection.text.is_some(), "frame click on a selected plain shape lost the caret"); +} + #[test] fn dragging_inside_edited_text_selects_text() { let (mut s, id, x) = editing_text_box(0.0); diff --git a/crates/engine/src/tools.rs b/crates/engine/src/tools.rs index a7190b73..d66370c1 100644 --- a/crates/engine/src/tools.rs +++ b/crates/engine/src/tools.rs @@ -661,7 +661,12 @@ impl Session { // placeholders take the caret straight away like PowerPoint. A click in a selected // table puts the caret in the clicked cell. let table = st.shape(id).is_some_and(|s| matches!(s.kind, ShapeKind::Table(_))); - let enter_text = (!toggle && ((text && (selected || is_text_first(st, id))) || (table && selected))).then_some((id, p)); + // A click on the frame band of a text box selects the box whole, as PowerPoint + // does, so it can be moved, duplicated or deleted; only the inside takes the caret. + let on_frame = text + && is_text_first(st, id) + && st.shape(id).is_some_and(|s| edit_zone(&xfrm_of(&st.doc, &st.selection, s), p, tol) == EditZone::Frame); + let enter_text = (!toggle && !on_frame && ((text && (selected || is_text_first(st, id))) || (table && selected))).then_some((id, p)); let ids2 = ids.clone(); self.select(|_, sel| { sel.shapes = ids2; diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 3b1fedf7..a7075866 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity with PowerPoint -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** major (community PR wave 2026-10-14: 14 new dialogs (41 in all), Eyedropper, Export button, New Window, Grid Options, WordArt gallery and handles, Slide Sorter sections, Quick Access Toolbar, Format Background and Format Shape depth, rich-text clipboard; duplicated rows and headers folded; previous: minor, File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-15 · **Change:** major (community PR wave 2026-10-14: 14 new dialogs (41 in all), Eyedropper, Export button, New Window, Grid Options, WordArt gallery and handles, Slide Sorter sections, Quick Access Toolbar, Format Background and Format Shape depth, rich-text clipboard; duplicated rows and headers folded; previous: minor, File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) How DeckCraft looks and feels next to PowerPoint: the shell, the ribbon, menus, panes, dialogs, direct manipulation, keyboard and precision. Observations of PowerPoint are kept locally in @@ -92,6 +92,7 @@ menu colours taken from the theme tokens (menu `chrome`, page `card`). | Text caret, selection, double/triple click, drag-select | yes | yes | | Exact size/position, lock aspect | yes | yes — Size and Position dialog (height, width, rotation, scale %, lock aspect, position from the top-left), one undo step; ribbon spinners still resize one side at a time | | Drag the frame of a text box being edited to move it | yes | yes | +| Click the frame of a text box or placeholder to select it whole (then move, duplicate or delete it) | yes | yes; a click inside takes the caret | | Table cells: caret and selection in the cell, click into another cell, drag row and column borders | yes | yes; no cell-range selection by dragging across cells | | Context menus on shapes, slides, text, thumbnails | yes | yes, shorter; a right click selects the shape under the pointer first | | WordArt transform | Transform gallery | Shape Format ▸ Text Effects ▸ Transform (`format.textWarp`): No Transform, Follow Path and the other 36 presets. Adjust handles: see WordArt adjust handles | @@ -193,6 +194,7 @@ consistency is open (#73, #56). | 2026-10-12 | minor | Dragging the frame of a text box being edited moves it, as in PowerPoint | | 2026-10-12 | minor | Table cells: caret and selection shown in the edited cell, clicks place the caret in the clicked cell, row and column borders drag to resize | | 2026-10-14 | minor | Format Background pane: solid transparency, picture transparency, tile, offset, scale, alignment, mirror, and Apply to All copying a picture. Not a new dialog; the dialog count stays 27 | +| 2026-10-15 | minor | Clicking the frame of a text box or placeholder selects the box whole instead of starting text entry (#787) | | 2026-10-13 | trivial | Editor slide rasters sit on the physical pixel grid (#184) | | 2026-10-14 | minor | Copy and paste rich text (HTML: bold, italic, underline) with other apps | | 2026-10-13 | minor | Animations ▸ Preview and Animation Pane ▸ Play All play the current slide on the editor canvas |