Skip to content

feat(gui): animation, Preferences to the Colony convention, and the 57 shared palettes - #41

Merged
MotherSphere merged 5 commits into
mainfrom
feat/motion
Aug 26, 2026
Merged

feat(gui): animation, Preferences to the Colony convention, and the 57 shared palettes#41
MotherSphere merged 5 commits into
mainfrom
feat/motion

Conversation

@MotherSphere

@MotherSphere MotherSphere commented Aug 25, 2026

Copy link
Copy Markdown
Member

Stacked on #40 - merge that first and this retargets to main on its own.

Two commits. The second moves the setting the first introduced, which is why they travel together.


1. Animation

Three rules, and the first is what makes it affordable.

Nothing animated changes layout. Every animation interpolates a colour. A frame in flight never re-measures a row, never reflows the mod list and never touches the file tree - which matters, because a fully expanded Skyrim Data tree is six figures of rows. A pleasant consequence: turning motion off changes no spacing anywhere.

The clock only runs while something is moving. anim::animating gates the frame subscription, so an idle window subscribes to no timer at all. The cost at rest is not small, it is zero - the same condition the drag-scroll, hover-to-expand and run-poll timers already use.

A phase ends by itself. It derives everything from the instant it began, so nothing has to remember to switch it off, and a missed frame is a jump forward rather than an animation stuck half way.

What moves

The two tab strips cross-fade. Both ends of the blend come from iced's own button::primary and button::secondary evaluated against the live theme, so no colour is named and it stays correct on the dark palette and on any palette added later. The tab being left runs the transition backwards - without that the arriving tab fades in while the old one snaps, which reads as a glitch.

A new status message fades in, armed by comparing status after each message rather than at the dozen places across state.rs that assign it. "Remember to also start the fade" is a rule that would be forgotten by the second one.

The trap this window has fallen into twice

AnimationTick is ambient. It fires sixty times a second, so treating it as an action would not shorten an armed two-click confirmation's life - it would end it between the two clicks, every time. The pointer taught this window that once and the download tick taught it again. It joins the set assertion that exists to catch the next timer somebody adds.


2. Preferences, measured against the convention

Checked against design/settings-page.md in Project-Colony-Resources. The page broke the first structural rule and was missing one of the three mandatory categories.

It was a modal, which is the one thing it must not be

The convention names a modal, a separate window and a popover as the three counter-examples. This was the first.

It is a page now: it replaces the content area, and the header, menu bar, toolbar and status bar all stay where they are - the chrome does not move because the user went to configure it.

What that buys is not tidiness. The card was capped at 620x240, so every category was read through a 240-pixel slot however large the window was. The screen that most needs room to be scanned had the least of it.

Accessibility was missing, and it is not optional

The Accessibility category is part of the skeleton, not a nice-to-have.

The first three categories are imposed and ordered - General, Appearance, Accessibility. This had five, with the third absent.

Motion moves there from Appearance, where commit 1 had put it. Not filing: Appearance is what the window looks like, Accessibility is what it does for somebody who needs it to do less.

The convention also asks for a high-contrast palette, a dyslexia-friendly font and a text scale. Eidos has none of the three - every size in this window is a literal, so a scale is a change to the whole GUI rather than a setting. The category says so in as many words rather than showing switches that do nothing.

The rest of the checklist

before now
title "Settings", 18 "Preferences", 22
shape modal on a scrim, 620x240 page, fills the content area
categories 5, no Accessibility 6, imposed three first
category heading none heading + one line of consequence
the no-Save contract unstated "Preferences are saved automatically."
rail [6, 10], 132 wide [8, 14], 148 - fits "Accessibility"
function name settings_dialog preferences_page

Two deliberate deviations, both about scale

The convention's chevrons are Nerd Font codepoints. Eidos bundles no Nerd Font - it uses PNG icons - so those would render as tofu. The existing triangles stay.

Section titles are 14, not 15. That number assumes Colony's type scale, whose body text is 13 where Eidos's is 12; 15 would leave a section title as loud as the category heading above it. What the convention specifies is the hierarchy, and this keeps it: 22 / 16 / 14 / 12.


tests 278 GUI (25 new), 138 elsewhere
clippy clean, pinned 1.94.1
timers when idle 0
layout moved by animation none

Six of the new tests state the convention itself, so a later refactor breaks loudly: the imposed three and their order, About last, every category explaining itself in words it does not repeat, General carrying the no-Save contract, and motion filed under Accessibility. The rank table is an exhaustive match - adding a category without placing it does not compile.

