Commit Graph
421 Commits
Author SHA1 Message Date
Nikita RakitinandCursor f0be1aa767 Edit: Paste Special (destination theme, keep source, picture, text only)
Adds edit.pasteSpecial. Keep Source Formatting bakes the copied shapes'
theme colours and theme fonts into fixed values (captured at copy time),
Picture draws the copied shapes into one PNG picture, and Keep Text Only
pastes plain text. Ctrl+Alt+V, Edit › Paste Special… and the Paste menu
open a Paste Special dialog.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 23:55:45 +03:00
Nikita Rakitin f9d5a43174 Round-trip comments, links, shows, groups and inherited placeholders
An owned corpus of those element kinds survives open, save and reopen.
Paragraph default run properties, including a link, and action sounds
were dropped on save, so import and export now keep them.
2026-10-13 23:43:03 +03:00
Nikita RakitinandCursor 0e763ab78c Text: Bullets and Numbering dialog
The Bullets and Numbering menus only offered a fixed list of styles.
"Bullets and Numbering…" at the foot of both menus opens PowerPoint's
dialog: Bulleted and Numbered tabs with their styles, a custom bullet
character, Start at, Size (% of text, 25-400) and Color (automatic,
text or an accent), and None.

format.numbering now takes color and size like format.bullets, and
both clamp the size to PowerPoint's 25-400 %.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 23:39:22 +03:00
Nikita RakitinandCursor d4175d2902 Animations: add 34 PowerPoint motion path presets
Adds Basic (stars, heart, crescent moon, teardrop, ...), Lines & Curves
(zigzag, waves, S curves, spirals, bounces, ...) and Special (figure 8,
bean, peanut, neutron, ...) motion paths with PowerPoint preset ids, so
they appear in the Motion Paths gallery, play back, and round-trip
through PPTX.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 23:37:00 +03:00
Nikita RakitinandCursor bc42b62cc4 Pictures: Remove Background with a classical matte
The Picture Format > Remove Background button was a disabled
placeholder. picture.removeBackground now flood-fills from every edge
pixel through similar colours: each step must be close to the pixel
before it (so gradients and soft shadows follow) and to the edge colour
it started from (so it can't leak into the subject). That region turns
transparent with a one-pixel soft edge, and the picture points at a new
PNG; undo brings the original back. Tolerance 0-100 (default 20); the
ribbon offers normal, looser and tighter. Pictures whose edges are all
subject are left alone, and pictures over 16 megapixels are refused.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 23:35:25 +03:00
Nikita RakitinandCursor c22747b104 Morph: the same text moves and resizes instead of cross-fading
A title typed as "TEST" on a title slide and on a content slide pairs
the two titles, but they differ in placeholder, size and alignment, so
Morph with the default Objects option cross-faded them: the old title
rose and faded out while the new one faded in.

Paired shapes whose text is the same now use the word morph (shared
words travel and resize from their old place to their new one) whatever
the Morph option, as PowerPoint does. Different texts still cross-fade
with Objects.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 23:22:19 +03:00
Nikita RakitinandCursor 3489c3b22d Format: report damaged embedded media and open such files as a repaired copy
load() ignored every failed media read, so a .deckcraft with a missing,
oversized or bad-CRC media member opened silently with empty pictures,
and the next Save overwrote the original with that loss.

load_checked() now lists each unreadable member (linked media saved
without bytes is not damage). file.open/openBytes return damagedMedia,
show a message, and open the deck as an untitled "(Repaired)" copy so
Save asks for a new name and the original file stays intact.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 22:55:52 +03:00
Nikita RakitinandCursor 6a85afa236 Pptx: round-trip an owned corpus and keep notes-master text
Open, save and reopen decks we author, and compare package parts and
text. The check showed a notes master's header and footer were replaced
by an empty notes master on save.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 22:34:29 +03:00
Nikita RakitinandCursor 69a4424e75 Transitions: built-in sounds, Stop Previous Sound and Loop Until Next
Follow-up to the transition sound picker. The Sound box now lists
[Stop Previous Sound] and six built-in sounds (Chime, Click, Camera,
Whoosh, Drum Roll, Laser) that are synthesised as small WAVs when
chosen, so no sound files ship with the app; the same built-in sound is
embedded once however many slides use it. Loop Until Next Sound repeats
a sound in the show until another slide's sound or Stop Previous Sound.
PPTX reads and writes p:stSnd loop="1" and p:endSnd, keeps the sound's
file name in p:snd/@name, and setting a slide's transition to None no
longer drops its sound.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 22:30:19 +03:00
Nikita RakitinandCursor 4ea79d4c5e Shapes: Edit Points — point types, Bézier handles, right-click menu
shape.pointType makes a vertex smooth (handles in line and equal),
straight (in line) or corner (independent), turning its segments into
curves so it has handles. shape.moveHandle drags a control handle; the
other one follows as the point's type says (given, or read from the
handles). shape.points also reports each vertex's type.

Canvas: clicking a vertex shows its handles, which drag (one undo step
per drag) and follow the type chosen for it. Right-click a vertex for
Smooth / Straight / Corner Point and Delete Point, or anywhere for Exit
Edit Points; a shape's context menu gets Edit Points. The selection box
is hidden while points are edited.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 22:29:57 +03:00
Nikita RakitinandCursor 733522d528 Shapes: Edit Points — drag, add and delete a shape's vertices
New cmd::points with shape.editPoints (enter/leave; a preset becomes a
freeform in shape-local points), shape.points (vertices in slide points),
shape.movePoint, shape.addPoint (splits a line, quadratic or cubic
segment) and shape.deletePoint. After each change the box is fitted
around the outline and kept in place on the slide, rotation and flips
included. Closing edges are made explicit so they take points too.

On the canvas the edited shape shows its vertices as handles: drag one
(one undo step per drag), Cmd/Ctrl-click a vertex to delete it or the
outline to add one; Escape, Enter or a click elsewhere leaves Edit
Points. Shape Format ▸ Edit Shape gets an Edit Points item.

Not yet: smooth/straight/corner point types and Bézier control handles.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 22:22:57 +03:00
Nikita RakitinandCursor e9315bc141 UI: seeded whole-window soak with memory and timing bounds
ui_soak_tests.rs drives the app through the egui test harness with seeded
random clicks, drags, keys (plain, Ctrl, Shift), typing and scrolling over the
sample deck. Every ten actions it checks that every document validates, the
selection is in range, undo, journal, thumbnail and texture caches stay
bounded, and no command or render panic was caught; at the end, that input
really reached the editor and the mean frame stays under 250 ms. Two seeds x
150 actions run in CI; DECKCRAFT_UI_SOAK_STEPS/SEED run it long.

Panics swallowed by the guards were invisible to tests: Session now counts
them (panics_caught) and deckcraft_render::panics_caught() counts render
panics drawn as blank. Textures::thumb_count exposes the thumbnail cache size.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 22:09:48 +03:00
Nikita RakitinandCursor 460a2712e4 Transitions: choose a transition sound and play it in the show
Transition sounds were read from and written to .pptx but could not be
set and never played. Transitions › Timing gets a Sound box ([No Sound]
or Other Sound…), backed by a new transition.sound command that embeds
an audio file on the selected slides (or removes it with none). In the
slide show the sound plays as its slide comes in and, as in PowerPoint,
keeps playing into the next slide unless that slide has a sound of its
own. Built-in sounds, Stop Previous Sound and Loop Until Next Sound are
still missing; the parity docs say so.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 22:07:06 +03:00
Nikita RakitinandCursor 283b30f12d UI: WordArt Transform gallery applies a text warp
Shape Format ▸ Text Effects lists the 40 prstTxWarp presets and
No Transform. format.textWarp sets or clears that warp on the
selected text, with optional adjust values.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:58:35 +03:00
Nikita RakitinandCursor 607ffc5591 Text: set text transparency from Text Fill (#32)
There was no way to make text see-through: the only Transparency
slider was the shape fill's. The new format.textTransparency command
keeps the text's colour and sets its alpha (replacing any it had, and
dropping it at 0). A run without its own colour takes the colour it
shows now, so inherited placeholder or theme colours stay as they are.
Gradient text gets the alpha on every stop. The Format Shape pane's
Text Options page gains a Text Fill section (colour and Transparency),
and the Shape Format › Text Fill dropdown has the same slider.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:55:21 +03:00
Nikita RakitinandCursor 14bc6a7ef9 Format Shape: shadow, glow and reflection detail controls
shape.effects gains shadowProps / glowProps / reflectionProps that adjust
the current effect (or add a default one): shadow colour, transparency,
blur, angle and distance; glow colour, transparency and size; reflection
transparency, size, distance and blur. Values are clamped, non-finite ones
ignored. The Effects page of the Format Shape pane exposes them as sliders
and colour pickers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:52:56 +03:00
Nikita RakitinandCursor 85833cf5f5 Engine: seeded editor soak tests; tiny charts no longer blank the slide
soak_tests.rs drives every non-file command with plausible and hostile
parameters over a ~100-slide deck, checking after each step that the
document validates, the selection is in range, the undo stack is bounded,
undo then redo restores the exact document, and every 100 steps that the
deck saves and reopens as .deckcraft and .pptx. Four seeds x 400 steps run
in CI (under a second); DECKCRAFT_SOAK_STEPS/SEED run it long.

