The Format Shape pane's Line page had only colour, width and dash. It now
has PowerPoint's compound type, cap type, join type, begin/end arrow type
and begin/end arrow size drop-downs, and the Size page has Lock aspect
ratio for the width and height fields. shape.line gains compound,
headSize and tailSize, and changing an arrow's kind keeps its size.
Co-authored-by: Cursor <cursoragent@cursor.com>
The Print page always laid notes pages and handouts out on portrait US
Letter. It now offers Letter, A4, Legal and A3 in portrait or landscape,
as PowerPoint's Print settings do, and the preview shows the chosen
page. file.export, app.print and the MCP export tool take the same
`paper` and `orientation` params; unknown values are an error. A notes
page keeps its slide within the top half of the page, so landscape
paper still leaves room for the notes. Full-page slides still print on
slide-shaped pages.
Co-authored-by: Cursor <cursoragent@cursor.com>
The pane's bars now come from deckcraft_anim::Timeline, the same schedule the
slide show plays, so With/After Previous, delays, repeats and auto-reverse line
up on a shared seconds ruler. Dragging a bar sets the delay and dragging its
right edge sets the duration (snapped to 0.05 s), each through
animation.timing, so one undoable command per drag. Bar geometry and drag maths
live in animtimeline.rs as pure, unit-tested functions; kittest drives the
drags. Parity and gaps docs updated.
Co-authored-by: Cursor <cursoragent@cursor.com>
deckcraft_geom::squircle builds a rectangle outline whose corners start
curving earlier along the edge and keep a shorter circular arc, as design
tools' corner smoothing does. shape.smoothCorners turns rectangles and
rounded rectangles into that outline (custom geometry, so it round-trips
through PPTX), and Edit Shape gains a Smooth Corners slider.
Co-authored-by: Cursor <cursoragent@cursor.com>
The font menu lists every family the scan of the system font folders finds,
but on Linux the scan only looked in /usr/share/fonts, /usr/local/share/fonts,
~/.fonts and ~/.local/share/fonts. Fonts under XDG data dirs, in folders
fontconfig is configured with (NixOS, Guix, custom setups) and, inside
Flatpak, the host's fonts never showed.
- system_font_dirs adds $XDG_DATA_HOME/fonts and each $XDG_DATA_DIRS/fonts,
Flatpak's /run/host/{fonts,local-fonts,user-fonts}, and the <dir> entries
of /etc/fonts/fonts.conf, local.conf and conf.d; duplicates are dropped.
- font.add {data} adds a font from its bytes for the session; font.rescan
catalogs the installed fonts again, and the font menu has a
"Rescan Installed Fonts" item for fonts installed while DeckCraft runs.
Co-authored-by: Cursor <cursoragent@cursor.com>
Advancing rendered the new slide on the UI thread at full screen size
before anything changed on screen. While a slide rests (no animation or
transition playing) the show now renders the next slide's first frame on
a background thread; entering that slide uploads the ready image instead
of rendering, waiting for the render if it is still running. Slides with
animated GIFs and the web build keep rendering on entry.
Co-authored-by: Cursor <cursoragent@cursor.com>
Design Ideas showed the current slide once per built-in theme, and picking
one ran design.theme on the whole presentation; with a large picture every
idea looked the same. The pane also re-rendered eight slides every frame.
- New commands design.ideas (query) and design.applyIdea {id, slide?}
(cmd/ideas.rs). A slide with a picture gets full-bleed with the title on a
band, picture left or right with the text beside it, and a framed picture;
a text slide gets a title column, a centred title and a title at the foot.
An idea moves the slide's own shapes, crops the picture to cover its new
box, sends it to the back, and changes only that slide (one undo step).
- The pane shows the layout ideas first, then the themes under a "whole
presentation" heading, and caches the thumbnails until the deck revision,
slide or pane width changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
Pie and doughnut data labels still went through fmt_num, which rounded
anything non-integral to one decimal (999.99 drew as "1000.0"). They now
use Chart::data_label like the other label paths (series format, else
axis format, else General), and fmt_num is gone.
Co-authored-by: Cursor <cursoragent@cursor.com>
Per-axis size caps let AV1 accept 65536x65536 frames (over 8 GiB of luma)
and did not count references or pictures in flight. A shared budget module
in deckcraft-bitstream sizes a picture with checked arithmetic, and the
AV1, VP9, HEVC and H.264 decoders refuse a picture size whose current
picture, reference slots and in-flight pictures would exceed 4 GiB (1 GiB
on 32-bit), before allocating. set_memory_budget lowers it for hosts and tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
Shape ids are local to one presentation, and decoded audio was cached by the
address of a buffer the cache did not keep. Switching decks, closing the last
one, or replacing a clip could keep the previous sound. Playback now stops
when the document changes or closes, a new source replaces the voice's audio,
and the decode and duration caches hold the bytes they are keyed by.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
The film-grain header accepted scaling points whose values didn't strictly
increase, and building the scaling lookup then divided by zero. In threaded
decoding the panic left the frame's reference and picture slots unset, so
the output and dependent frames waited forever.
- The header rejects Y/Cb/Cr point values that don't strictly increase
(spec 6.8.20).
- The scaling lookup moves into scaling_lut, which skips a non-increasing
pair and uses checked indexing (defence in depth).
- run_job decodes inside guarded (catch_unwind), so a panic becomes an error
published to the picture and reference slots.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Charts: every category/value/size cache draws from one chart-wide
budget of 1M decoded slots, so sparse ptCount/idx declarations across
many series can't expand far beyond the XML.
- Chart parts go through the same 96 MiB XML size guard as other XML.
- Package::open charges every inflated byte, including parts it then
discards, against a 4 GiB decompression budget and stops reading
entries once it is spent.
- Legacy OLE previews index each VML drawing once (shape id -> picture
part) instead of reparsing it and its rels for every object.
Co-authored-by: Cursor <cursoragent@cursor.com>
The browser file picker only queued a name and bytes. When the read
finished, DeckCraft guessed the command and ran it on whichever
document and slide were active, so a picture, sound or outline could
land in the wrong place, and a later pick could finish first.
The request now keeps the document, the slide or master, the operation
and a cancellation id. A completion applies there, or is dropped when
that target is gone or the picker was dismissed.
Co-authored-by: Cursor <cursoragent@cursor.com>
decode_at accepted up to 400 million pixels and then called to_rgba8, which
the decoder's allocation limit doesn't cover (a grey image converts to four
times its decoded size), and every picture that failed to decode was kept,
bytes and all, for the life of the process.
- Raster decodes check the RGBA size (512 MiB cap) from the header before
decoding and again before the conversion.
- The failed-source cache keeps at most 256 entries and 64 MiB of encoded
bytes, oldest forgotten first; a larger failed input isn't kept, and a hit
must be the same Arc.
Co-authored-by: Cursor <cursoragent@cursor.com>
Currency and percent signs in right-to-left runs looked ahead through the
whole ET run for every sign, and each neutral span searched both ways for
the nearest strong direction. Both are now precomputed or carried along, so
a long run of signs or many short neutral spans cost linear work. The old
code stays in the tests as a reference and 4000 random paragraphs match it.
Co-authored-by: Cursor <cursoragent@cursor.com>
A shape path gradient tested every raster pixel against every flattened
outline edge, so an outline with tens of thousands of segments cost
billions of tests and stalled rendering. The outline is now simplified
before rasterising so pixels x edges stays under 64M: points closer than
a raster pixel are dropped first, then every k-th point is kept, with
each polygon left closed and at least 8 edges.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
Row and column insert/delete ignored merged cells, leaving covered cells
with no anchor or new cells overlapping an existing span. These edits now
read the merged areas first, shift or resize each one (a row or column
inserted inside a merge widens it, one deleted from it narrows it), move
the anchor and its text to the next covered cell when the anchor row or
column is deleted, and rebuild spans and merge flags from the result.
Co-authored-by: Cursor <cursoragent@cursor.com>
Timeline::with found trigger sequences with Vec::contains and then rescanned
every animation once per sequence, and checked MAX_EFFECTS only after a
match, so a slide with many distinct triggers cost quadratic work before the
effect cap could stop it.
Animations are now grouped once (an index by sequence, first-appearance order
kept, main first), the build stops at MAX_EFFECTS before the next sequence,
and paragraph groups are deduplicated with a set instead of Vec::contains.
Co-authored-by: Cursor <cursoragent@cursor.com>
The loopback control server spawned a thread per connection, read
unbounded lines, queued requests without limit and set no socket
deadlines. It now caps simultaneous clients (16) and request lines
(16 MiB), uses a bounded request queue (64) that answers busy when full,
closes sockets idle for 10 minutes or blocked on a write for 30 seconds,
and frees a connection slot whenever a client thread ends.
Co-authored-by: Cursor <cursoragent@cursor.com>
A cpal stream error was only kept and logged on stop, and the player kept
`streaming` true, so a device that died mid-play froze the voice and video.
AudioOut gains `failure()` (CpalOut reports DeviceNotAvailable), and
Player::tick also treats a stream with no callbacks for a second while a
voice plays as stalled. Either way the stream is stopped, voices continue on
the wall clock, and the next play retries the device; after three failures
the player gives up on the device and plays silently.
Co-authored-by: Cursor <cursoragent@cursor.com>
Ctrl+C and Ctrl+X ran edit.copy / edit.cut and handed the returned text
to egui's clipboard; the ribbon's Copy and Cut, the slide's right-click
menu, the menus, the palette and the control channel ran the same
commands through SlideApp::run, which dropped the text, so it never left
DeckCraft. SlideApp::run now keeps the text of a successful Copy or Cut
and hands it to the clipboard on the next frame, like pending URLs.
Regression test: copy_and_cut_commands_reach_the_system_clipboard runs
both commands and checks the frame's CopyText output (it fails without
the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The row count came from 1/(1.5*rh) - the row spacing alone - and
ignored how far a settled hexagon actually reaches. On a 16:9 slide
the eleven rows stopped at ~75% of the height: from t~0.9, when every
cell has settled, the bottom quarter was plain black until the t=1
endpoint snapped to the full new slide.
The count now derives from the real geometry: rows sit 0.866*rh apart
and the flat bottom zone of a settled hexagon reaches 0.289*rh past
its centre, so counting to the flat-zone reach covers the slide for
every aspect ratio - counting to the vertex reach still left small
V-notches of black above the bottom edge on about a third of them
(US Letter, square, ...). 16:9 gains four rows (15, was 11), 4:3
seven (20, was 13).
Tests: at t=0.99, when every cell is settled and the seams are nearly
closed, a 0.02-step grid over the slide must sit inside settled
incoming cells (padded 1% past the hairline seams) on five aspect
ratios including US Letter and square. Fails on the old count at
(0.2, 0.74); before/after frames rendered with render-transition.
Cmd/Ctrl+L and the on-screen button draw the show's laser colour at the pointer. The pen and the laser are not on together, and a click does not advance the slide while the laser is up.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
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>
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).
INTERSECTCLIPRECT, EXCLUDECLIPRECT and EXTSELECTCLIPRGN limit what later records paint, including a region made of several rectangles. A rotated map stores the bounds of the clip rectangle.
Co-authored-by: Cursor <cursoragent@cursor.com>
Every colour menu ends with Eyedropper. Choosing it closes the menu; the pointer is a
crosshair over the slide with the colour under it and its hex beside it, a click takes
the colour (slide.colorAt) and Esc, a right click or a click off the slide cancels.
The colour comes back through the menu that asked, which opens for a frame and returns
it as if its swatch had been clicked, so Shape Fill, Shape Outline, Font Color, the
Format pane and the other callers all apply it their own way without changes.