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 |