Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,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. |
Expand Down
64 changes: 64 additions & 0 deletions crates/engine/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3098,6 +3098,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);
Expand Down
7 changes: 6 additions & 1 deletion crates/engine/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,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;
Expand Down
2 changes: 2 additions & 0 deletions docs/ui-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,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 |
| Group fill and outline | formats the members | Shape Fill and Shape Outline reach nested members in one undo step; a transparency-only fill edit retains each member's colour (#1079) |
Expand Down Expand Up @@ -199,6 +200,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 |
Expand Down
Loading