Toggling a layer's Audio, Lock or Shy switch (or Hide Shy Layers) threw
away every cached frame: the RAM preview identity hashes each comp's
address, which any edit replaces, and the layer and disk cache keys
hashed every switch. None of these change pixels.
- Switches::pixels, Layer::same_pixels and Comp::same_pixels (exhaustive
field lists, so a new field must be considered) and Comp::pixel_form.
- Layer cache keys hash Switches::pixels; the disk cache's comp content
key hashes Comp::pixel_form.
- Frames::identity: when a new revision's comps draw the same pixels as
those of the comp's last new identity, that identity (and the project
holding its comps' addresses) is kept, so the frames stay, also when the
switch is toggled back.
With audio on, playback skipped frames that weren't rendered yet. Now the
sound waits: until the frames ahead (to the end of the preview, or what
fits in the RAM preview) are cached, every frame shows as it renders,
silently, like a preview without audio; then the sound starts in sync.
Reaching an uncached frame stops the sound instead of skipping frames.
Tests: audio_lock_and_shy_switches_keep_the_frames (frames.rs, nested
comps and toggling back), audio_lock_and_shy_keep_cached_layers (engine),
the disk key in tests_disk_cache, and
preview_with_audio_shows_every_frame_and_sounds_once_cached.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An angle's revolutions were painted text: only the degrees could be
dragged or typed, so full turns needed the dial or typing past 360.
fx_widgets::angle_field draws `0x+45.0°` as two hot numbers: the
revolutions (widgets::hot_int_at, ten pixels a turn, fractions adding up
between frames so slow drags still step; a typed value is truncated) and
the degrees, each keeping the other. Effect Controls, the Timeline and
the Properties panel use it, so they read and edit angles alike (the
Timeline showed `-1x-40.0°` and Properties had its own split).
Automation ids: timeline.prop.<uid>.revolutions,
effectControls.prop.<uid>.revolutions, properties.prop.<uid>.revolutions.
Regression test: angle_revolutions_scrub_and_take_typing drags and types
the revolutions in all three panels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Choosing Draft Settings (or any entry) in a Render Queue item's Render
Settings menu did nothing when the menu had been moved up to fit the
window. egui hit-tests a fixed-position Area where it was asked to go,
not where it was moved, so the popup's "pressed outside" check was true
over its own entries and the menu closed on the press, before the click.
widgets::pressed_outside tests the press against the rect the popup was
drawn at. Every popup with the same check uses it: popup_menu (Render
Queue, Effect Controls, dropdowns), the colour popup, the Preview and
viewer popups, the panel menu, the Flowchart, OpenType and font menus.
Regression test: ui_render_queue.rs picks Draft Settings from a menu
moved up to fit a 1000 px window; it failed before the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#94 asked for native Windows on ARM builds and for their availability and limits to be
documented. The builds landed in #101 (an arm64 entry in the release matrix, the PE checks in
package.ps1 and the windows-arm64 workflow that installs the MSI on ARM64 hardware), but the
README never said they exist. Get started now lists the installers on the Releases page for
each platform, says the Windows ARM64 build runs without x64 emulation, and says what CI checks
on ARM64 hardware and what it doesn't yet (the app's window and a native test run).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iLqXiCVkUULoN4eWTD7cB
effect.apply / effect.applyLast run through the menu dispatcher (Effect
menu, Last Effect, an effect dropped on a Timeline layer or on Effect
Controls) now bring up Effect Controls, opened if closed and brought to
the front without taking the focus, on the layer the effect went to
(selecting it if it was dropped on an unselected layer, keeping the new
effect selected). Double-clicking an effect's name in the Timeline opens
it in Effect Controls (layer.select, prop.select, window.panel) instead
of twirling it, as in After Effects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 04bd305b9dccbb34d8c70fe870513208cdae0b7b)
Opening a comp (Project double-click, Open Composition, a Timeline tab)
now reopens the Composition panel if it was closed and brings it (and
the Timeline) to the front, as in After Effects. A closed Composition
panel also reopens in the centre (with the Layer panel or other viewers,
else above the Timeline) instead of beside Effects & Presets, which also
fixes Window > Composition.
App::raise_panel opens/fronts a panel without moving the focus;
show_panel builds on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit adf2c6dac78ce426ea0bf6b4199b7238d8ad99dd)
gaps.md records the Windows report and its fix under real-user experience and G2; parity.md's
Text row, the ROADMAP's reliability note and the README's Text bullet say text can use any
installed font; architecture.md says when the system font scan runs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EjTYdt76hTG4Kh2CJFnCr
A Windows user reported that the Properties panel's font menu offered only the bundled Inter,
JetBrains Mono and Noto Serif. The system font folders were scanned only when a project asked for
a family the bundled fonts lack, so until then every font menu (Properties, Character, Essential
Graphics) and `file.findMissing {what: fonts}` saw the bundled families alone, on every
platform. Listing the families now scans the system fonts first, and the desktop app starts that
scan on a background thread at launch so the menus are complete when first opened.
The style menus in the Properties and Character panels list the chosen family's own styles
instead of a fixed Regular / Medium / SemiBold / Bold / Italic. Both menus build their lists only
while open.
New query for agents: `text.fonts {query?, rescan?}` (MCP tool `list_fonts`) lists every family
with its styles, origin (bundled / system / user) and own-language name; `rescan` picks up fonts
installed since launch.
Tests: a fresh-process test that the family list includes every family in the platform's font
folders without a prior scan (failed before), and `text.fonts` filtering, rescan and per-family
styles, and the `list_fonts` MCP tool.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EjTYdt76hTG4Kh2CJFnCr
A selected pin was drawn only 1 px bigger and slightly brighter. Now, like
selected and unselected mask vertices in the viewer, a selected pin is
filled with its kind colour and ringed in white, and an unselected pin is
hollow (dark centre, coloured outline). Drawing only; hit-testing is
unchanged.
Regression test: selected_puppet_pins_are_filled_and_unselected_hollow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 103b5c41e9e8ea84008593e05af50b01a2800b3e)
Clicking an effect's name selected it (prop.select with the effect's group
uid) but group rows never painted the selection, so there was no feedback.
Group rows (effects, masks, shape groups...) now fill with row_selected when
selected, exactly like selected property rows.
Regression test: timeline_selected_effect_row_highlights.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 8b140428b39a24c3e1207ffde0430e759e316112)
- Project panel Clear (#95's focus routing) goes through invoke, so deleting
items that compositions use still asks first (M3.16).
- ui_project_delete: let a dialog finish egui::Modal's sizing frame before
clicking its buttons (#95 moved dialogs to egui::Modal).
- #71's i18n / theme tests clear the frame's texture delta; egui asserts on
unhandled deltas in debug builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release builds add an arm64 MSI and portable zip, cross-compiled on the x64
runner (signing and WiX as for x64). package.ps1 checks each binary's PE
header before packaging: Machine must match -Arch, the app must be a GUI
program and the CLI a console program. It skips the CLI smoke run when an
ARM64 build can't run on the host. A new Windows ARM64 workflow packages the
ARM64 MSI, installs it on a Windows 11 ARM64 runner, checks both installed
exes are ARM64, runs the CLI, and uninstalls. Ported from printcraft#66.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rust 1.99 renamed AtomicU32::fetch_update to try_update; try_update is not
available on the workspace's minimum Rust (1.95).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 2232354b902238827b2e80a29ef266c2e5bde272)
(cherry picked from commit 95da7943454ca5b2575993d11f9ba046822ce255)
Fixes the Windows installer: the Start Menu shortcut and Add/Remove Programs icon were blank. The shortcut description was wrong. Release builds no longer open a console window behind the app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MSI Icon id had no .ico/.exe extension, which Windows Installer needs for
advertised shortcuts and ARPPRODUCTICON, so both showed a blank icon. Release
builds of the desktop app are now GUI-subsystem (no console window; the CLI stays
a console program), the shortcut/summary description names what EffectCraft does, and
packaging-lint rejects an Icon id without an extension. Found in printcraft#57.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As in After Effects, the Delete key and the Project panel's trash button ask before deleting
items that layers in other compositions use, saying how many layers in how many compositions go
with them (Delete / Cancel; Escape cancels). Unused items are deleted at once; agents running
project.delete directly are not asked. project.usage {items?} reports the count, sharing the
deletion's item and usage helpers. Follow-up to #72.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japanese (and Chinese, Korean) text in a family that lacks the glyphs, such as the default Inter,
rendered as missing-glyph boxes: `fallback_for` only searched the bundled faces and faces some
other layer had already loaded, so the result depended on render order. Now, after those cheap
candidates, it tries the platform's usual family for the character's writing system (kana and
hangul pick their language; Han ideographs follow the locale, Japanese first when it is unknown,
`EFFECTCRAFT_CJK_LOCALE` overrides) in the preferred face's style, and finally any installed face
that covers the character, reading files without keeping them. Results are cached per 256-character
block and preferred face.
`resolve` also accepts a face's full name (`Yu Gothic Bold`) or PostScript name (`YuGothic-Bold`),
which is how After Effects scripts set `textDocument.font`; the sfnt reader now records name id 6.
Tests: script classification, PostScript / full-name resolution on the bundled Inter, and (native,
skipped when no Japanese family is installed) that kana, ideographs and CJK punctuation in Inter
Bold fall back to an installed Japanese family's bold cut, with the cache returning the same face.
`cargo xtask ci` runs clippy and the tests with --release, where egui has no Style::debug (it
exists only with debug_assertions), so ui_agent_audit.rs failed to compile since M13.14. The
debug option is set only in debug builds, and the test, which needs the widget types egui records
with it, is ignored in release builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As in After Effects, a double-click below the items in the Project panel opens File ▸ Import ▸
File... (rows and the scroll bar keep their own clicks). Dropping files on the window already
imported them; the test now covers both. (On Wayland, winit delivers no file drops at all, so
dragging files in works on X11, macOS and Windows.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Wayland compositors take a window's icon from the desktop entry named by its app ID; the .deb,
.rpm and Flatpak install it, an AppImage installs nothing, so its window got the generic icon.
Started from an AppImage ($APPIMAGE), the app now writes a hidden (NoDisplay) desktop entry
pointing at it, and its 256 px icon, under $XDG_DATA_HOME (~/.local/share) before the window
opens. Nothing is written when an installed package or the user's own entry provides the ID;
the entry follows the AppImage when it moves; failures are only logged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The details header (item name, size, duration, codec) and the "Select an item" hint were painted
with no width limit, so in a narrow Project panel they ran past its right edge. They now stop
10 px short of it and end in "…" when they don't fit (widgets::text_fit, a shared helper), and
are registered for automation (project.details.*).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers what issue #46 reports (v0.1.1 on Linux): a panel dragged by its hamburger onto another
group's edge docks there as its own group, and dragging the gap between panels resizes them.
Both work on main (tab dragging was reworked in M3.15 for v0.2.0); this keeps them working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While paused, the app prefetched the frames around the current one as soon as the viewer's
frame finished. During a drag every step changes the project, so those frames were stale at the
next step, and they held the CPU and GPU the next viewer frame needed: on the demo project a
drag step took 102 ms on average to show (up to 413 ms) though a frame renders in 20 ms. Paused
prefetch now waits until edits pause for 0.3 s: 16 ms a step (11 ms render).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>