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>
- engine: add animation.painter (Alt+Shift+C) and animation.painterApply (Alt+Shift+V) supporting single-use and sticky modes
- tools: apply animation painter on canvas shape click
- edit: disarm animation painter on deselect (Escape)
- ui-egui: add Animation Painter button in Advanced Animation ribbon group
- tests: unit tests for single and sticky animation painting
- docs/parity: mark Animation Painter as done (breadth parity to 80%)
- contributors: add Johannesgauss to contributors/people.toml
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Three causes, each making links look like they "don't save":
- With the caret at a word boundary (end of text, empty paragraph),
insert.hyperlink formatted an empty range and changed nothing. It now
inserts the address (or the slide's title, or `text`) as linked text, as
PowerPoint does. Typing right after a link no longer extends it.
- The slide show ignored hyperlinks and shape actions. A click on one now
follows it (ShowState::follow): slides, next/previous/first/last, Last
Slide Viewed, End Show, and URLs; the pointer turns into a hand over links.
- The desktop app was built without eframe's `links` feature, so
ctx.open_url did nothing natively (also the ribbon's app page link).
Enabled as in DesignCraft, EffectCraft and PrintCraft.
Saving was fine: links round-trip through .deckcraft and .pptx.
generated with Claude Code
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(pictures): animated GIFs play, with pause (media.gifPlay)
GIF pictures used to show only their first frame. The renderer now decodes every frame (cached, capped at 256 MB and 1024 px a side) and draws the frame for each GIF's play time, passed in RenderOpts::gif_times; exports and thumbnails pass none and keep frame 1. The editor canvas and the slide show redraw only when a GIF reaches its next frame. A corner button on hover (or selection) pauses and resumes a GIF from the same frame, through the new media.gifPlay command.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(pictures): animated GIFs play, with pause (media.gifPlay)
GIF pictures used to show only their first frame. The renderer now decodes every frame (cached, capped at 256 MB and 1024 px a side) and draws the frame for each GIF's play time, passed in RenderOpts::gif_times; exports and thumbnails pass none and keep frame 1. The editor canvas and the slide show redraw only when a GIF reaches its next frame. A corner button on hover (or selection) pauses and resumes a GIF from the same frame, through the new media.gifPlay command.
Also adds my entry to contributors/people.toml.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Animated GIFs: cap frames (1000), canvas (4096²), per-GIF (64 MB) and cache (192 MB) memory
A few MB of hostile GIF could decode to millions of tiny frames or a 100 Mpx canvas per frame,
and eight cached GIFs could hold 2 GB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Flips mirror a shape's geometry, never its text. The text was drawn with the shape's full
transform, so every label on a flipped arrow came out mirrored (flipH) or mirrored upside down
(flipV). As in PowerPoint, the text rect keeps its flipped place and the text is mirrored back
about the rect's centre: flipH text stays upright, flipV (or both flips) turns it 180°, rotation
still applies, and the same holds for shapes inside flipped groups.
render::text_transform is that transform (plus vertical-text rotation). Drawing, the PDF text
layer and link boxes, morph text, the editing caret and selection, and click-to-place-the-caret
all use it, so a click lands on the character drawn under it.
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>
Text with a:hlinkClick (to a URL or a slide) looked like the text around it. As in PowerPoint,
a linked run now takes the theme's hlink colour and a single underline, unless the run itself
sets a colour (PowerPoint 2019 and later honour it) or an underline (u="none" removes it).
Colours from list and master styles don't count, so links in titles are blue too.
This is part of run resolution, so links added with insert.hyperlink look the same, Format
shows them underlined, and turning underline off writes u="none". The run's own props are left
alone, so the look follows the theme.
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Sprint 1: bidi and Arabic typography engine
fonts: ISO 15924 script and a:latin/a:ea/a:cs slot classification;
shape_range shapes an item with the whole paragraph as HarfRust
pre/post context, explicit script/language/direction, glyphs kept in
visual order; per-grapheme-cluster fallback (slot face, theme script
font, OS cascade families, bundled craft-fonts face); GDEF ligature
carets.
text: UAX #9 levels for the whole paragraph (a:pPr/@rtl), itemization
by level, script, face and shaping style (paint-only changes no longer
split words), logical line breaking at cluster boundaries, per-line
L1/L2 reordering (L4 mirroring by the shaper); RTL start edge and
bullets, justLow kashida; caret Affinity, logical/visual movement,
bidi hit testing and selection as the union of visual boxes.
model/pptx: theme a:font script fonts and algn="justLow" round-trip.
engine: format.rtl (Right-to-Left Text Direction), justifyLow align.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fonts: the bundled script face comes before installed fonts; no Microsoft family names
Arabic, Hebrew, Thai and Devanagari clusters try the craft-fonts face first, so every machine
draws the same, then openly licensed or OS-native installed families, then any installed face.
Installed Microsoft faces are still used through that scan, but not named (AGENTS.md §1.1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Tayeb <tayeb@192.168.11.179>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Tables: rows grow to fit their text, cell text in the PDF text layer (#13)
A stored row height (a:tr/@h) is a minimum, as in PowerPoint: each row grows so its cells' text fits. Tables from pandoc or python-pptx store h="0" and drew every row on top of each other. Cell editing (click to edit, cell outline) uses the same heights, and the PDF text layer now includes table cell text.
* Tables: a grown table's box covers its drawn rows; resize scales from drawn heights
Handles, hit tests and marquee used the stored frame, so rows grown to fit their text drew
below the selection box (a file with h=0 rows had a 0 pt tall box). Resizing divided by the
stored row total, which is ~0 for those files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- crates/media: probe (duration, rate/channels, size/fps; WMA/WMV recognised), audio decode to
f32 PCM (MP3, AAC M4A/ADTS, ALAC, WAV, AIFF, CAF, FLAC, Vorbis via symphonia; Opus with our
decoder in Ogg/WebM/MKV), video decode (MP4/MOV/WebM/MKV) with YUV->RGBA, poster frames,
trim/fade/volume, Player mixer + per-voice clock (device-clocked or wall-clocked), VideoFeed.
- Codec crates ported from FilmCraft: bitstream, h264, hevc, vp9, av1, isobmff, matroska, ogg, opus.
- insert.audio/insert.video probe and store duration/size and a poster frame; unplayable media
is embedded with a warning. New media.play/pause/toggle/stop/seek/info/posterFrame.
- UI: MediaHost (cpal output injected by the desktop app), control bar under media, video frames
on canvas and in the show; autoplay, click to play, loop, rewind, across slides, hide, full screen.
- Tests with in-code WAV/AIFF and CC0 self-generated fixtures (make.sh, ATTRIBUTION.md rows).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>