The long soak found that a chart a few points wide panicked in f64::clamp
(label gutter min above max), and the render guard then blanked the whole
slide. chart.rs now uses a non-panicking clamp_soft for the gutters and the
zero line; a render test draws tiny charts next to a shape without the
guard.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:50:23 +03:00
Nikita RakitinandCursor 16c5d6a6a3 Format Shape: line compound, cap, join, arrows and sizes; lock aspect ratio
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>
2026-10-13 21:45:42 +03:00
Nikita RakitinandCursor e6920f8ebe Print: choose paper size and orientation for notes and handouts
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>
2026-10-13 21:43:54 +03:00
Nikita RakitinandCursor eec67560df Fonts: find fonts where Linux and Flatpak keep them; add and rescan commands (#277)
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>
2026-10-13 21:16:23 +03:00
Nikita RakitinandCursor ede2ca69b6 Designer: layout ideas that rearrange the current slide (#314)
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>
2026-10-13 21:10:59 +03:00
Nikita RakitinandCursor 7d23e4670b Charts: pie and doughnut data labels use the number format (#324)
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>
2026-10-13 21:01:43 +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 d2066a1f07 Pptx: budget chart points, chart XML size, inflate work and VML lookups (#401)
- 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>
2026-10-13 20:53:49 +03:00
Nikita RakitinandCursor 201ab63c48 Render: cap RGBA conversion and bound the failed-image cache (#402)
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>
2026-10-13 20:52:20 +03:00
Nikita RakitinandCursor cb26e7a04f Text: bidi levels for language runs in linear time (#403)
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>
2026-10-13 20:52:15 +03:00
Nikita RakitinandCursor ce878ef74e Render: budget shape path-gradient work on dense outlines (#404)
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>
2026-10-13 20:49:02 +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
Nikita RakitinandCursor de098cfa52 Anim: group animations by sequence in one pass (#410)
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>
2026-10-13 20:39:57 +03:00
Nikita 6873a25635 Codecs: a hostile number is a format error, not an overflow
H.264 and HEVC quantiser offsets, Matroska block times, Opus granule timing and QuickTime PCM frame sizes are range-checked before the arithmetic.
2026-10-13 20:16:51 +03:00
ZetenypappandClaude Opus 5.5 1baf1f5569 Copy and Cut buttons and menus put the text on the system clipboard (#431)
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>
2026-10-13 18:45:38 +02:00
Sazid Al Bayazid 8ba97e61be Slide show: Honeycomb's hexagons cover the whole slide again (#420)
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.
2026-10-13 21:58:25 +06: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 09b72e4b70 Pptx: keep the math namespace equations inherit from the slide root or a wrapper (#412) 2026-10-13 17:41:28 +02:00
MarekSpodBiedry 902edb3c85 UI: notes, alt text and rename drafts no longer carry over to another document (#413) 2026-10-13 17:40:31 +02:00
MarekSpodBiedry d4cf50dd3d MCP: do not resend a command whose reply was lost (#409) 2026-10-13 17:38:28 +02:00
MarekSpodBiedry 33ade63687 Table: split and cell fill stay inside the table (#400) 2026-10-13 17:38:20 +02:00
MarekSpodBiedry 45b8dce220 Info: keep a property draft when another field or the side menu is clicked (#422) 2026-10-13 17:38:13 +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
MarekSpodBiedry a4a7ebe658 Bitstream: reject bad NAL length widths, check NAL endpoints and signed Exp-Golomb limits (#397) 2026-10-13 17:37:25 +02:00
MarekSpodBiedry 9aebc8ba51 AV1: reject tile groups that repeat, skip or reorder tiles (#395) 2026-10-13 17:37:24 +02:00
MarekSpodBiedry efb2127e4e Format scientific number formats (0.00E+00) in chart labels (#419) 2026-10-13 17:36:03 +02:00
MarekSpodBiedry c623b4f555 Matroska: stop following chapter and tag nesting past 32 levels (#396) 2026-10-13 17:35:58 +02:00
MarekSpodBiedry 34372569e7 xtask: assets audit no longer panics on Unicode names and reads real paths from git (#424) 2026-10-13 17:34:03 +02:00
MarekSpodBiedry 3ed12de918 Opus: validate the header in Decoder::from_head (#425) 2026-10-13 17:33:29 +02: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
NikitaandCursor 4fee02636a Metafile: clip EMF and WMF drawing to rectangle regions
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>
2026-10-13 17:57:34 +03:00
NetterSilver 12dd392705 Docs: update the progress docs for this change 2026-10-13 16:41:57 +02:00
NetterSilver 7887a13868 Colour menus: an Eyedropper that takes a colour from the slide (#367)
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.
2026-10-13 16:41:57 +02:00