47 Commits
Author SHA1 Message Date
tylerjenningsw de0f573620 Layer Styles: the controls scroll both ways above the buttons (#1062)
Keeping OK and Cancel in view took away the outer scroll area that also
scrolled the dialog sideways, and the controls only scrolled up and
down. In a narrow window (960x600 at 200 % UI Scale leaves the controls
about 190 points next to the 200-point list) they ran past the dialog's
right edge with no way to reach them.

The controls now scroll both ways in the room left under their heading,
with no minimum scrolled size, so a small budget can't hide the end of
them; the list scrolls in its height the same way. Each control also
registers dialog.layerStyle.value.<style>/<path> next to its label's id.

Tests:
- layer_style_controls_stay_reachable_in_a_small_window: in a 960x600
  window at 200 %, and opened large then narrowed, Noise is scrolled to,
  painted whole and dragged; fails before (it lies past the window).
- layer_style_buttons_stay_in_view now checks both buttons at 100 % and
  200 %, each case in a fresh app with a real edit: OK keeps it, Cancel
  puts the project and its undo history back as before the dialog.
2026-10-12 22:42:53 -04:00
tylerjenningsw 544d46a218 Workspaces: a failed save says a restart may undo the changes (#696) 2026-10-12 22:03:01 -04:00
tylerjenningsw e643457fa3 Commands: an earlier invocation's frontend failure isn't a later command's result (#696) 2026-10-12 22:03:01 -04:00
tylerjenningsw ecf485e375 Docs: engine.execute reports a failed workspace save through workspaceStorageError (#696) 2026-10-12 21:35:48 -04:00
tylerjenningsw dbaf559b58 Docs: workspace commands report a save that didn't reach disk (#696) 2026-10-12 21:32:24 -04:00
tylerjenningsw 41b04bf414 Workspaces: tests for a failed, a storage-less and an on-disk workspace save (#696) 2026-10-12 21:32:22 -04:00
tylerjenningsw b9a9a0a414 Workspaces: Save, Save As, Rename and Delete fail when the file isn't written (#696) 2026-10-12 21:32:18 -04:00
tylerjenningsw 73ea427015 Commands: a frontend-only command's own failure is its result (#696) 2026-10-12 21:31:16 -04:00
tylerjenningsw bf911d18bc Layer Styles: OK and Cancel stay in view (#1062)
The Layer Style dialog's OK and Cancel came after the style list and
the style's controls in one scroll area, and at its usual size they
were laid out past the dialog's bottom edge: clipped, and a click on
them did nothing until the dialog was scrolled.

The dialog now uses above_footer: OK and Cancel sit at its bottom right
and the list and the controls scroll above them, also at 200 % UI
Scale.

Test: layer_style_buttons_stay_in_view (both buttons painted whole and
clicked, at 100 % and 200 %; fails before: Cancel isn't painted).
2026-10-12 21:24:43 -04:00
tylerjenningsw bcfb25ca93 Dialogs: share the buttons-first layout Settings uses
Settings keeps its buttons in view by laying them out first, at the
bottom of its body, and giving its pages the height left above them,
clipped to it. That layout is now dialogs::above_footer, so other
dialogs can keep their buttons in view the same way. Settings looks and
works as before.
2026-10-12 21:24:41 -04:00
tylerjenningsw 765915e126 Workspaces: deleting the current workspace saves after switching to Default (#696) 2026-10-12 21:07:39 -04:00
tylerjenningsw bfb7507565 Timeline: a Quick Offset of layer bars ends on release off screen (#1012)
The layer bar Quick Offset was followed and ended in its row's code, and
rows out of view aren't drawn. Released with its row scrolled away, it
stayed active: the next drag of that bar, even a plain one, ran as a
Quick Offset from the old origin, into the old undo step.

Like the key drag, it now follows the pointer below the rows and ends
when the button is released, wherever the bar's row is.

Test: a_quick_offset_released_off_screen_ends (release with the row
scrolled away; a plain drag after it moves both layers together as its
own undo step; fails before).
2026-10-12 18:49:51 -04:00
tylerjenningsw e1c9c33fb1 Keyframes: a refused key drag step doesn't count towards the drag (#1012)
keys.quickOffset and keys.move stored the drag's new total before the
edit ran. When the edit refused the step (keys.quickOffset with a key
landing past the end of time), the project stayed as it was but the
total kept the refused delta, and with incremental steps (merge without
fromStart) the next step moved by it too.

The drag state is now stored only after the edit applies.

Test: a_refused_step_does_not_count_towards_the_drag (+1 s, a refused
step, -1 s: the keys end where they started; fails before: the last key
ends near the end of time).
2026-10-12 18:49:48 -04:00
tylerjenningsw f72b8993ef Keyframes: moving keys keeps subframe spacing (#1012)
keys.move and the nudges snapped each key's destination to the nearest
frame, so moving keys a Quick Offset had staggered onto subframes put
them back on frames: four keys at 0, 1/3, 2/3 and 1 frame, moved one
frame, came out at 1, 1, 2 and 2. Layer bars kept theirs, because
layer.timing snaps the move, not the times.

The move is now rounded to whole frames and every key translated by it.
layer_shift turns a comp-time move into a layer-time move through the
layer's start and stretch (reversed too) without a round trip through
comp time; keys.quickOffset uses it as well.

Test: moving_staggered_keys_keeps_their_subframe_spacing (Quick Offset,
then keys.move and keys.nudgeForward; fails before: 1/3 frame lands on
a frame).
2026-10-12 18:49:34 -04:00
tylerjenningsw e2d50ec926 Timeline: Shift ranges and key boxes stagger in comp order (#1012)
In After Effects a marquee selection staggers in the comp's layer order,
not in the direction the box was drawn. A Shift-click range of layer
bars and a box of keys already come out top layer first; this test makes
both with the pointer (the range from the bottom layer up, the box from
the bottom right to the top left) and checks the Quick Offset that
follows: the top layer stays and the bottom one moves the whole delta.
2026-10-12 17:45:30 -04:00
tylerjenningsw 94b4d0ca34 Timeline: Quick Offset layers and keyframes in selection order (#1012)
Ctrl/Cmd+Alt dragging a selected layer bar or a selected key now runs a
Quick Offset (layer.quickOffset / keys.quickOffset with fromStart, one
undo step). The mode is chosen when the drag starts and needs at least
two unlocked layers; the last selected layer moves by whole frames with
the pointer. A readout by the pointer shows the Total Offset and the
offset Per Layer, translated.

A plain drag still moves the selection together, Alt alone still scales
a key group in time (it now leaves Ctrl/Cmd+Alt to Quick Offset), and
Ctrl/Cmd+Alt+click still toggles Hold. The key drag's readout is drawn
by a shared drag_tip. Keys also register timeline.keyAt.<uid>.<ticks>,
exact where keys on subframes can share a frame.

Tests (ui_quick_offset): staggering layer bars in selection order and
keys per layer, one undo each; plain drags and Alt scaling unchanged; the
readout while dragging.
2026-10-12 17:37:34 -04:00
tylerjenningsw 28e9e5229e Quick Offset: layer.quickOffset and keys.quickOffset commands (#1012)
After Effects 25.4's Quick Offset staggers selected layers, or each
layer's selected keyframes, in the order they were selected: the first
stays, the last moves by the drag and the ones between by equal steps,
landing on subframes.

layer.quickOffset {delta, layers?} moves each layer's start, in and out
point by total * i / (n - 1), worked out once in ticks with integer
arithmetic, so 1 frame over 4 layers gives 0, 1/3, 2/3 and 1 frame
exactly. keys.quickOffset {delta} does the same per layer for the
selected keys, the layers in the order their first key was selected;
every selected key of a layer, across its properties, moves by that
layer's comp-time offset through its start and stretch (reversed too),
keeping values, interpolation, easing and labels. Locked layers are
skipped and at least two layers are needed. With merge and fromStart,
delta is the whole drag and each step starts again from before it, so
the drag is one undo step and a key passed over survives. A delta or a
destination out of the range of times is an error, not a clamped move.

Docs: agents.md; architecture.md notes Quick Offset as the exception to
times snapping to frames.
2026-10-12 17:37:31 -04:00
tylerjenningsw 7643f803e0 Keyframes: moving keys keeps the selection order
keys.move (and nudging) rebuilt the key selection in layer and property
order, so the order the keys were selected in was lost after a move. A
Quick Offset that follows staggers layers in that order (#1012).

The move now goes through move_key_refs, which maps each selected key to
its new time and returns the selection in its original order, with the
same collisions (a key landing on another replaces it), roving retiming
and locked-layer handling as before. key_gesture is the drag-step logic
(start again from the keys and project as they were before the drag)
split out of shift_keys so other key drags can share it.

Test: moving_keys_keeps_the_selection_order (fails before: the keys come
back sorted by layer).
2026-10-12 17:37:29 -04:00
tylerjenningsw f6d42e2d67 Text: formatting tabs keep the edit; keys after a click away skip it (#831)
Two follow-ups to ending text editing on a click in another panel.

A press anywhere in a panel group focused the group's shown panel, even
a press on another of its tabs. Opening Properties or Character behind
Effects & Presets (as in Undocked Panels) therefore ended the edit on
the press, before the tab's click focused the panel chosen, and later
formatting went to the whole layer instead of the selected characters
(an empty Type-tool layer was even removed). A press on another tab now
waits for that tab's click.

The click away is resolved after the frame's panels, so keys that came
after it in the same frame's input were still typed into the text. The
text keyboard handler now takes the frame's keys in order up to a press
outside the viewer: keys before the click are typed, keys after it are
neither typed nor run as shortcuts, and from the next frame they are
the app's.

Tests: opening_a_formatting_tab_keeps_the_text_selection (a real click
on the Properties tab keeps the edit and its selection; Faux Bold then
styles only "Hello") and keys_after_a_click_away_in_the_same_frame_skip_
the_text ("A", a click on a Timeline row, then "x", Space and Delete in
one frame: only "A" is typed, nothing is played or deleted; Space in the
next frame plays). Both fail on the previous commit.
2026-10-11 14:48:20 -04:00
tylerjenningsw a47995aabb Docs: gaps.md records the toolbar gradient-swatch tests 2026-10-11 14:46:55 -04:00
tylerjenningsw 3d7f928465 Shapes: the toolbar's gradient swatches open the Gradient Editor (#612)
With Fill or Stroke set to Linear or Radial Gradient, clicking its
toolbar swatch opened Fill / Stroke Options, so the gradient's colours
couldn't be edited from the toolbar as in After Effects (reported on
Discord, EffectCraft 0.8.0 on Windows 11). The swatch now opens the
Gradient Editor (prop.editGradient) on the selected shape layer's first
Gradient Fill or Gradient Stroke Colors, in contents order through nested
groups; OK sets that property. With no shape layer selected, or none
with that gradient, the swatch opens the options as before (the kind new
shapes get: the toolbar keeps no gradient stops of its own yet). The
words still open the options, a Solid Color swatch the colour picker,
and Alt-click still steps through the kinds.

Tests (ui_toolbar_gradient): a Linear Fill swatch opens the editor on the
layer's Gradient Fill Colors and OK saves an added stop; a Radial Stroke
swatch edits the Gradient Stroke, not the fill; without a selected
gradient the options open; the words, Solid swatches and Alt-click are
unchanged. The two gradient-swatch tests fail without this change.
2026-10-11 14:46:45 -04:00
tylerjenningsw 1b7ab07822 Text: stop capturing keyboard input after clicking away (#831)
After typing into a text layer, clicking another panel (a solid in the
Timeline, say) left the edit open, and the viewer took the keyboard back
as soon as nothing had the focus: typed characters went into the text
and Space added a space instead of playing.

A click in any panel other than the viewers and the panels that format
the selected characters (Character, Paragraph, Properties) now ends the
edit with text.endEdit, so the text is kept and an empty layer the Type
tool started is removed, and the viewer's text focus is surrendered. The
editor takes the keyboard back only after a click in the viewer or one
of those panels, no longer merely because nothing has the focus. A click
elsewhere in the viewer still commits, and with the Type tool still
starts new text.

Test: clicking_another_panel_stops_typing_into_text, with the Type tool
and with an edit started by command: Character > Faux Bold keeps the edit,
its selection and typing; clicking the solid's Timeline row ends it,
selects the solid, typed characters leave the text unchanged and Space
plays; an empty Type-tool layer is removed. Fails on main.
2026-10-11 13:01:36 -04:00
tylerjenningsw f4c5015b35 Settings: keep action buttons visible at high UI scales
At 200 % in a 1600x1000 window, Previous / Next / Cancel / OK were cut
off. Settings sized its pages from the window height less a fixed 170,
and the shared modal put everything, buttons included, in one more
scroll area, so a footer that didn't fit scrolled out of the dialog.

The modal helper gets a modal_fit variant without that scroll area: it
hands its body the height left under the title. Settings lays out its
buttons first, from the bottom up, and gives the page list and the page
what they leave; both scroll on their own and the buttons stay put. The
pages are a clipped child, so a wide page can't widen the dialog under
the buttons. Other dialogs keep modal as it was.

Tests: settings_buttons_stay_in_view_at_200_percent opens Settings at
200 % in 1600x1000 and checks that each button is inside the window and
painted unclipped (fails before this change: the scroll area clipped
them at the bottom). It scrolls the page to UI Scale without moving
them, then types 150 and clicks Cancel (200 % again, nothing saved) and
OK (150 % saved). The UI Scale test no longer needs a larger window and
clicks Cancel at 200 %.
2026-10-11 11:48:07 -04:00
tylerjenningsw f45f57da7d Docs: gaps.md records the #612 viewer gradient-handle tests 2026-10-11 11:12:15 -04:00
tylerjenningsw c39f29882c Shapes: drag gradient start and end points in the Composition viewer (#612)
With a shape layer's Gradient Fill or Gradient Stroke (or a property in
it) selected, its Start Point and End Point show in the viewer as
draggable crosshairs with a guide between them, as in After Effects. The
points are in the space of the shape group holding the gradient, so they
are mapped through the enclosing groups' transforms and the layer's
(shapes::item_matrix), and a drag converts back the same way. A drag is
one prop.set merge: one undo step, and on an animated point a keyframe at
the current time. The hook runs after the viewer's own gestures, like the
effect point controls, so a handle takes the press and the layer doesn't
move. They follow View ▸ Show Layer Controls.

Tests (ui_gradient_handles, only pre-existing APIs, all three fail without
this): dragging the End Point moves it by the drag in one undo step without
moving the layer; inside a moved, 200%-scaled group the handle sits where
the group draws the point and a drag converts to group space; no handles
unless the gradient or a property in it is selected, or with layer
controls off. ui_effect_controls still passes.
2026-10-11 11:12:05 -04:00
tylerjenningsw 3df7e8f77c Shapes: a closed colour picker leaves the next Escape to the dialog (#612)
The picker marked every pass it drew, including the one it closed in, so
for one pass after closing the dialog Escape handler still stepped aside
and a quick second Escape did nothing. The mark is now set only while the
picker stays open and cleared in the pass it closes (OK, Cancel, Escape or
a click outside); the picker also consumes the Escape it handles.

Test: the_escape_after_the_picker_closes_cancels_the_editor presses Escape
to close the picker and again in the very next pass, with no settling
frames: the editor cancels. It fails on the previous marking.
2026-10-11 08:29:54 -04:00
tylerjenningsw 42d8120147 Settings: allow custom UI scale percentages (#682)
Settings > Appearance > UI Scale offered only 75, 100, 125, 150, 175 and
200 %, so a size between 75 and 100 wasn't possible. It is now the shared
numeric field (Kind::Int(75, 200, "%")): type or drag any whole
percentage. The engine already stored the scale as a clamped integer and
the UI applies it with set_zoom_factor, so the range, the default, live
preview, Cancel and saved preferences are unchanged; the preset list
goes.

Numeric settings now apply a typed number once it is entered (Enter or
leaving the field), not at every keystroke: typing 85 used to apply the
partial 8, clamped to 75, and resize the whole interface before the 5.
Dragging still applies as it moves.

Tests: the UI Scale test types 85 a key at a time (nothing applies and
the field keeps the keyboard until Enter), Cancel, 50 and 300 clamp to
75 and 200, 90 with OK, and a new app with the saved preferences starts
at 90 %. The GPU Information transaction test types 125.
2026-10-10 23:47:27 -04:00
tylerjenningsw 5c1484b6e7 Shapes: the Gradient Editor uses the shared colour picker; Escape closes only the picker (#612)
The stop colour now uses widgets::srgb_color_button, the After Effects-style
picker every colour input opens (#638): hex field, HSB/RGB fields, OK and
Cancel, sRGB as the project stores colours. The stop keeps its alpha.

The dialog Escape handler only stepped aside for egui popups, and this
picker isn't one, so in a modal dialog Escape with the picker open closed
the dialog too. The picker now marks the pass it was open in
(color_picker::open_recently), and the handler leaves Escape to it: Escape
restores the colour the picker opened with and closes only the picker.

Tests: ui_gradient_editor clicks inside the picker's colour field over the
modal editor, types a hex code, closes the picker by clicking outside it
and saves the stop; a new test checks that Escape and the picker's Cancel
restore the stop's colour and keep the editor and its other edits. That
test fails without the Escape change. ui_color_hex still passes.
2026-10-10 23:43:40 -04:00
tylerjenningsw ec1bc8cc99 Shapes: edit gradient color and opacity stops (#612)
The Timeline's "Edit Gradient…" on a gradient property (Gradient Fill /
Gradient Stroke ▸ Colors, Gradient Overlay) only painted its label. It now
opens a Gradient Editor (prop.editGradient): opacity stops above a preview
bar and colour stops below. Click to add a stop (it takes that track's
own value there), drag or type a Location to move it, set its opacity or
colour (egui's colour picker), Delete removes it (never the last of a
track, never while typing). The tracks stay in order while editing and the
selection follows its stop.

The editor works on a draft. OK sets the property once with prop.set at
the composition time it opened, snapped to the frame, with
timeBase: "comp" (offset and stretched layers get the key where it was
opened): one undo step, and nothing at all when the draft is unchanged, so
OK adds no keyframe. Cancel and Escape change nothing; Escape with the
picker open closes only the picker. On an animated gradient only the
keyframe at that time changes; when its stop counts no longer match a
neighbouring keyframe's (gradients only blend with equal counts) the
editor says so. The draft is saved in the typed form, so one colour stop
and each stop's RGBA survive.

prop.editGradient checks its target is a gradient property before the UI
opens anything, so agents get an error for a wrong one. New Gradient
color_at / opacity_at sample one track each (sample() multiplies opacity
in, which would apply it twice to a new stop).

Tests: ui_gradient_value (fails on main: the value had no automation id),
ui_gradient_editor (11), gradient_editor and keyframe unit tests.
2026-10-10 22:22:13 -04:00
tylerjenningsw 75cc264ccf Lint: clippy's collapsible_match passes on Rust 1.95
cargo +1.95 clippy --workspace --all-targets --release -- -D warnings
stopped on seven `if` blocks that clippy wants folded into their match
arm (psd, text, effects x2, pdf x2, render). Each becomes a match guard.
The behaviour is unchanged: when a guard is false the value falls through
to arms that can't match it and then to `_ => {}`. The one exception is
the PDF PostScript calculator, where a `{` past the nesting limit would
fall through to the operator arm; it keeps its own no-op arm, so it is
still skipped rather than pushed as an operator.
2026-10-10 16:54:16 -04:00
tylerjenningsw 8cb8a416ac Packaging: the legacy-folder fallback covers every release before this installer change (#466)
v0.7.0 has no App Paths Path value either, so the comments no longer
name a release.

(cherry picked from commit 4ae87f1e83)
2026-10-10 16:29:06 +00:00
tylerjenningsw 4359899f7a Project: a Cut is read with the modifiers held where it is in the frame (#484)
Cause: clipboard events carry no modifiers, and the Cut and Copy/Paste
mapping read i.modifiers, egui's state after all of the frame's events.
A Shift+Delete with Shift released in the same frame became Ctrl+X (no
deletion), and a Ctrl+X followed in the same frame by Ctrl up and Shift
down became Shift+Delete: the selected items and the layers using them
deleted without the prompt.

Fix: walk the frame's events in order from the modifiers held when it
began (the state the last pass ended with, recorded each pass), so each
Cut, Copy and Paste uses the modifiers at its place in the stream. When
that state isn't known (a pass was skipped), a Cut is never Shift+Delete.
Only Shift alone counts, not Ctrl/Alt+Shift.

Tests: a_cut_is_read_with_the_modifiers_where_it_is_in_the_frame queues
modifier changes and a Cut in one frame (Shift then Cut and Shift up;
Ctrl, Cut, Ctrl up, Shift down; and the mixed orders); the Windows
Shift+Delete helper now sends Shift, Cut and Shift up as one frame. They
fail on the previous mapping and pass now.
2026-10-09 19:55:48 -04:00
tylerjenningsw 80a23372df Docs: gaps.md records the #484 Shift+Delete tests 2026-10-09 16:27:50 -04:00
tylerjenningsw a65d70f10e Project: Shift+Delete deletes the selected items without asking (#484)
With the Project panel focused, Delete Project Items Without Confirmation
runs project.delete on the selection: items compositions use go without
the prompt, with their layers, in one undo step. In the other panels it
does nothing, and like every shortcut it doesn't fire while typing or
with a dialog open.

On Windows the windowing layer sends Shift+Delete as Cut (the old Cut
key), which was mapped to Ctrl+X: in the Project panel, which has no
clipboard, a Cut with only Shift held is Shift+Delete now. Elsewhere it
still cuts.

Tests (ui_project_delete): Shift+Delete as a key and as Windows' Cut
deletes a comp, a solid in use, footage and a folder without the prompt
and one undo restores them; a multi-selection with a folder holding a
solid in use goes together; an empty selection deletes nothing; the next
Delete still asks after Shift+Delete or an answered prompt; it does
nothing in the Composition panel, while renaming or with a dialog open;
Shift+Backspace, Alt/Ctrl+Delete and Ctrl/Alt+Shift+Delete don't delete.
The deleting tests fail without this change.
2026-10-09 16:27:48 -04:00
tylerjenningsw 01ba8a85e9 Project: answering the Delete Items prompt confirms that deletion only (#484)
The prompt's Delete set a flag that let the next project.delete skip the
prompt. It now runs project.delete directly (delete_items::delete_confirmed),
so the confirmation goes with that request and can't carry over to a later
deletion. Shift+Delete uses the same path.
2026-10-09 16:27:21 -04:00
tylerjenningsw f59acb5b80 Project: Delete Project Items Without Confirmation is a command, on Shift+Delete (#484)
A frontend command (project.deleteWithoutConfirmation) so the shortcut is
listed and rebindable in Keyboard Shortcuts and runs by id over the
control channel and MCP. The UI runs project.delete for it.
2026-10-09 16:24:53 -04:00
tylerjenningsw 4aea350f2a Packaging: the installer's Repair puts back a damaged effectcraft-cli.exe (#466)
Cause: Repair used REINSTALLMODE=ecmus. effectcraft-cli.exe has no version
resource, so Windows Installer treats it as unversioned: a changed copy
with a later write time counts as a user edit and is kept, and "c" only
checks files that carry an MSI checksum, which these don't.

Fix: Repair uses "amus", which puts back every file whatever its version
or date.

Tests: test-msi.ps1's stand-in CLI is unversioned like the real one; the
test damages it in place and repairs with the REINSTALLMODE read from
installer-ui.wxs (no longer msiexec /fa after deleting it), then compares
contents. It fails with ecmus (the damaged file stays) and passes now.
2026-10-09 14:49:19 -04:00
tylerjenningsw 462913a483 Packaging: upgrading a custom-folder install from 0.6.0 or earlier keeps its folder (#466)
Cause: the folder search read only the App Paths "Path" value, which this
branch introduced. 0.6.0 and earlier could already be installed elsewhere
with INSTALLFOLDER=..., but have no Path value, so an upgrade went to
Program Files while the wizard said it would stay in the same folder.

Fix: when there is no Path value, a second search reads the App Paths
default value (the full path of effectcraft.exe, written by every
release). A "file" registry search with no FileSearch inside yields the
folder of that file.

Tests: test-msi.ps1 installs a legacy package (the same .wxs without the
Path value) into a custom folder, then upgrades it without naming a
folder; it fails on the previous .wxs (the upgrade lands in Program
Files) and passes now. A further upgrade checks the Path value route.
2026-10-09 14:45:47 -04:00
tylerjenningsw 184df34875 CI: the MSI test reads Apps entries one value at a time (#466)
Get-ItemProperty throws on a malformed Uninstall entry, which aborted the
upgrade check on a real Windows 10 machine. Failures now print their message.
2026-10-09 14:11:53 -04:00
tylerjenningsw 2412173b7e Contributors: tylerjenningsw's display name is tylerjenningsw 2026-10-09 14:05:49 -04:00
tylerjenningsw 6c58eea643 Contributors: credit tylerjenningsw as Tyler Jennings 2026-10-09 14:05:18 -04:00
tylerjenningsw 5d58fd4029 Docs: releasing.md describes the MSI install folder and its test (#466) 2026-10-09 14:00:46 -04:00
tylerjenningsw 7dc2b7a381 README Downloads: the Windows installer lets you pick the folder (#466) 2026-10-09 14:00:29 -04:00
tylerjenningsw f2d335b138 CI: the MSI is install-tested with a custom folder, upgrade, repair and uninstall (#466)
test-msi.ps1 builds the MSI around stub binaries, validates it (ICE) and,
silently: installs into a folder with spaces and a non-ASCII name, upgrades
without naming a folder (it must stay there, one entry in Apps), repairs a
deleted file, uninstalls (files and App Paths gone), and installs to the
Program Files default. Run by a windows-latest job in packaging-lint.yml.
2026-10-09 14:00:09 -04:00
tylerjenningsw 9ceda6a8f5 CI: packaging lint checks every Windows .wxs file (#466) 2026-10-09 13:59:46 -04:00
tylerjenningsw b32965f209 Packaging: the Windows installer asks where to install EffectCraft (#466)
A fresh install opens on a destination page (Program Files by default;
Change... browses, with Up and New folder), then a Ready page that shows
the folder before anything is written. An upgrade goes straight to the
Ready page with the installed copy's folder and can't move it; opening the
MSI again offers Repair or Remove. Cancel, finish, files-in-use and error
dialogs are authored too. Silent installs (/qn) show none of it.

The dialogs (installer-ui.wxs) are original, built from Windows Installer's
own controls and events, not the WixUI library, whose MS-RL licence is not
on the allowed list. package.ps1 builds both .wxs files.
2026-10-09 13:58:35 -04:00
tylerjenningsw 8b8a57780f Packaging: Windows upgrades and repairs install into the existing folder (#466)
The MSI records its folder as the App Paths "Path" value of effectcraft.exe
and reads it back with a registry search, so an upgrade, a repair or a
silent reinstall goes where the installed copy already is. A folder passed
on the command line (INSTALLFOLDER=...) still wins, and INSTALLFOLDER is
Secure so a folder chosen in the UI reaches the elevated install. v0.6.0 and
earlier have no Path value and upgrade in Program Files, where they are.
2026-10-09 13:51:44 -04:00