Not verified: the appearance. I have not opened the window. The blend is asserted at its endpoints and midpoint, and the page structure by its data, not looked at.

The panes were a fixed 3:2, which is a guess about somebody else's screen. On a
narrow window the right pane's tab strip runs out of room and clips the last tab
- "Diagnostics (2)" was the one that prompted this - and there was nothing to do
about it: the space between the lists was a gap, not a handle.

It is a handle now. Six pixels, full height, the toolbars' muted line at rest
and the panel border's burgundy while held, so a drag in progress is visible
rather than inferred.

The ratio is clamped to 0.15..0.85 in both the live drag and the settings
parser. A pane dragged to zero takes the divider with it, and then neither pane
can be recovered - so an out-of-range value in `settings.ini` reads as the
default rather than as a preference somebody chose, the same policy
`drag_scroll_speed` already applies.

It is written to `settings.ini` on RELEASE, not on every pointer move: the drag
emits a message per frame and the settings are a file.

Nothing new was needed to carry it. `app.cursor` and `app.window` were already
tracked, `mouse_area` was already imported, and `PointerAt` was already ambient -
which matters, because resizing the furniture must not disarm a two-click
confirmation armed on a row.

Tested through the real message loop: the pointer alone does not resize, a grab
makes it follow, both clamps hold at the edges, release stops the drag and hands
the value to the preferences, and a move after release is inert again.
Three rules hold this together, and the first is what makes it affordable.

**Nothing animated changes layout.** Every animation here interpolates a colour.
A frame in flight never re-measures a row, never reflows the mod list and never
touches the file tree - which matters, because a fully expanded Skyrim Data tree
is six figures of rows and re-laying it out sixty times a second would be a
stutter, not a flourish. Turning motion off therefore changes no spacing at all.

**The clock only runs while something is moving.** `anim::animating` gates the
frame subscription, so an idle window subscribes to no timer. The cost at rest
is not small, it is zero - the same condition the drag-scroll, hover-expand and
run-poll timers already use.

**A phase ends by itself.** `Phase` holds the instant a transition began and
derives everything from elapsed time, so nothing has to remember to switch it
off, and a missed frame shows up as a jump forward rather than as an animation
stuck half way.

## What moves

The two tab strips cross-fade. Both ends of the blend come from iced's own
`button::primary` and `button::secondary`, evaluated against the live theme, so
no colour is named and the transition stays right on the dark palette and on any
palette added later. The tab being LEFT runs the same transition backwards -
without that the arriving tab fades in while the old one snaps, which reads as a
glitch rather than as a transition.

A new status message fades in, so a status that replaces another is visibly a
new one rather than a word that quietly became a different word. It is armed by
comparing `status` after each message rather than at the dozen places across
`state.rs` that assign it: "remember to also start the fade" is a rule that
would be forgotten by the second one.

## Motion is a setting, because it has to be

`motion` in `settings.ini`, on unless explicitly off, with the toggle under
Preferences -> Appearance. Off means every animated value is drawn at its
destination on the first frame - not a quicker animation, none - and the frame
timer is never subscribed. The Colony convention requires that a program which
animates can be told not to; this is that.

`AnimationTick` is ambient. It fires sixty times a second, so treating it as an
action would not merely shorten an armed two-click confirmation's life, it would
end it between the two clicks every time - the defect the pointer and the
download tick already taught this window, at sixty times the rate. It joins the
set assertion that exists to catch the next timer somebody adds.

273 GUI tests, 138 elsewhere, clippy clean on the pinned 1.94.1.
Measured against `design/settings-page.md` in Project-Colony-Resources, this
page broke the convention's first structural rule and was missing one of its
three mandatory categories.

## It was a modal, which is the one thing it must not be

The convention names a modal, a separate window and a popover as the three
counter-examples, and this was the first. It is a page now: it replaces the
content area, and the header, menu bar, toolbar and status bar all stay exactly
where they are - the program's chrome does not move because the user went to
configure it.

What that buys is not tidiness. The card was capped at 620x240, so every
category was read through a 240-pixel slot however large the window was. The
screen that most needs room to be scanned had the least of it.

## Accessibility was missing, and it is not optional

"The Accessibility category is part of the skeleton, not a nice-to-have." The
first three categories are imposed and ordered - General, Appearance,
Accessibility - and this had five with the third absent.

Motion moves there from Appearance, where the previous commit had put it. The
distinction is not filing: Appearance is what the window looks like,
Accessibility is what it does for somebody who needs it to do less.

The convention also asks for a high-contrast palette, a dyslexia-friendly font
and a text scale. Eidos has none of the three - every size in this window is a
literal, so a scale is a change to the whole GUI rather than a setting. The
category says so in as many words rather than showing switches that do nothing.

## The rest of the checklist

- Titled **Preferences**, at 22, not "Settings" at 18. The convention settles the
  user-facing word; only the code is still named `settings_*`, which costs the
  user nothing.
- `settings_dialog` is now `preferences_page`: a function whose name disagrees
  with what it returns is how the next reader is misled.
- Each category opens with its own heading and one line saying what it changes.
  General's carries the contract that matters most: preferences are saved
  automatically. There is no Save button and there must not be one.
- Close at 13, padding [6, 14]; rail buttons at [8, 14], widened to 148 so
  "Accessibility" does not resize the column.
- Descriptions in a muted ink, so a category reads as titles with explanations
  rather than as two columns of equally loud text.

## Two deliberate deviations, both about scale

The convention's chevrons are Nerd Font codepoints. Eidos bundles no Nerd Font -
it uses PNG icons - so those would render as tofu; the existing triangles stay.

Section titles are 14, not the convention's 15. That number assumes Colony's
type scale, whose body text is 13 where Eidos's is 12, and 15 would leave a
section title as loud as the category heading above it. What the convention
specifies is the hierarchy - page, category, section, body - and this keeps it,
at 22 / 16 / 14 / 12.

Six new tests state the convention where a refactor would otherwise break it
silently: the first three categories and their order, About last, every category
explaining itself in words it does not repeat, General carrying the no-Save
contract, and motion filed under Accessibility. The rank table is an exhaustive
match, so adding a category without placing it does not compile.

278 GUI tests, clippy clean on the pinned 1.94.1.
@MotherSphere MotherSphere changed the title feat(gui): animate the tab strips and the status line feat(gui): animate the window, and rebuild Preferences to the Colony convention Aug 25, 2026
The theme setting had never done anything. `theme()` ignored its `App` argument
and returned the same parchment whatever was picked, so the Light / Dark /
System list had changed no pixel since it was written. It is gone, and the key
it stored is now an unknown key an old `settings.ini` carries harmlessly.

In its place: **25 theme families, 57 palettes and the 8 accent overrides** from
the Colony catalogue, generated from the design tokens in
Project-Colony-Resources rather than copied here. Adding a family upstream needs
no arm, no picker entry and no code in this repository - only a tag bump.

## One palette, filled from one of two places

Every colour in this window now comes from one 38-field `ThemePalette`: either
`PARCHMENT` - the look Eidos has always worn, written out as those same 38
fields instead of scattered across seventy literals - or one of the catalogue's
57. `theme.rs` is the only file allowed to name a colour; everywhere else asks
the palette. A literal hex outside it is a bug, right on one theme and wrong on
fifty-seven.

The parchment stays the default and keeps its own row at the top of the picker.
An upgrade repaints nobody's window, and a picker you cannot come back through
is a trap.

## Two colours had to stop being fixed

The origin tint - a plugin belonging to the selected mod - was a fixed pale
blue. It is now the card tinted towards the accent, and the STRENGTH of the tint
is chosen rather than fixed: on several palettes the selection is itself an
accent-tinted card, and one ratio landed on top of it - on catppuccin/frappe the
two came out 0.02 apart, which is to say identical. Five strengths are measured
against the three tints this must never be confused with and the furthest wins.
A test walks all 57 palettes and fails if any pair becomes confusable.

The FOMOD installer's ink-on-burgundy was a fixed pale colour. On a palette
whose accent is light that is unreadable, so it asks `contrast_on` which side
can actually be read.

## High contrast, which the convention had been asking for

Derived from whichever theme is on rather than shipped as a twin, so it works on
the parchment and on all 57. It fills the Vision section the Accessibility
category was carrying an apology for. It moves the ink and the lines, never the
grounds - a high-contrast mode that repainted the backgrounds would be a
different theme rather than the same one read more easily, and a test says so.

## Drawn here, not by the shared widgets

`colony-ui` ships a theme picker and an accent picker. Both are built to Colony's
type scale, whose body text is 13 where this window's is 12 - a picker a size and
a half larger than the page around it. The catalogue is the valuable part and it
is shared; the hundred lines that draw it are this program's.

