Commit Graph
468 Commits
Author SHA1 Message Date
Nikita RakitinandCursor c7a4930c4c UI: open presentations in the background with a progress window (#350)
Opening from the file picker or the Backstage recent list read and parsed
the file on the UI thread, so a large PPTX froze the window. The read now
runs on a thread (engine: read_presentation_file / add_presentation_file),
and a modal shows the file name, elapsed time and Cancel; Escape cancels
too. A cancelled read is dropped when it finishes. Scripted opens (control,
MCP, CLI) and the web build stay synchronous.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:08:47 +03:00
Nikita RakitinandCursor 241ecd59d7 Recovery: unique uids, per-instance ownership, keep snapshots until replaced (#390)
Document uids started at 1 in every process, so two instances sharing
the recovery folder overwrote and deleted each other's entries, and
file.recovery.open deleted each entry as soon as it was loaded, leaving
nothing if the app died before the next checkpoint.

- Uids are a random per-process base plus a counter (below 2^53).
- Each instance holds a lock on owner-<id>.lock in the folder; entries
  record their owner, and recovery open skips a running owner's entries
  and removes stale lock files.
- Opening an entry no longer deletes it: the recovered presentation keeps
  the entry's uid and takes ownership, so the snapshot stays until the
  next recovery save replaces it or a save/close removes it.
- Entry files and metadata are both written via temp file + rename.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:56:21 +03:00
Nikita RakitinandCursor 272ab5f96f Edit: copies remap media, connector ends, animation targets and triggers (#406)
Paste added clashing media under a fresh id but left the pasted shapes on the
old id, reid did not touch connector ends despite its comment, pasted slides
kept animations on the original shape ids, and slide.duplicate remapped
animation targets but not triggers.

- reid returns the old-to-new shape map; connector ends on a copied shape
  follow the copy, ends on a shape outside the copied set are detached.
- remap_animations points targets and triggers at the copies (an animation
  whose target wasn't copied is dropped, an uncopied trigger cleared); used
  by slide paste and slide.duplicate.
- bring_media returns the media ids it had to change and paste rewrites the
  shapes (and a pasted slide's picture background) to use them; slide copy
  now also carries a picture background's media.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:47:27 +03:00
MarekSpodBiedry 007741718e Slide: leading tabs in a new slide's body set the outline level (#370) 2026-10-13 17:52:00 +02:00
MarekSpodBiedry 97855c70a0 File: save through a unique, exclusively created temporary file (#391) 2026-10-13 17:48:28 +02:00
MarekSpodBiedry 33ade63687 Table: split and cell fill stay inside the table (#400) 2026-10-13 17:38:20 +02:00
MarekSpodBiedry 27d73db0d9 Engine: a command that fails after editing puts the presentation back (#405) 2026-10-13 17:38:10 +02:00
MarekSpodBiedry 863f7cbd87 Engine: moving the selection to another text body settles the composition (#421) 2026-10-13 17:37:44 +02:00
NikitaandCursor 525b7ed3f9 Palette: skip commands that need a value
Searching for Export ran file.export with an empty object and reported a missing path. The palette now offers only commands that accept that call, so Export opens the dialog.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 18:31:02 +03:00
NikitaandCursor 82dcd86c88 Pptx: read and write embedded fonts
A deck that embeds a TrueType or OpenType face, including one wrapped as Embedded OpenType, now draws with that face and saves it back. MicroType Express subsets stay in the file and still draw in a stand-in.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 18:22:48 +03:00
Sazid Al Bayazid e70fce996b Model: an exhausted id space refuses growth instead of duplicating ids (#407)
alloc_id bumped next_id with saturating_add: a presentation whose
next_id reached u32::MAX (a hostile file can arrive that way;
fix_next_id imports it) handed out that same id to every new slide,
shape, layout and media - duplicate ids, ambiguous references, silent
corruption on every later insert.

alloc_id now returns Option<u32> via checked_add, and every caller
refuses: the engine's insert/duplicate/media/layout commands answer
with a clear error naming the command, paste and duplicate re-id
helpers propagate it, media addition is fallible, and a fresh slide is
Option. The pptx export's synthesized blank layout is guarded the same
way. Loading keeps such a deck viewable: repair re-ids duplicate
slides while it can, and a duplicate it cannot re-id stays as it is -
growth is refused, and only that duplicate keeps its ambiguous id.

Renumbering the loaded graph was rejected: it needs the
cross-document reference remapping that issue #406 tracks.

Tests: model exhaustion (alloc_id None, fresh media refused, a new
slide refused), engine (slide.new, shape.insert, slide.duplicate and
edit.duplicate all refuse; the document holds no duplicate ids),
format repair (an unre-id-able duplicate stays visible).
2026-10-13 21:13:25 +06:00
NetterSilver 35312529e1 slide.colorAt: the colour the slide is drawn with at a point (#367)
A read-only command, {slide?, x, y (slide units), scale?}: the slide is rendered and
the pixel at the point is returned as {color: "RRGGBB", alpha, x, y}. This is what the
Eyedropper takes, and what scripts and MCP use to read a colour off a slide. A point
outside the slide, a missing or non-numeric coordinate or an unknown slide is an error.
2026-10-13 16:41:57 +02:00
Brandon ThomasandClaude Opus 5.5 0c5ddb5dea Gradients: read, write and draw a:tileRect
Gradient tile rectangles were dropped on import. PowerPoint's "from
corner" radial presets depend on them: fillToRect puts the focus on a
corner and tileRect stretches the gradient's box a whole size past it, so
the circle is centred on that corner and reaches the far one. Without it
(after the two-point radial change) such fills washed out. A linear
gradient now spans its tile rectangle; a radial one ends on the circle
around it, with the focus still placed in the shape's own box (fitted to
PowerPoint's export of the corpus deck tdf114848: mean diff over the
filled box 0.77). Round-trips through pptx.

Wild synthetic set: tile-rect gradients 16.74 -> 2.48.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 08:16:41 -04:00
NikitaandCursor 503ee5d67e A tabbed slide body is an outline, and --help exits (#360)
slide.new kept a leading tab as text, while text.set already turned it into a bullet level. The desktop --help flag was opened as a missing file.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 12:38:57 +03:00
pierrevbc 44a032d3de Tables: editing tests for tables with a file-defined style
A table whose style the file defines (its parts and background drawn
since #292 and #294) edits like the others: the caret is laid out with
the file style's lettering (a regular header where the built-in style
bolds it), a double-click selects the word drawn there, a click moves
the caret to the clicked cell and a border drag resizes the columns
while the cell stays edited.
2026-10-13 11:30:13 +02:00
pierrevbc d63d250d58 Tables: Tab and Shift+Tab skip cells hidden under a merge
Tab moved into the cells covered by a merged cell, editing text that is
never drawn. It now goes to the next visible cell; Tab in the last
visible cell still adds a row, and Shift+Tab in the first cell stays.
2026-10-13 11:30:13 +02:00
pierrevbc 456c5c5373 Tables: drag row and column borders to resize them
Hovering a table border shows a resize cursor (new hit kinds
TableColumn and TableRow) and dragging it resizes, one undo step per
drag, as in PowerPoint:

- an inner column border moves between its two columns and the table
  keeps its width;
- the right edge widens or narrows the last column and the table;
- a row's bottom border (the last one is the table's bottom edge) sets
  that row's height, which stays a minimum: rows still grow to fit
  their text.

Columns stop at 12 pt. Borders hidden inside merged cells aren't hit;
the left and top edges still move the table. A border drag while a
cell is edited keeps editing it. The drag goes through
table.columnWidth, which takes a new keepTotal option (the next column
absorbs the change), and table.rowHeight. Hit testing is done in the
table's own frame, so rotated tables resize along their axes.
2026-10-13 11:30:13 +02:00
pierrevbc 70314fa828 Tables: clicks place the caret in the clicked cell
- A double-click in a cell puts the caret where clicked and selects the
  word there, as in shapes (it used to put the caret at the end).
- While a cell is edited, a click in the same table moves the caret to
  the clicked cell and position; a drag selects text within the cell
  and Shift+click extends only within it.
- A click in an already selected table enters the clicked cell at the
  click; dragging the table still moves it.
- The release that follows a double-click no longer re-enters the text
  at the click, which collapsed the word just selected (shapes too).

Cells are found from the drawn cell rects, so merged cells and rows
that grew to fit their text are hit where they are drawn.
2026-10-13 11:30:13 +02:00
pierrevbc b5a8642417 Tables: show the caret and text selection in the cell being edited
Editing a table cell outlined the cell but drew no caret or selection,
and Up/Down/Home/End fell back to paragraph moves, because the text
layout used for drawing, hit testing and caret movement only existed
for shapes.

text::text_frame now lays out the edited text as it is drawn and gives
the transform to the slide, for shapes and cells alike: a cell's text is
laid out in its rect (merges honoured) with the cell's margins, anchor
and the table style's look, through the renderer's own cell_body and
cell_rects. layout_for and tools::text_pos use it, and the canvas draws
the caret and selection from it. The cell outline follows merged cells.
2026-10-13 11:28:22 +02:00
pierrevbc ab81f6bc73 Canvas: right click selects the shape under the pointer
A right click on an unselected shape opened the slide's context menu
(Layout, Reset Slide...) because only the primary button reached the
pointer tool; the shape had to be left-clicked first. The right-button
press now goes through Session::pointer as PointerKind::ContextClick
(`contextclick` on the control channel and MCP) and selects like
PowerPoint before the menu opens: an unselected shape is selected alone
(a text box or placeholder is not put into text editing, nothing is
dragged), a selected shape keeps the selection, multiple selection
included, and empty slide clears it. In the box whose text is being
edited, the caret and text selection stay, and the context menu offers
text items (Paragraph, Bullets, Numbering, Link) next to Cut, Copy and
Paste, which act on the selected text.
2026-10-13 11:26:20 +02:00
pierrevbc 9d185aeb68 Canvas: drag the frame of a text box being edited to move it
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.
2026-10-13 11:26:11 +02:00
lloyd 720b66fbd3 Merge remote-tracking branch 'upstream/main' into fix-211-markdown-outlines
# Conflicts:
#	crates/engine/src/tests.rs
2026-10-13 08:58:03 +02:00
Yevhenii Hurin 922445300b describe keeps a remark with its option and allows a bare call
animation.remove's remark belongs to its `ids` option, so describe shows
it there instead of as a note on the whole command. A command with an
all-optional set of parameters is suggested as a bare id.

[claude-opus-5-5] on behalf of hoblin
2026-10-13 09:55:19 +03:00
Marco Arnulfo 85345aaa13 Open: a password-protected presentation says so, and open errors show on the start screen (part of #346) 2026-10-13 06:44:47 +02:00
Marco Arnulfo e6a41d1ada Review: the Language dialog marks the selected text with the language you pick (fixes #339) 2026-10-13 06:04:28 +02:00
Brandon ThomasandClaude Opus 5.5 1660982e58 File tab: PowerPoint's File view (Home, New, Open, Info, Save As, Print, Export, Close) (#76) (#344)
* Engine: file.info and PowerPoint-style print slide ranges (#76)

file.info reports what File › Info shows (name, path, format, unsaved
changes, slide and hidden-slide counts, sections, size on disk,
properties). file.export's PDF `slides` also takes the range typed in
PowerPoint's Print page ("1-3, 5", 1-based); bad or out-of-range text is
an error instead of a silent full export.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* UI: the File tab (Backstage) like PowerPoint's (#76)

File (first ribbon tab) or Alt+F opens a full-window view in place of the
ribbon, slides and status bar: a side menu (Back, Home, New, Open, Info,
Save, Save As, Print, Export, Close; Account and Options at the bottom)
and the chosen page. Esc or Back returns; Up/Down walk the side menu and
show each page as it gets the focus; Enter runs Save/Close/Options.
Laid out after PowerPoint for Windows as observed: 160-pt menu a step
darker than the page, outline icons, current page in the accent colour.

The start screen is now File › Home (as in PowerPoint), with only Home,
New and Open at the top and no Back. Pages call existing commands and
file helpers: file.new {theme} and the sample deck (Home, New); recent
files with search, Browse and Recover Unsaved (Open); file.info and
file.properties, Accessibility Checker, Revert (Info); file.save /
file.saveAs / file.export by type (Save, Save As); a PDF with the Print
page's slides, layout (full page, notes, handouts 1-9), hidden slides and
frames, opened in the system viewer or downloaded on the web, with a
page preview (Print, Ctrl+P); Create PDF, Create Handouts and Change File
Type (Export); appearance and About (Account); Preferences (Options).
Share (cloud) is left out.

Close and Ctrl+W now ask Save / Don't Save / Cancel about unsaved
changes instead of closing at once. The Export dialog shares
SlideApp::export_as with the Export page. New UI commands: app.backstage
{page}, app.closePresentation, app.print; ui.inspect reports the page.
New icons drawn in code: back, save as, account, recent, new file, open
folder.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Docs: File tab (Backstage) in ui-parity, gaps, checklist and progress log (#76)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 23:56:02 -04:00
Brandon ThomasandClaude Opus 5.5 6334d689d2 Text: Ctrl+arrows, Ctrl+Backspace/Delete and other text keys work like PowerPoint (#268) (#333)
* Text: Ctrl+arrows, Ctrl+Backspace/Delete and the other text keys work like PowerPoint (#268)

Observed black-box in PowerPoint for Windows on synthetic text:
- Word stops (crates/model/src/words.rs): words, punctuation runs, symbol
  runs, tabs, line breaks and each CJK character are tokens; trailing spaces
  join the token before. it's, a-b, 3.14, 1,000 are one word; e.g., a_b and
  x@y.com are not. Grapheme clusters never split.
- Ctrl+Left/Right and Ctrl+Shift+Left/Right move by those stops, across
  paragraphs; with a selection they go on from its start/end. In a
  right-to-left paragraph Left and Right swap.
- Ctrl+Backspace deletes to the previous stop; Ctrl+Delete deletes the rest
  of the word, or spaces plus the next word, then applies PowerPoint's smart
  spacing. With a selection both work from its start.
- Ctrl+Up/Down by paragraph, Ctrl+Home/End to the text's ends.
- End on a wrapped line goes past the trailing space and the caret stays
  drawn at that line's end (TextSel::line_end); End again stays, Home
  returns to the same line.
- Ctrl+]/[ and Ctrl+Shift+>/< change font size (egui's Period/Comma/
  bracket key names now match registry shortcuts).
- Shift+F3 cycles lower -> Title -> UPPER; a caret changes its word.
- Double-click selects the word without trailing spaces; triple-click
  includes the paragraph mark.
- Mac keeps Option for words and Cmd for line/text ends (Session::mac_keys).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* rustfmt

* Canvas: one caret, drawn at a wrapped line's end after End

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 22:09:50 -04:00
Brandon ThomasandClaude Opus 5.5 9d571ef644 Selection Pane: reorder by drag and drop, groups, Show/Hide All and Lock, as in PowerPoint (#133) (#332)
* #133: Selection pane engine: restack inside groups, Show/Hide All, Lock like PowerPoint

- arrange.bringForward/sendBackward/bringToFront/sendToBack and arrange.reorder work on a
  group's children, which stay in their group (as PowerPoint's pane arrows and drag do);
  steps that change nothing leave no undo step.
- selection.select (pane click: Ctrl toggles, children and top-level objects don't mix,
  picking a hidden object shows it), selection.showAll (everything, children too) and
  selection.hideAll (top level).
- shape.lock cascades through groups; locked objects can be selected, formatted and typed
  in, but not moved, sized, rotated, flipped, reshaped or cropped (commands and canvas).
- PPTX: Lock reads and writes PowerPoint's full locking attribute set per element kind
  (observed by saving locked objects from PowerPoint); partial locks aren't Lock.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Front-most first with groups folding; drag a row to restack among its siblings (accent
insertion line); Bring Forward / Send Backward arrows; Show All / Hide All; per-row eye and
padlock; double-click or F2 renames in place; Ctrl+click toggles, Shift+click selects a run;
arrows, Home, End and Esc in the list; right-click menu. Lock/Unlock on the canvas menu,
greyed handles on locked objects. egui_kittest tests drive the pane.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 22:05:44 -04:00
Brandon ThomasandClaude Opus 5.5 ea3b1159d5 Korean, Japanese and Chinese: input-method composition and interface text (#101) (#331)
* Text: input method composition as commands, one undo step per composition (#101)

text.compose shows an input method's preedit in place at the insertion point and replaces it on
each update; an empty preedit cancels and restores the text and selection. text.commit replaces
the composition with the result as one undo step. Any other edit, key or click settles an open
composition first. The composition is underlined under the baseline (thicker under the part being
converted), and the caret line is measured for empty placeholders too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* UI: Korean, Japanese and Chinese input and interface text (#101)

The slide turns the input method on while it takes typing, with the candidate window at the
caret, and forwards egui Ime events to text.compose / text.commit, so a Korean keyboard composes
syllables instead of separate jamo. Interface text in scripts the interface fonts lack (Hangul,
Simplified Chinese, Thai...) borrows faces from the font database (installed fonts such as Malgun
Gothic; nothing bundled): file names, titles, notes, outline, sections, comments and typed text.
Ported and widened from WordCraft's #565/#840 fallback. The notes pane keeps text typed before
focus leaves in the same frame (Escape, another slide). Control channel: ui.ime.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 22:03:42 -04:00
Brandon ThomasandClaude Opus 5.5 f94f3c12d1 Canvas: objects off the slide show and can be grabbed around it, as in PowerPoint (#270) (#329)
PowerPoint's Normal view draws shapes, pictures and text that sit partly
or wholly off the slide on the area around it, at full strength, with
their effects, master and layout graphics included; the slide show,
exports and thumbnails clip to the slide. DeckCraft clipped everything to
the slide in the editor too.

- render: `slide_extent`/`layout_extent` estimate how far a slide's
  drawing reaches (boxes, outlines, shadow, glow, reflection);
  `render_slide_drawn` also reports what a render actually drew (text
  spilling out of its box, table rows that grew); `pasteboard_region`
  picks the pixels the editor renders: the slide plus what reaches off
  it and is in view, rounded to 256 px tiles so scrolling rarely
  re-renders. A slide with nothing off it renders exactly as before.
- ui-egui: the canvas texture covers that region (slide and master
  views); it re-renders only when the slide, zoom or the region changes,
  and a second time only if text drew beyond the estimate.
- Hit testing, selection, marquee and moves already worked in slide
  space; tests now cover them off the slide.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 21:47:05 -04:00
Brandon ThomasandClaude Opus 5.5 85366b1a62 Fonts: Office's downloaded fonts are used; opening a file says which fonts are missing (#326)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 20:34:54 -04:00
xc4pr b97af97a58 Text: the caret shows on dark slides and in empty placeholders
The caret was always stroked black. On a dark slide (the Nocturne theme,
or any dark background with light text) it could not be seen. It is now
drawn in the colour that text typed at the caret takes, and falls back to
the theme token when that text has no fill.

An empty placeholder showed no caret at all: its body holds no paragraph
until something is typed, so the edit layout had no line for the caret to
stand on. The edit layout now treats such a body as one empty paragraph,
and the canvas leaves the placeholder's prompt out while it is being typed
into, so the caret does not sit inside "Click to add title".

Fixes #318.
2026-10-13 00:39:17 +02:00
Brandon ThomasandClaude Opus 5.5 4d13652f0f Pictures: duotone recolours, and layout/slide colour map overrides apply (#269)
Theme background pictures (Office 2007-era themes such as Treck) carry an
a:duotone recolour in phClr; DeckCraft read it but drew the picture
unchanged, so those backgrounds came out grey. The painter now resolves the
duotone's colours against the slide's colours and placeholder colour, and
the decoded-image cache applies them (luminance picks a colour between dark
and light) and keys on them.

Layouts and slides can replace the master's colour map
(p:clrMapOvr/a:overrideClrMapping, e.g. dark section layouts mapping bg1 to
dk1). They were read as the master's map; now Layout and Slide carry an
optional colour map, resolve::Ctx uses slide > layout > master, and the
writer keeps them.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 17:35:52 -04:00
Yevhenii Hurin 66f7e6e1ed Self-review: no ID[=JSON] left, params grammar documented
A --cmd without a value now asks for COMMAND[=PARAMS]. The params field
says how describe reads it, table.split's row and col are optional as
the command reads them, and the MCP instructions name only shape
commands for `ids`.

[claude-opus-5-5] on behalf of hoblin
2026-10-13 00:15:53 +03:00
Yevhenii Hurin 717e855cec deckcraft-cli help explains the CLI to an agent
`deckcraft-cli help` and each subcommand's `--help` say what it does,
with a working example; `--help` never runs the subcommand. `describe`
spells out a command's parameters and what it acts on, `commands` lists
one command per line, and the MCP instructions say what format.* acts on.

[claude-opus-5-5] on behalf of hoblin
2026-10-12 23:41:42 +03:00
FabPei 28f51bf8ad fix: DeckCraft upstream #104
Make text in grouped shapes editable: double-click reaches the
text-holding group member instead of being a no-op.
2026-10-12 22:12:42 +02:00
Isaac Levine dc6ea04a8f Engine: check automation read and write roots on every path a command reads or writes 2026-10-12 09:58:07 -04:00
Магда М.Г. f2c27cbba1 Text: draw shrink-on-overflow at the scale the file was saved with
PowerPoint draws normAutofit text at its saved fontScale/lnSpcReduction;
we ignored them and only shrank when our own layout overflowed, so saved
decks showed larger text and wider line spacing than PowerPoint, with text
running past placeholders. The layout now starts from the saved scale
(nonsense values ignored) and still shrinks further when the text doesn't
fit. Editing the text resets the saved scale so the new text is fitted
afresh, as PowerPoint refits on edit.
2026-10-12 14:52:34 +02:00
lloydandClaude Opus 5.5 9c64754f20 Read Markdown outlines as Markdown (#211)
File ▸ Open's Outline filter accepts `.md`, but a `.md` was read with the
plain outline format (unindented line = slide, indent = bullet), so a
Markdown outline written the usual way kept `#` in its titles and made
every top-level list item a slide of its own.

`deckcraft_format::markdown_to_slides` reads ATX headings (`#`–`######`,
closing `#`s dropped) as slide titles, list items (`-`, `*`, `+`, `1.`,
`1)`) as bullets nested by their indentation, and other lines under a
heading as body paragraphs. A `.md` without headings is still read as a
plain outline, so existing indented `.md` outlines open as before; `.txt`
is unchanged. Opening a `.md` and dropping one on the window
(`slide.fromOutline {markdown: true}`) use it. Both readers now share the
slide builder.

Tests: the outline from the issue (2 slides, nested bullets), numbered and
`*`/`+` lists with text under a heading, `slide.fromOutline` with
`markdown`, plain `.txt`/`.md` outlines unchanged, and heading / list-item
edge cases (not-a-heading forms, closing hashes, Unicode, long digit runs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:22:47 +02:00
justinjohn0306andClaude Opus 5.5 21249df5da PPTX: .potx and .ppsx declare a template and slide show main part (#210)
Saving with a .potx or .ppsx name wrote a byte-identical .pptx: the
main part was always presentationml.presentation.main+xml, because
format_for_path folded all three extensions into "pptx". It now keeps
potx/ppsx, and deckcraft_pptx::export_as picks the template or
slideshow content type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:21:58 +05:30
justinjohn0306andClaude Opus 5.5 1601fd250a Slide Size: choose Portrait or Landscape (#198)
The Slide Size dialog gets an Orientation choice that swaps width and
height, and design.slideSize takes orientation: portrait|landscape so a
preset can be made portrait from the CLI and MCP too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:39:40 +05:30
Isaac Levine 96abeba71b Slides: edit.selectAll takes scope slides to select every slide (#188)
The current slide stays put and any shape or text selection is cleared. A scope other than slides is an error; without a scope the command still selects shapes.
2026-10-11 22:53:29 -04:00
Matúš LacoandClaude Opus 5.5 c871c5f634 Export: animated GIF of the deck (Export… dialog, CLI, MCP) (#171)
There was no way to turn a presentation into an animated GIF. file.export now writes one: format
`gif` (or a .gif path), one frame per slide drawn by the slide renderer and shown for
secondsPerSlide, encoded with the `image` crate's GIF encoder (already a dependency, no new
crates) with a 256-colour palette per frame: exact when a slide has 256 colours or fewer,
NeuQuant otherwise. Slides are composited onto white.

Options: width (small 640, medium 960 (default), large 1280, extraLarge 1920, or 16-3840 px; the
height follows the slide's aspect ratio, no side over 3840), secondsPerSlide (0.1-600, default 5,
written in GIF hundredths), loop (forever by default, or once: no NETSCAPE2.0 block), slides (a
list of indices, in play order) and includeHidden (hidden slides are skipped by default).
Out-of-range or wrongly typed values, NaN and infinities are BadParams errors, never panics; at
most 500 frames and 200 million pixels in total, checked before anything is rendered.

- Engine: crates/engine/src/gif_export.rs (options, frame list, size, encoder);
  cmd::file::gif_bytes, save_bytes("gif"), format_for_path(".gif").
- UI: the Export… dialog (Command palette; the app has no File menu yet) gets "Animated GIF (.gif)" with Size, Seconds per slide, From/To slide,
  Loop forever and Include hidden slides; an invalid range or value keeps the dialog open with a
  message. The web build downloads the bytes (image/gif).
- CLI: convert IN OUT.gif, run --export OUT.gif --export-params JSON; usage lists the format and
  options.
- MCP: the export tool lists gif and width/secondsPerSlide/loop (slides and includeHidden now
  apply to PDF and GIF).
- Docs: file-format parity GIF row, gaps (video export remains), parity checklist row M -> P
  (P2 breadth 36% -> 37%, overall 79%), control protocol, MCP, README, ROADMAP progress log.

Transitions and build animations are not animated in the GIF (tracked with video export).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:00:33 +02:00
Hevar Hemin f033c76913 Bidi: auto-detect RTL paragraph direction and keep typing caret advancing smoothly
When typing in Kurdish, Arabic or Hebrew in a new document or text shape, default
paragraph direction (props.rtl: None) was resolving to LTR. In an LTR
paragraph, visual reordering pinned newly typed characters and the caret to the
left margin, resetting to the start of the line with every keystroke and jumping on
space characters due to UAX #9 rule L1.

- Add first_strong_is_rtl() in deckcraft-model following UAX #9 rules P2/P3.
- Auto-resolve unassigned paragraph direction to RTL in shape_para layout when the
  paragraph starts with an RTL script, defaulting alignment to Align::Right.
- Auto-promote paragraph to RTL (props.rtl = Some(true), props.align = Some(Align::Right))
  when typing or inserting text starting with an RTL character.
- Add regression tests in deckcraft-model, deckcraft-text, and deckcraft-engine.
2026-10-11 22:28:45 +03:00
Matúš LacoandClaude Opus 5.5 1ce5f686e8 Notes: speaker notes show their formatting and keep it when edited
Notes from a .pptx were read and written with their formatting, but the notes pane and Notes
Page drew them as plain text (Slide::notes_text in a plain TextEdit), and any edit there saved
the text back through slide.notes as TextBody::from_text, dropping every bold run, level,
indent and bullet of the slide's notes.

- ui-egui: a notes editor (notes.rs) shared by the notes pane and Notes Page. It draws bold,
  italic, underline, strikethrough and size from the runs, and each paragraph's margin, hanging
  indent and bullet or number (Wingdings/Symbol bullets mapped as on slides); paragraphs are
  laid out one by one so wrapped lines align with the text, not the bullet. Each edit is applied
  to a formatted draft, so typed text takes the formatting where it lands (Enter in a bulleted
  point starts another), and the draft is saved once the field loses focus or the slide changes.
- engine: slide.notes takes `body` (formatted notes, as slide.inspect's new `notesBody`), and
  `text` now changes only what was retyped (model edit::replace_text); text sharing nothing
  with the old notes is still plain.
- text: map_symbol_bullet is public for the notes editor.

PDF notes pages still print the notes as plain text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:21:33 +02:00
Mikkel Georgsen 736dd52b51 Commands: an invalid shape id is an error instead of the selection
A shape id that was present but not a valid id (negative, not an integer,
larger than u32) was treated as absent, so commands such as text.set and
text.edit silently edited the selected shape instead. id_param now tells three
cases apart: absent (the selection fallback is unchanged), valid, and invalid
(an error naming the command). arrange, edit (select), inspect, review, table,
text and the animation painter pick this up.
2026-10-10 22:05:22 +02:00
Mikkel Georgsen d34b5df775 Slides: going to a slide that does not exist is an error
slide.go clamped an out-of-range index to the last slide and reported
success, so a request for slide 99 of a short deck landed on the last slide
without a word. It now returns an error and stays on the current slide. A slide
id beyond u32 is reported as an unknown slide instead of being read as id 0.
2026-10-10 22:05:13 +02:00
Mikkel Georgsen 20366a8ea9 Export: an unknown format is an error instead of a deckcraft file
file.export with an explicit format outside png, jpeg, jpg, pdf, pptx, potx,
ppsx, deckcraft, outline and txt fell through to the native deckcraft zip, so a
request for "docx" reported success and wrote a deckcraft archive under that
name. It now returns an error and writes nothing. A format inferred from the
file extension behaves as before.
2026-10-10 22:04:58 +02:00
MarekSpodBiedry d2acdf07db Comments: browser-created comments and replies get a creation timestamp (#72) 2026-10-10 19:01:56 +02:00
d556ff98b5 Fields: date fields show today's date in their own format (#36)
Every datetime field showed the deck-wide fixed date text instead of a date: empty when no slide
had a fixed date, and that text on every slide when one did. A fixed date is plain text in its
slide's placeholder; a field is an automatic date, so it now shows the local date and time in the
format of its type (ECMA-376 datetime1 to datetime13 as PowerPoint lists them for English,
datetime and datetimeFigureOut the short date). Numeric dates follow the run's language
(en-GB 09/10/2026, de-DE 09.10.2026...); formats with names stay English-only, so other languages
keep the field's saved text, as do unknown types and builds without a clock (wasm). The saved
text is kept on save.

- text: datetime module (field types, formats, local clock via chrono, not on wasm);
  Fields::field_in passes the run's language.
- render: RenderOpts::now injects the clock (tests, deterministic output); the PDF text layer
  uses the same fields.
- engine: Header & Footer turns existing date placeholders into fixed text or back into a
  field; new date fields are saved with the date they show (local, not UTC).

Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 06:25:32 -04:00