Skip to content

Canvas: drag the frame of a text box being edited to move it - #243

Merged
h-a-n-a-s-h-i merged 2 commits into
storytold:mainfrom
pierrevbc:text-border-move
Oct 14, 2026
Merged

h-a-n-a-s-h-i merged 2 commits into
storytold:mainfrom
pierrevbc:text-border-move

Conversation

@pierrevbc

@pierrevbc pierrevbc commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Once a text box or placeholder is in text editing, it can't be dragged: every press inside its
box, border included, places the caret or starts a text selection. A click on an unselected text
box or placeholder enters editing straight away (like PowerPoint), so moving a text box takes an
extra step: click outside, then drag it before the click turns into editing. In PowerPoint,
grabbing the border of a box being edited moves it, with the move cursor on hover.

Reproduce on main (0.6.0):

  1. deckcraft --sample, click into the title of slide 1: the caret appears.
  2. Press on the title's dashed border and drag: the caret moves or text gets selected; the title
    doesn't move. The cursor over the border is the text cursor.

Cause: Session::down (crates/engine/src/tools.rs) sends any press inside the edited box's
axis-aligned bounds, inflated by the hit tolerance, to the caret. Only the resize, rotate and
adjust handles are checked first. The canvas shows the text cursor over the whole box while
editing (crates/ui-egui/src/canvas.rs).

What changes

Two commits: the change, and the progress docs.

  • Engine. New Hit::Frame { id }: hit_test returns it, after the handles of the selection,
    when the point lies in a band the hit tolerance wide on either side of the edge of the box being
    edited. The test runs in the shape's own frame (Xfrm::affine().inverse()), so it follows
    rotation and flips; in very small boxes the inner half of the band shrinks to a quarter of the
    box so text stays reachable. Lines (connectors with text) and locked shapes have no frame.
  • Press on the frame (Session::down): leaves text editing, selects the box and starts the
    usual Drag::Move (smart guides, snapping, Shift to constrain, Alt to duplicate), committed as
    one undo step ("Move"). A click without a drag only selects the box, with no undo step, as in
    PowerPoint.
  • Inside the box, caret and text selection are unchanged. The zone now follows the rotated
    box instead of its axis-aligned bounds, so a press in the corner area of a rotated box's bounds,
    off the box, no longer moves the caret.
  • Canvas. Hit::Frame shows the move cursor, read from tool.hover like the other cursors;
    no geometry in the UI.
  • Agents see {"hit": {"kind": "frame", "id": …}} from a down event (ui.pointer, MCP
    pointer), since the gesture goes through Session::pointer.
  • Progress docs. docs/ui-parity.md gets a Direct manipulation row (status line, history);
    ROADMAP.md a progress log line. The checklist rows involved (Text boxes, In-place text editing)
    are already D, and no gaps.md entry covered this.

How it was tested

cargo xtask ci passes on this branch (fmt, clippy -D warnings, workspace tests, layers,
assets, wasm32 check).

New tests in crates/engine/src/tests.rs, on decks built in code:

  • dragging_the_frame_of_edited_text_moves_the_box: hover on either side of the edge reports
    Hit::Frame, inside reports the text; press just outside the top edge and drag: the box moves
    by the drag, editing ends, one undo step that restores it; a click on the inner side of the band
    selects the box with no undo step.
  • dragging_inside_edited_text_selects_text: the box doesn't move, the text is selected, no undo
    step.
  • frame_of_rotated_edited_text_follows_the_rotation: 30° box, frame hit on the rotated edge,
    move keeps the rotation; inside still places the caret; a point in the axis-aligned bounds but
    off the box doesn't.
  • resize_handles_beat_the_frame_of_edited_text: the SE handle, inside the band, still resizes.

Checked by hand in the app, on 0.5.0 (deckcraft --sample --control, real egui pointer events through
ui.click/ui.drag, ui.screenshot): the title dragged by its border while editing, move cursor
over the border.

Known limits

  • While editing text, DeckCraft still hides the rotation handle (unchanged); PowerPoint shows it.
  • A shape inside a group: unchanged (the frame belongs to the edited box; the group is not
    selected).

Related PRs

While a text box or placeholder is being edited, every press inside its
box placed the caret, so the box could not be moved without leaving
editing first. Like PowerPoint, a band on either side of the box edge
(the hit tolerance wide, in the shape's rotated frame) is now its frame:
hit_test reports Hit::Frame there, the canvas shows the move cursor, and
a press leaves text editing, selects the box and drags it as one undo
step. A click without a drag just selects the whole box. Inside the box
the caret and text selection work as before; resize and adjust handles
still win. The text-click zone now follows the rotated box instead of
its axis-aligned bounds.
- ui-parity.md: Direct manipulation row added; status line and history.
- ROADMAP.md: progress log line. The checklist rows involved (Text boxes, In-place text editing)
  are already D; no gaps.md entry covered this.
@h-a-n-a-s-h-i
h-a-n-a-s-h-i merged commit 80d3ad5 into storytold:main Oct 14, 2026
@echelon

echelon commented Oct 14, 2026

Copy link
Copy Markdown
Member

Thank you for this contribution, @pierrevbc! It has landed on main and will ship in the next DeckCraft release.

If you haven't already, please come join us on Discord: https://discord.gg/artcraft

h-a-n-a-s-h-i pushed a commit that referenced this pull request Oct 14, 2026
anishguduru pushed a commit to anishguduru/deckcraft that referenced this pull request Oct 16, 2026
…, and refuses them without one (storytold#134) (storytold#243)

The schema documented printing/changes/copy/accessibility and their
defaults unconditionally, but the engine (like Acrobat) only enforces
/P behind an owner password: open_password alone encrypts the document
and leaves everything allowed, even when a caller passed copy: false
or changes: "none" explicitly, and the call reported success.

State the condition in the tool and property descriptions, and turn an
explicit restriction without permissions_password into InvalidArgs that
names the argument and what is needed, instead of writing an
unrestricted file. open_password alone is unchanged.

Co-authored-by: wstock <1394687+wstock@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants