feat(config,client): OS reduced-motion detection with tri-state animations.enabled (UI/UX v3 P3c) - #84
Merged
Merged
Conversation
animations.enabled を tri-state 化し、OS 由来の値は #[serde(skip)] の 内部フラグとして AnimationsConfig に持たせる。これで約 70 箇所の 読み出しと全アニメーションのコードを一切変えずに reduced motion が 効く。計画書が古くなっていた点(macOS は P2 の objc2 判断を共有、 という記述)も訂正した。
5 タスク(config の 3 値化 → OS 検出 → 反映 → 設定パネル → 文書)。 計画中に見つけた罠を明記した: config のホットリロードは Config を 作り直すため、OS 状態を引き継がないと config.toml を編集した瞬間に アニメーションが復活する。
…flag auto / true / false の 3 値にし、OS 由来の状態は #[serde(skip)] の 非公開フィールドとして持つ。乗数の計算は AnimationsConfig 内に 閉じているため、約 70 箇所の読み出しとアニメーション側のコードは 変更不要。既存の enabled = true / false もそのまま読める。
Windows は既存の windows-sys で SPI_GETCLIENTAREAANIMATION を読む (追加依存なし)。macOS は objc2 コアのみで NSWorkspace を叩き、 objc2-app-kit のサブツリーは入れない。Linux は None を返す。 判定不能は「抑制なし」として扱うため、検出失敗が動きを勝手に 止めることはない。
ネイティブの変更通知は使わず、フォーカス獲得時に読み直す。 「システム設定を開いて変更 → 戻ってくる」という実際の操作を そのまま拾えるうえ、両 OS で同じ形になる。config のホット リロードは新しい Config を作り直すため、OS 状態の引き継ぎを 明示的に行う。
auto / on / off を設定パネルから選べるようにした。auto は 現在どちらに解決されているか(通常 / 動きを抑制)を併記する。 併記しないと、OS が抑制を要求している環境で行の表示が 実態と食い違うため。8 ロケール全部に文字列を追加した。
window_widget_descs(AccessKitと矢印キーナビゲーションが直接読む共有ディスクリプタ経路) がauto行のOS reduced-motion値をfalse固定にしていたため、OSが縮小モーションを要求していても スクリーンリーダーが「Auto (normal)」と読み上げてしまう不具合を修正。 SettingsPanelにanimations_os_reduced(AnimationsConfig::os_reduced_motion()のミラー、 animations_enabledと同じパターン)を追加し、set_os_reduced_motionを呼ぶ全箇所 (起動時サンプリング・フォーカス取得時の再サンプリング・ホットリロード時の引き継ぎ)で 同期する。settings_window.rsのkind()はこのフィールドを直接読むようになったため、 build_window_widgets側の後付けパッチとそれ専用のanimations_os_reducedパラメータ (呼び出し元まで連鎖していた)を削除した。 回帰テストとしてanimations_row_label_reflects_the_panels_os_reduced_fieldを追加。 window_widget_descsを直接呼び、修正前は失敗することを確認済み。
CONFIGURATION.md に 3 値の意味と「検出は無効化方向にしか働かない」 「OS 由来の値は config.toml に書き戻さない」を明記。計画書の P3c を 完了にし、macOS の依存判断が P2 から独立したことも記録した。
3つのテストが SettingsPanel::default() 直後に animations_enabled = Auto を 代入していたが、AnimationsEnabled のデフォルトは既に Auto であり冗長な代入 だった。cargo clippy --workspace --all-targets --all-features -- -D warnings がこの3箇所でエラーになっていたため削除。
Coverage reportGenerated by |
Windows/macOS の CI が needless_return で失敗していた。1 つの関数本体に 複数の #[cfg] ブロックを収めていたため、Linux では最後のブロックだけが 残り return を含まないが、Windows/macOS では自プラットフォームの return が関数末尾に残ってしまい clippy に検出された。Linux 単独の ローカル検証では再現しない構造だったので、#[cfg] ごとに独立した関数 定義へ分割し、各 tail 式から return を除去して構造的に解消した。
Coverage reportGenerated by |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
UI/UX v3 P3c is the switch that turns the motion language off for people who asked their OS to stop moving things. P3a built the timing primitives (#77, #78), P3b1–P3b3 applied them to surfaces, hover and press (#79, #80, #82, #83); this makes all of it respect the OS accessibility preference.
Design:
docs/superpowers/specs/2026-08-29-p3c-reduced-motion-design.mdPlan:
docs/superpowers/plans/2026-08-29-p3c-reduced-motion.mdThe shape, and why no animation code changed
animations.enabledbecomes tri-state andAnimationsConfigcarries the OS state itself:effective_multiplier()returns 0 forNo, or forAutowhen the OS flag is set. Every animation in this client — surface open/close, hover cross-fade, press pulse, tab accent — reaches its duration throughscaled_duration_ms, so all ~70 call sites and every animation type are untouched. The switch lives in one place because that is where the multiplier already lived.Three properties are structural rather than maintained by care:
os_reduced_motionto a larger multiplier.Nonefrom the platform layer means "cannot tell" and is read as "not reduced".config.toml. The field is private and#[serde(skip)]; the settings panel writes the file back throughtoml_edit, and a serializable OS value would persist a setting the user never chose. The privacy also makesAnimationsConfig { .. ..Default::default() }illegal outside the crate, which is how we know the seal holds.enabled = truemeans "animate anyway" — an OS-wide preference is not always what someone wants inside a terminal.enabled = falsestill means never. Pre-P3c configs keep parsing; the default changes fromtruetoauto, which is the point of the phase and only ever removes motion.Detection
SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION)— reports whether animations are enabled, so reduced motion is its negationwindows-sysalready present, feature already enabled)NSWorkspace.accessibilityDisplayShouldReduceMotionobjc2core only — deliberately not theobjc2-app-kitsubtree for one BOOLSampled at startup and on
WindowEvent::Focused(true). Focus-gain rather than a native change notification because it matches what the user does — open System Settings, change the preference, come back — and needs no observer machinery on either platform.ThemeChangedis the existing precedent for reacting to an OS preference here.Correction to the v3 plan: it said macOS detection would "share the objc2 dependency decision with P2". That was stale — P2c added
window-vibrancy, which does not expose this preference. P3c made its own call.Two traps this phase had to be built around
A config hot-reload silently un-does it. Reloading builds a fresh
Configwhose OS flag starts unset, so savingconfig.tomlwould have restored animations the OS asked us to stop. The sampled value is now carried across explicitly (lifecycle.rs, before the new config replaces the old).The settings row was correct on screen and wrong to a screen reader. The
autovalue shows how it resolves — "Auto (normal)" / "Auto (reduced)" — because a row reading just "auto" implies animations might be on when they are all off. Butaccessibility.rsand the keyboard-navigation path call the shared widget-description path directly, bypassing the render path where the real value was patched in, so AccessKit announced "Auto (normal)" under OS reduced motion. On a feature that exists for accessibility, being wrong for exactly that user is the defect that matters most. Fixed by givingSettingsPanelits own mirror of the sampled value, stamped at the same three sites, so every consumer reads one source.Test coverage
nexterm-config— the truth table (Auto+ OS reduced → 0;Yesoverrules the OS;Nostays 0 whatever the OS says), backward-compatible parsing of the old booleans, the default, and that the OS flag never serializes.nexterm-client-gpu— the settings row cycles both directions, theautolabel distinguishes its two resolutions, each state writes back its own TOML spelling, and the shared descriptor path (the one AccessKit reads) reports the right label.nexterm-client-gpu;cargo test --workspacegreen;cargo clippy --workspace --all-targets --all-features -- -D warningsandcargo fmt --all -- --checkclean.platform::reduced_motion()itself has no unit test, deliberately: it is one FFI call per platform with no branching worth pinning, and CI cannot set an OS accessibility preference. Keeping it thin is what makes that acceptable — every decision lives above it, where the tests are.Known limitations
autoanimates there; the manual setting stays the documented fallback. GNOME'senable-animationsand the XDG settings portal are both plausible later.objc2's runtime class lookup findingNSWorkspace, which holds because winit andwindow-vibrancylink AppKit. If that turns out false, the fallback isobjc2-app-kit— a dependency swap, not a redesign.Test plan
cargo test --workspacecargo clippy --workspace --all-targets --all-features -- -D warningscargo fmt --all -- --checkenabled = truewith the OS preference on, confirm animations still runconfig.tomland confirm animations stay off (the hot-reload carry-over)