The accent list still comes from `ACCENT_OVERRIDES`, never a local copy: its
ORDER is load-bearing across the ecosystem.

`accent` stores a key or nothing. "Auto" is the absence of an override, never a
colour - writing a resolved colour would freeze it against the theme it came
from.

289 GUI tests, 39 suites elsewhere, clippy clean on the pinned 1.94.1. Every
literal left in the GUI is inside a `#[cfg(test)]` block.
@MotherSphere MotherSphere changed the title feat(gui): animate the window, and rebuild Preferences to the Colony convention feat(gui): animation, Preferences to the Colony convention, and the 57 shared palettes Aug 25, 2026
@MotherSphere

Copy link
Copy Markdown
Member Author

Third commit: the theme catalogue

The theme setting had never done anything. theme() ignored its App argument and returned the same parchment whatever was picked, so the Light / Dark / System list had changed no pixel since it was written. It is gone; the key it stored is now an unknown key an old settings.ini carries harmlessly.

In its place: 25 families, 57 palettes, 8 accent overrides, from the Colony catalogue - generated from the design tokens rather than copied here. Adding a family upstream needs no arm, no picker entry and no code in this repo, only a tag bump.

One palette, filled from one of two places

Every colour now comes from one 38-field ThemePalette: either PARCHMENT - the look Eidos has always worn, written out as those same 38 fields instead of scattered across ~70 literals - or one of the catalogue's 57.

theme.rs is the only file allowed to name a colour. A literal hex outside it is a bug: right on one theme, wrong on fifty-seven. Every literal left in the GUI is now inside a #[cfg(test)] block.

The parchment stays the default and keeps its own row at the top of the picker. An upgrade repaints nobody's window, and a picker you cannot come back through is a trap.

Two colours had to stop being fixed

The origin tint - a plugin belonging to the selected mod - was a fixed pale blue. It is now the card tinted toward the accent, and the strength of the tint is chosen rather than fixed: on several palettes the selection is itself an accent-tinted card, and one ratio landed on top of it. On catppuccin/frappe the two came out 0.02 apart, which is to say identical. Five strengths are measured against the three tints this must never be confused with, and the furthest wins. A test walks all 57 palettes and fails if any pair becomes confusable.

The FOMOD installer's ink-on-burgundy was a fixed pale colour. On a palette whose accent is light that is unreadable, so it now asks contrast_on which side can actually be read.

High contrast

Derived from whichever theme is on rather than shipped as a twin, so it works on the parchment and on all 57. It fills the Vision section that Accessibility was carrying an apology for.

It moves the ink and the lines, never the grounds - a high-contrast mode that repainted the backgrounds would be a different theme rather than the same one read more easily. A test says so.

Drawn here, not by the shared widgets

colony-ui ships a theme picker and an accent picker, both built to Colony's type scale - body text 13 where this window's is 12, so a picker a size and a half larger than the page around it. The catalogue is the valuable part and it is shared; the hundred lines that draw it are this program's.

The accent list still comes from ACCENT_OVERRIDES, never a local copy: its order is load-bearing across the ecosystem. And accent stores a key or nothing - "auto" is the absence of an override, never a colour, because writing a resolved colour would freeze it against the theme it came from.

tests 289 GUI, 39 suites elsewhere
clippy clean, pinned 1.94.1
palettes covered by a test 57 / 57
hardcoded colours outside theme.rs 0 (excluding tests)

Still not verified: the appearance. I have not opened the window.

@MotherSphere
MotherSphere deleted the branch main August 26, 2026 09:02
@MotherSphere MotherSphere reopened this Aug 26, 2026
@MotherSphere
MotherSphere changed the base branch from feat/resizable-panes to main August 26, 2026 09:03
# Conflicts:
#	crates/eidos-gui/src/main.rs
#	crates/eidos-gui/src/modinfo.rs
#	crates/eidos-gui/src/state.rs
#	crates/eidos-gui/src/theme.rs
#	crates/eidos-gui/src/update.rs
#	crates/eidos-instance/src/settings.rs
@MotherSphere
MotherSphere merged commit 7f71b25 into main Aug 26, 2026
6 of 7 checks passed
@MotherSphere
MotherSphere deleted the feat/motion branch August 26, 2026 09:09
@github-actions github-actions Bot mentioned this pull request Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant