From 01efc9bafeae7dd75f5f6f04a61b91ea68b47ef1 Mon Sep 17 00:00:00 2001 From: devmobasa <4170275+devmobasa@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:14:05 +0200 Subject: [PATCH] fix: allow disabling toolbar idle fade Add an accessible opt-out across config, runtime preferences, and both settings surfaces while preserving the existing default behavior. --- config.example.toml | 4 ++ configurator/src/app/pages/ui/toolbar.rs | 6 +++ configurator/src/app/search/terms.rs | 5 +++ .../src/models/config/draft/from_config.rs | 1 + configurator/src/models/config/draft/mod.rs | 1 + configurator/src/models/config/setters.rs | 1 + configurator/src/models/config/tests.rs | 14 +++++++ .../src/models/config/to_config/ui.rs | 1 + configurator/src/models/fields/toggles.rs | 1 + docs/CONFIG.md | 10 +++-- .../wayland/backend/state_init/input_state.rs | 1 + .../wayland/runtime_ui_state/live_state.rs | 5 +++ .../wayland/runtime_ui_state/rollback.rs | 1 + src/backend/wayland/runtime_ui_state/seeds.rs | 8 ++++ .../tests/preference_actions.rs | 5 +++ .../wayland/state/toolbar/events/tests.rs | 2 + src/backend/wayland/state/toolbar/fade.rs | 1 + src/config/tests/load.rs | 17 +++++++++ src/config/types/toolbar/config.rs | 9 +++++ src/input/state/core/base/state/init.rs | 1 + src/input/state/core/base/state/structs.rs | 2 + src/input/state/core/tool_controls/toolbar.rs | 3 ++ src/input/state/tests/toolbar_display.rs | 4 ++ src/runtime_ui_state/types.rs | 2 + src/runtime_ui_state/wire/v1.rs | 8 +++- .../view/sections/settings_pane.rs | 1 + src/ui/toolbar/apply/layout.rs | 9 +++++ src/ui/toolbar/apply/mod.rs | 1 + src/ui/toolbar/events.rs | 2 + src/ui/toolbar/model/activation.rs | 1 + src/ui/toolbar/model/event_policy.rs | 5 +++ src/ui/toolbar/model/settings.rs | 7 ++++ src/ui/toolbar/snapshot/build.rs | 1 + src/ui/toolbar/snapshot/fade.rs | 38 ++++++++++++++++++- src/ui/toolbar/snapshot/types.rs | 2 + 35 files changed, 175 insertions(+), 5 deletions(-) diff --git a/config.example.toml b/config.example.toml index 9014d1b4..16991c90 100644 --- a/config.example.toml +++ b/config.example.toml @@ -620,6 +620,10 @@ context_aware_ui = true # Show preset action toast notifications on apply/save/clear show_preset_toasts = true +# Dim the top strip after ~4 seconds without drawing. Set false to keep +# the bar fully visible (accessibility). +idle_fade = true + # Show cursor tool preview bubble near the pointer show_tool_preview = false diff --git a/configurator/src/app/pages/ui/toolbar.rs b/configurator/src/app/pages/ui/toolbar.rs index aecae8ad..c11f51aa 100644 --- a/configurator/src/app/pages/ui/toolbar.rs +++ b/configurator/src/app/pages/ui/toolbar.rs @@ -69,6 +69,12 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { "", |app| app.draft.ui_toolbar_use_icons, |value| Message::ToggleChanged(ToggleField::UiToolbarUseIcons, value), + ) + .switch_row( + "Dim toolbar when idle", + "Fade the top bar to 55% after a few seconds without drawing. Turn off to keep it fully visible.", + |app| app.draft.ui_toolbar_idle_fade, + |value| Message::ToggleChanged(ToggleField::UiToolbarIdleFade, value), ); page.group("Sections") diff --git a/configurator/src/app/search/terms.rs b/configurator/src/app/search/terms.rs index 9e5cba1a..29cf387d 100644 --- a/configurator/src/app/search/terms.rs +++ b/configurator/src/app/search/terms.rs @@ -209,6 +209,11 @@ pub(super) const UI_TOOLBAR_TERMS: &[&str] = &[ "show marker opacity controls", "show tool preview bubble", "show preset action toasts", + "dim toolbar when idle", + "idle fade", + "toolbar fade", + "toolbar dim", + "keep toolbar visible", "force inline toolbars", "mode overrides", "edit mode", diff --git a/configurator/src/models/config/draft/from_config.rs b/configurator/src/models/config/draft/from_config.rs index c7e7b4a3..ba4c4a38 100644 --- a/configurator/src/models/config/draft/from_config.rs +++ b/configurator/src/models/config/draft/from_config.rs @@ -149,6 +149,7 @@ impl ConfigDraft { ui_toolbar_use_icons: config.ui.toolbar.use_icons, ui_toolbar_show_more_colors: config.ui.toolbar.show_more_colors, ui_toolbar_show_preset_toasts: config.ui.toolbar.show_preset_toasts, + ui_toolbar_idle_fade: config.ui.toolbar.idle_fade, ui_toolbar_layout_mode: ToolbarLayoutModeOption::from_mode( config.ui.toolbar.layout_mode, ), diff --git a/configurator/src/models/config/draft/mod.rs b/configurator/src/models/config/draft/mod.rs index ad6d9a0f..fc17663e 100644 --- a/configurator/src/models/config/draft/mod.rs +++ b/configurator/src/models/config/draft/mod.rs @@ -103,6 +103,7 @@ pub struct ConfigDraft { pub ui_toolbar_use_icons: bool, pub ui_toolbar_show_more_colors: bool, pub ui_toolbar_show_preset_toasts: bool, + pub ui_toolbar_idle_fade: bool, pub ui_toolbar_layout_mode: ToolbarLayoutModeOption, pub ui_toolbar_zoom_chip_display: ZoomChipDisplayOption, pub ui_toolbar_show_zoom_chip: bool, diff --git a/configurator/src/models/config/setters.rs b/configurator/src/models/config/setters.rs index 3a08cfbc..190f4a10 100644 --- a/configurator/src/models/config/setters.rs +++ b/configurator/src/models/config/setters.rs @@ -190,6 +190,7 @@ impl ConfigDraft { ToggleField::UiToolbarUseIcons => self.ui_toolbar_use_icons = value, ToggleField::UiToolbarShowMoreColors => self.ui_toolbar_show_more_colors = value, ToggleField::UiToolbarPresetToasts => self.ui_toolbar_show_preset_toasts = value, + ToggleField::UiToolbarIdleFade => self.ui_toolbar_idle_fade = value, ToggleField::UiToolbarShowPresets => { self.set_toolbar_section_visible(ToolbarSectionFlag::Presets, value); } diff --git a/configurator/src/models/config/tests.rs b/configurator/src/models/config/tests.rs index c6d1930b..b69b28b9 100644 --- a/configurator/src/models/config/tests.rs +++ b/configurator/src/models/config/tests.rs @@ -1335,6 +1335,20 @@ fn config_draft_round_trips_ui_reduced_motion() { assert_eq!(round_trip.ui.reduced_motion, ReducedMotion::On); } +#[test] +fn config_draft_round_trips_ui_toolbar_idle_fade() { + let mut config = Config::default(); + config.ui.toolbar.idle_fade = false; + + let draft = ConfigDraft::from_config(&config); + assert!(!draft.ui_toolbar_idle_fade); + + let round_trip = draft + .to_config(&config) + .expect("expected config to round trip"); + assert!(!round_trip.ui.toolbar.idle_fade); +} + /// The revision is provenance for a review the user went through, so a draft /// that never applied a migration must not stamp one on an old file. #[test] diff --git a/configurator/src/models/config/to_config/ui.rs b/configurator/src/models/config/to_config/ui.rs index c2f2dc15..5cd5c458 100644 --- a/configurator/src/models/config/to_config/ui.rs +++ b/configurator/src/models/config/to_config/ui.rs @@ -48,6 +48,7 @@ impl ConfigDraft { config.ui.toolbar.use_icons = self.ui_toolbar_use_icons; config.ui.toolbar.show_more_colors = self.ui_toolbar_show_more_colors; config.ui.toolbar.show_preset_toasts = self.ui_toolbar_show_preset_toasts; + config.ui.toolbar.idle_fade = self.ui_toolbar_idle_fade; config.ui.toolbar.layout_mode = self.ui_toolbar_layout_mode.to_mode(); config.ui.toolbar.zoom_chip_display = self.ui_toolbar_zoom_chip_display.to_config(); config.ui.toolbar.show_zoom_chip = self.ui_toolbar_show_zoom_chip; diff --git a/configurator/src/models/fields/toggles.rs b/configurator/src/models/fields/toggles.rs index 93c6182f..d5f843e3 100644 --- a/configurator/src/models/fields/toggles.rs +++ b/configurator/src/models/fields/toggles.rs @@ -24,6 +24,7 @@ pub enum ToggleField { UiToolbarUseIcons, UiToolbarShowMoreColors, UiToolbarPresetToasts, + UiToolbarIdleFade, UiToolbarShowPresets, UiToolbarShowActionsSection, UiToolbarShowActionsAdvanced, diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 5b06555d..afc780d9 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -22,7 +22,7 @@ saved separately so moving through the UI does not rewrite unrelated configurati - individual toolbar item visibility and toolbar item order; - the toolbar layout preset and which named toolbar sections are shown; - the toolbar's own appearance and behaviour toggles — icons vs text labels, extra colours, - context-aware UI, preset toasts, the tool preview, and the delay sliders; + context-aware UI, preset toasts, idle fade, the tool preview, and the delay sliders; - the status bar, whether its segments respond to clicks, and which segments are shown; - the board and page badges, the floating badge, and the zoom chip; - the click highlight and the ring the highlight tool keeps on screen; @@ -167,7 +167,7 @@ Both `config.toml` mechanisms leave a timestamped `.bak` beside the file. | Toggle the status bar, its interactivity, or one of its items; the board/page badges, floating badge, or zoom chip | `runtime-ui.toml` | Runtime-UI writer | | Toggle click highlight or the highlight-tool ring | `runtime-ui.toml` — both at once | Runtime-UI writer | | Toggle the input HUD | `runtime-ui.toml` | Runtime-UI writer | -| Toggle the Step section, delay sliders, tool preview, preset toasts, extra colors, or context-aware UI | `runtime-ui.toml` | Runtime-UI writer | +| Toggle the Step section, delay sliders, tool preview, preset toasts, idle fade, extra colors, or context-aware UI | `runtime-ui.toml` | Runtime-UI writer | | Save or clear a preset slot in the overlay | `config.toml` — that one `[presets.slot_N]` table | Overlay editor (with a timestamped `.bak`) | | Recolor a quick color swatch in the overlay | `config.toml` — that one `[[drawing.quick_colors]]` entry | Overlay editor (with a timestamped `.bak`) | | Rename, recolor, add, or delete a board | The session file, for boards marked `persist` | Configurator → Boards for the templates a new session starts from | @@ -1107,6 +1107,10 @@ context_aware_ui = true # Show preset action toast notifications on apply/save/clear show_preset_toasts = true +# Dim the top strip after ~4 seconds without drawing. Set false to keep +# the bar fully visible (accessibility). +idle_fade = true + # Show cursor tool preview bubble show_tool_preview = false @@ -1188,7 +1192,7 @@ top_controls = [ - **Pinned**: `top_pinned` is the authored default for whether the top toolbar opens on startup. Pinning or unpinning in the overlay saves to `runtime-ui.toml` and leaves this value alone. The show/hide keybinding (`toggle_toolbar`, default F9) updates the remembered pin, so the next start matches what was on screen. - **Minimize**: the toolbar minimize button collapses the top strip to a small edge tab instead of hiding it, so there is always an on-screen way back; `top_minimized` is the authored default, and the state you leave the bar in survives restarts as a runtime preference in `runtime-ui.toml`. F9 still toggles full visibility. - **Micro mode**: `cycle_toolbar_display` (default F2) cycles the top strip full → micro → hidden. Micro collapses the strip to one 44px round chip showing the active tool inside a ring stroked in the current color (ring width follows stroke thickness); clicking the chip restores the full strip. The full/micro form persists as a runtime preference in `runtime-ui.toml`, seeded by the authored `top_display_mode`; the hidden step alone is runtime-only — the next start derives the strip's visibility from the remembered pin (which the F9 show/hide toggle updates durably), so a cycle-hidden strip comes back. Entering micro un-minimizes the strip; if a config sets both `top_minimized` and micro, the minimized restore tab wins. -- **Idle fade**: the top-strip islands dim to 55% opacity after ~4 seconds without drawing activity and restore when the pointer approaches the toolbar (or on the next stroke). Open top-strip menus, the minimized tab, and the micro chip never fade. With `[ui] reduced_motion` the fade snaps instantly instead of animating; there is no separate config key. +- **Idle fade**: `idle_fade` dims the top-strip islands to 55% opacity after ~4 seconds without drawing activity and restores when the pointer approaches the toolbar (or on the next stroke). Open top-strip menus, the minimized tab, and the micro chip never fade. With `[ui] reduced_motion` the fade snaps instantly instead of animating. Set `idle_fade = false` (or uncheck **Idle fade** in the overlay Settings popover / **Dim toolbar when idle** in the configurator) to keep the bar fully visible. - **Top-only toolbar**: the unified top toolbar is the only supported layout. Drawing properties live in the contextual style pill; canvas management lives in the **"Canvas…" overflow popover**, the **bottom-right zoom chip**, and the **status-bar board picker**; presets live in the **top-strip presets island**; Session and Settings live in overflow popovers. Older panel keys (`side_layout`, `side_pinned`, `side_minimized`, `side_active_pane`, `collapsed_sections`, `side_offset`, `side_offset_x`, `show_settings_section`, and the retired `items.order.*` lists `side_sections`, `actions`, `pages`, `boards`, `presets`, `tool_options`, and `sessions`) remain readable, are preserved on unrelated saves, and surface as retired-setting diagnostics so they can be removed manually. - **Session/Settings popovers**: the top strip's overflow menu always carries "Session..." and "Settings..." entries. Opening one closes the other and the overflow menu; Escape and clicking away dismiss it. Content taller than the popover cap scrolls internally. - **Hidden items**: `ui.toolbar.items.hidden` removes known toolbar buttons/sections from sizing, drawing, and hit testing while preserving unknown future IDs. diff --git a/src/backend/wayland/backend/state_init/input_state.rs b/src/backend/wayland/backend/state_init/input_state.rs index 75741a7b..61e95093 100644 --- a/src/backend/wayland/backend/state_init/input_state.rs +++ b/src/backend/wayland/backend/state_init/input_state.rs @@ -102,6 +102,7 @@ pub(super) fn build_input_state(config: &Config) -> InputState { config.ui.toolbar.show_delay_sliders, config.ui.toolbar.show_marker_opacity_section, config.ui.toolbar.show_preset_toasts, + config.ui.toolbar.idle_fade, config.ui.toolbar.show_tool_preview, ); input_state.init_toolbar_minimized_from_config(config.ui.toolbar.top_minimized); diff --git a/src/backend/wayland/runtime_ui_state/live_state.rs b/src/backend/wayland/runtime_ui_state/live_state.rs index 60620980..4f29f6b5 100644 --- a/src/backend/wayland/runtime_ui_state/live_state.rs +++ b/src/backend/wayland/runtime_ui_state/live_state.rs @@ -123,6 +123,11 @@ pub(super) fn apply_live_toolbar_state( { input.show_preset_toasts = value; } + if include(&InteractionSeedTarget::ToolbarIdleFade) + && let Some(value) = bool_value(InteractionSeedTarget::ToolbarIdleFade) + { + input.idle_fade = value; + } if include(&InteractionSeedTarget::ToolbarToolPreview) && let Some(value) = bool_value(InteractionSeedTarget::ToolbarToolPreview) { diff --git a/src/backend/wayland/runtime_ui_state/rollback.rs b/src/backend/wayland/runtime_ui_state/rollback.rs index 6196e919..ea77c713 100644 --- a/src/backend/wayland/runtime_ui_state/rollback.rs +++ b/src/backend/wayland/runtime_ui_state/rollback.rs @@ -56,6 +56,7 @@ pub(in crate::backend::wayland) fn apply_toolbar_runtime_rollback( Target::ToolbarMoreColors => set_bool(value, |v| input.show_more_colors = v), Target::ToolbarContextAwareUi => set_bool(value, |v| input.context_aware_ui = v), Target::ToolbarPresetToasts => set_bool(value, |v| input.show_preset_toasts = v), + Target::ToolbarIdleFade => set_bool(value, |v| input.idle_fade = v), Target::ToolbarToolPreview => set_bool(value, |v| input.show_tool_preview = v), Target::ToolbarDelaySliders => set_bool(value, |v| input.show_delay_sliders = v), Target::HistoryCustomSection => set_bool(value, |v| input.custom_section_enabled = v), diff --git a/src/backend/wayland/runtime_ui_state/seeds.rs b/src/backend/wayland/runtime_ui_state/seeds.rs index 7415913f..33520a1c 100644 --- a/src/backend/wayland/runtime_ui_state/seeds.rs +++ b/src/backend/wayland/runtime_ui_state/seeds.rs @@ -51,6 +51,10 @@ pub(super) fn runtime_seeds_from_config( InteractionSeedTarget::ToolbarPresetToasts, InteractionSeedValue::Bool(config.ui.toolbar.show_preset_toasts), )?; + insert( + InteractionSeedTarget::ToolbarIdleFade, + InteractionSeedValue::Bool(config.ui.toolbar.idle_fade), + )?; insert( InteractionSeedTarget::ToolbarToolPreview, InteractionSeedValue::Bool(config.ui.toolbar.show_tool_preview), @@ -238,6 +242,10 @@ pub(super) fn toolbar_values( InteractionSeedTarget::ToolbarPresetToasts, InteractionSeedValue::Bool(input.show_preset_toasts), ), + Target::ToolbarIdleFade => RuntimeUiMutationValues::one( + InteractionSeedTarget::ToolbarIdleFade, + InteractionSeedValue::Bool(input.idle_fade), + ), Target::ToolbarToolPreview => RuntimeUiMutationValues::one( InteractionSeedTarget::ToolbarToolPreview, InteractionSeedValue::Bool(user_tool_preview(input)), diff --git a/src/backend/wayland/runtime_ui_state/tests/preference_actions.rs b/src/backend/wayland/runtime_ui_state/tests/preference_actions.rs index 9cbf9a2d..700d7c31 100644 --- a/src/backend/wayland/runtime_ui_state/tests/preference_actions.rs +++ b/src/backend/wayland/runtime_ui_state/tests/preference_actions.rs @@ -196,6 +196,11 @@ fn a_rollback_restores_every_durable_chrome_preference() { |i| i.show_preset_toasts, |i| i.show_preset_toasts = !i.show_preset_toasts, ), + ( + InteractionSeedTarget::ToolbarIdleFade, + |i| i.idle_fade, + |i| i.idle_fade = !i.idle_fade, + ), ( InteractionSeedTarget::ToolbarToolPreview, |i| i.show_tool_preview, diff --git a/src/backend/wayland/state/toolbar/events/tests.rs b/src/backend/wayland/state/toolbar/events/tests.rs index c069b8f7..2ac08adb 100644 --- a/src/backend/wayland/state/toolbar/events/tests.rs +++ b/src/backend/wayland/state/toolbar/events/tests.rs @@ -341,6 +341,7 @@ fn overlay_preference_toggles_leave_both_configs_untouched() { ToolbarEvent::ToggleMoreColors(true), ToolbarEvent::ToggleContextAwareUi(false), ToolbarEvent::TogglePresetToasts(false), + ToolbarEvent::ToggleIdleFade(false), ToolbarEvent::ToggleDelaySliders(true), ToolbarEvent::ToggleCustomSection(true), ToolbarEvent::ToggleInputHud(true), @@ -829,6 +830,7 @@ fn settings_popover_survives_its_own_controls_and_dismisses_on_everything_else() ToolbarEvent::ToggleStatusPageBadge(true), ToolbarEvent::ToggleFloatingBadgeAlways(true), ToolbarEvent::TogglePresetToasts(true), + ToolbarEvent::ToggleIdleFade(true), ToolbarEvent::ToggleInputHud(true), ToolbarEvent::TogglePresets(true), ToolbarEvent::ToggleActionsSection(true), diff --git a/src/backend/wayland/state/toolbar/fade.rs b/src/backend/wayland/state/toolbar/fade.rs index 1651fda9..63585cf2 100644 --- a/src/backend/wayland/state/toolbar/fade.rs +++ b/src/backend/wayland/state/toolbar/fade.rs @@ -64,6 +64,7 @@ impl WaylandState { pointer_near, menus_open: top_menus_open(input), reduced_chrome, + idle_fade_enabled: input.idle_fade, } } } diff --git a/src/config/tests/load.rs b/src/config/tests/load.rs index fe5f87a7..813fec6c 100644 --- a/src/config/tests/load.rs +++ b/src/config/tests/load.rs @@ -1270,6 +1270,23 @@ fn ui_status_bar_interactive_round_trips_disabled_value() { assert!(!reparsed.ui.status_bar_interactive); } +#[test] +fn ui_toolbar_idle_fade_defaults_to_true_and_round_trips_disabled() { + assert!(Config::default().ui.toolbar.idle_fade); + + let omitted: Config = toml::from_str("[ui.toolbar]\nuse_icons = true\n") + .expect("toolbar table without idle_fade should parse"); + assert!(omitted.ui.toolbar.idle_fade); + + let parsed: Config = toml::from_str("[ui.toolbar]\nidle_fade = false\n") + .expect("idle_fade = false should parse"); + assert!(!parsed.ui.toolbar.idle_fade); + + let serialized = toml::to_string(&parsed).expect("config serializes"); + let reparsed: Config = toml::from_str(&serialized).expect("serialized config reparses"); + assert!(!reparsed.ui.toolbar.idle_fade); +} + #[cfg(feature = "tablet-input")] #[test] fn load_defaults_tablet_input_to_enabled_when_section_is_missing() { diff --git a/src/config/types/toolbar/config.rs b/src/config/types/toolbar/config.rs index 5ede8adc..d02b437e 100644 --- a/src/config/types/toolbar/config.rs +++ b/src/config/types/toolbar/config.rs @@ -113,6 +113,10 @@ pub struct ToolbarConfig { #[serde(default = "default_show_preset_toasts")] pub show_preset_toasts: bool, + /// Dim the top strip after a few seconds without drawing + #[serde(default = "default_idle_fade")] + pub idle_fade: bool, + /// Show the cursor tool preview bubble #[serde(default = "default_show_tool_preview")] pub show_tool_preview: bool, @@ -161,6 +165,7 @@ impl Default for ToolbarConfig { show_marker_opacity_section: default_show_marker_opacity_section(), context_aware_ui: default_context_aware_ui(), show_preset_toasts: default_show_preset_toasts(), + idle_fade: default_idle_fade(), show_tool_preview: default_show_tool_preview(), top_offset: 0.0, top_offset_y: 0.0, @@ -242,6 +247,10 @@ fn default_show_preset_toasts() -> bool { true } +fn default_idle_fade() -> bool { + true +} + fn default_show_tool_preview() -> bool { false } diff --git a/src/input/state/core/base/state/init.rs b/src/input/state/core/base/state/init.rs index d972ee8d..806daf2e 100644 --- a/src/input/state/core/base/state/init.rs +++ b/src/input/state/core/base/state/init.rs @@ -256,6 +256,7 @@ impl InputState { show_delay_sliders: false, // Default to hidden show_marker_opacity_section: false, show_preset_toasts: true, + idle_fade: true, show_tool_preview: false, ui_toast: None, toast_queue: super::super::toast_queue::ToastQueue::default(), diff --git a/src/input/state/core/base/state/structs.rs b/src/input/state/core/base/state/structs.rs index 2ffe15b0..20d14370 100644 --- a/src/input/state/core/base/state/structs.rs +++ b/src/input/state/core/base/state/structs.rs @@ -495,6 +495,8 @@ pub struct InputState { pub show_marker_opacity_section: bool, /// Whether to show preset action toast notifications pub show_preset_toasts: bool, + /// Whether the top strip dims after a few seconds without drawing + pub idle_fade: bool, /// Whether to show the cursor tool preview bubble pub show_tool_preview: bool, /// Active (visible) UI toast (errors/warnings/info) diff --git a/src/input/state/core/tool_controls/toolbar.rs b/src/input/state/core/tool_controls/toolbar.rs index 60ea75da..f1672477 100644 --- a/src/input/state/core/tool_controls/toolbar.rs +++ b/src/input/state/core/tool_controls/toolbar.rs @@ -197,6 +197,7 @@ impl InputState { show_delay_sliders: bool, show_marker_opacity_section: bool, show_preset_toasts: bool, + idle_fade: bool, show_tool_preview: bool, ) { self.toolbar_top_pinned = top_pinned; @@ -221,6 +222,7 @@ impl InputState { self.show_delay_sliders = show_delay_sliders; self.show_marker_opacity_section = show_marker_opacity_section; self.show_preset_toasts = show_preset_toasts; + self.idle_fade = idle_fade; self.show_tool_preview = show_tool_preview; // Fold the legacy show_* booleans into explicit item overrides, // then re-derive them from the one resolver. Effective visibility @@ -550,6 +552,7 @@ mod tests { false, false, true, + true, false, ); diff --git a/src/input/state/tests/toolbar_display.rs b/src/input/state/tests/toolbar_display.rs index 7ea21b3d..c210b88f 100644 --- a/src/input/state/tests/toolbar_display.rs +++ b/src/input/state/tests/toolbar_display.rs @@ -895,6 +895,10 @@ fn run_only_toolbar_preference_events_never_mark_the_session_dirty() { ToolbarEvent::TogglePresetToasts(false), ToolbarEvent::TogglePresetToasts(true), ], + [ + ToolbarEvent::ToggleIdleFade(false), + ToolbarEvent::ToggleIdleFade(true), + ], [ ToolbarEvent::ToggleToolPreview(false), ToolbarEvent::ToggleToolPreview(true), diff --git a/src/runtime_ui_state/types.rs b/src/runtime_ui_state/types.rs index 0b79d1e9..0c1a20fc 100644 --- a/src/runtime_ui_state/types.rs +++ b/src/runtime_ui_state/types.rs @@ -127,6 +127,7 @@ pub(crate) enum InteractionSeedTarget { ToolbarMoreColors, ToolbarContextAwareUi, ToolbarPresetToasts, + ToolbarIdleFade, ToolbarToolPreview, ToolbarDelaySliders, /// The history pane's custom-step section. @@ -177,6 +178,7 @@ impl InteractionSeedValue { | Target::ToolbarMoreColors | Target::ToolbarContextAwareUi | Target::ToolbarPresetToasts + | Target::ToolbarIdleFade | Target::ToolbarToolPreview | Target::ToolbarDelaySliders | Target::HistoryCustomSection diff --git a/src/runtime_ui_state/wire/v1.rs b/src/runtime_ui_state/wire/v1.rs index d00147ed..ff2b771c 100644 --- a/src/runtime_ui_state/wire/v1.rs +++ b/src/runtime_ui_state/wire/v1.rs @@ -20,7 +20,7 @@ use crate::runtime_ui_state::{ /// The retired side fields (`side_pinned`, `side_minimized`, `side_pane`, /// `side_position`, and the `collapsed_sections` table) are deliberately absent: /// they now flow through `WirePassthrough::toolbar` as complete inert raw values. -const TOOLBAR_SCALARS: [(&str, InteractionSeedTarget); 22] = [ +const TOOLBAR_SCALARS: [(&str, InteractionSeedTarget); 23] = [ ("top_pinned", InteractionSeedTarget::TopPinned), ("top_minimized", InteractionSeedTarget::TopMinimized), ("top_position", InteractionSeedTarget::TopPosition), @@ -57,6 +57,7 @@ const TOOLBAR_SCALARS: [(&str, InteractionSeedTarget); 22] = [ "show_preset_toasts", InteractionSeedTarget::ToolbarPresetToasts, ), + ("idle_fade", InteractionSeedTarget::ToolbarIdleFade), ( "show_tool_preview", InteractionSeedTarget::ToolbarToolPreview, @@ -216,6 +217,7 @@ fn decode_value( | Target::ToolbarMoreColors | Target::ToolbarContextAwareUi | Target::ToolbarPresetToasts + | Target::ToolbarIdleFade | Target::ToolbarToolPreview | Target::ToolbarDelaySliders | Target::HistoryCustomSection @@ -391,6 +393,9 @@ pub(super) fn encode(wire: &RuntimeUiWireState) -> Result { insert_recognized(&mut toolbar, "show_preset_toasts", entry) } + InteractionSeedTarget::ToolbarIdleFade => { + insert_recognized(&mut toolbar, "idle_fade", entry) + } InteractionSeedTarget::ToolbarToolPreview => { insert_recognized(&mut toolbar, "show_tool_preview", entry) } @@ -474,6 +479,7 @@ fn encode_value( | InteractionSeedTarget::ToolbarMoreColors | InteractionSeedTarget::ToolbarContextAwareUi | InteractionSeedTarget::ToolbarPresetToasts + | InteractionSeedTarget::ToolbarIdleFade | InteractionSeedTarget::ToolbarToolPreview | InteractionSeedTarget::ToolbarDelaySliders | InteractionSeedTarget::HistoryCustomSection diff --git a/src/toolbar_gtk/view/sections/settings_pane.rs b/src/toolbar_gtk/view/sections/settings_pane.rs index e8c7a42d..bf19bfb5 100644 --- a/src/toolbar_gtk/view/sections/settings_pane.rs +++ b/src/toolbar_gtk/view/sections/settings_pane.rs @@ -109,6 +109,7 @@ fn settings_toggle_event(template: &ToolbarEvent, checked: bool) -> ToolbarEvent ToolbarEvent::ToggleFloatingBadgeAlways(checked) } ToolbarEvent::TogglePresetToasts(_) => ToolbarEvent::TogglePresetToasts(checked), + ToolbarEvent::ToggleIdleFade(_) => ToolbarEvent::ToggleIdleFade(checked), ToolbarEvent::ToggleInputHud(_) => ToolbarEvent::ToggleInputHud(checked), ToolbarEvent::TogglePresets(_) => ToolbarEvent::TogglePresets(checked), ToolbarEvent::ToggleActionsSection(_) => ToolbarEvent::ToggleActionsSection(checked), diff --git a/src/ui/toolbar/apply/layout.rs b/src/ui/toolbar/apply/layout.rs index d028b3c6..267d461c 100644 --- a/src/ui/toolbar/apply/layout.rs +++ b/src/ui/toolbar/apply/layout.rs @@ -204,6 +204,15 @@ impl InputState { } } + pub(super) fn apply_toolbar_toggle_idle_fade(&mut self, enable: bool) -> bool { + if self.idle_fade != enable { + self.idle_fade = enable; + true + } else { + false + } + } + pub(super) fn apply_toolbar_toggle_tool_preview(&mut self, show: bool) -> bool { if self.presenter_mode && self.presenter_mode_config.hide_tool_preview { return false; diff --git a/src/ui/toolbar/apply/mod.rs b/src/ui/toolbar/apply/mod.rs index a6e91a73..b1dd3484 100644 --- a/src/ui/toolbar/apply/mod.rs +++ b/src/ui/toolbar/apply/mod.rs @@ -196,6 +196,7 @@ impl InputState { self.apply_toolbar_toggle_context_aware_ui(enabled) } ToolbarEvent::TogglePresetToasts(show) => self.apply_toolbar_toggle_preset_toasts(show), + ToolbarEvent::ToggleIdleFade(enable) => self.apply_toolbar_toggle_idle_fade(enable), ToolbarEvent::ToggleToolPreview(show) => self.apply_toolbar_toggle_tool_preview(show), ToolbarEvent::ToggleStatusBar(show) => self.apply_toolbar_toggle_status_bar(show), ToolbarEvent::SetStatusBarInteractive(interactive) => { diff --git a/src/ui/toolbar/events.rs b/src/ui/toolbar/events.rs index 89253665..71611d43 100644 --- a/src/ui/toolbar/events.rs +++ b/src/ui/toolbar/events.rs @@ -259,6 +259,8 @@ pub enum ToolbarEvent { ToggleContextAwareUi(bool), /// Toggle preset action toast notifications TogglePresetToasts(bool), + /// Toggle top-strip idle fade + ToggleIdleFade(bool), /// Toggle cursor tool preview bubble #[allow(dead_code)] ToggleToolPreview(bool), diff --git a/src/ui/toolbar/model/activation.rs b/src/ui/toolbar/model/activation.rs index 467b1caa..131a90a9 100644 --- a/src/ui/toolbar/model/activation.rs +++ b/src/ui/toolbar/model/activation.rs @@ -31,6 +31,7 @@ pub(crate) enum ToolbarControlId { SettingsStatusAbout, SettingsFloatingBadgeAlways, SettingsPresetToasts, + SettingsIdleFade, SettingsInputHud, SettingsPresets, SettingsActions, diff --git a/src/ui/toolbar/model/event_policy.rs b/src/ui/toolbar/model/event_policy.rs index 2c561ae3..7d54f19a 100644 --- a/src/ui/toolbar/model/event_policy.rs +++ b/src/ui/toolbar/model/event_policy.rs @@ -100,6 +100,7 @@ pub(crate) enum ToolbarRuntimeUiPersistenceTarget { ToolbarMoreColors, ToolbarContextAwareUi, ToolbarPresetToasts, + ToolbarIdleFade, ToolbarToolPreview, ToolbarDelaySliders, HistoryCustomSection, @@ -341,6 +342,7 @@ pub(crate) fn popovers_for_event(event: &ToolbarEvent) -> &'static [ToolbarPopov | ToolbarEvent::ToggleStatusPageBadge(_) | ToolbarEvent::ToggleFloatingBadgeAlways(_) | ToolbarEvent::TogglePresetToasts(_) + | ToolbarEvent::ToggleIdleFade(_) | ToolbarEvent::ToggleInputHud(_) | ToolbarEvent::TogglePresets(_) | ToolbarEvent::ToggleActionsSection(_) @@ -448,6 +450,9 @@ fn persistence_for_event(event: &ToolbarEvent) -> ToolbarPersistence { ToolbarEvent::TogglePresetToasts(_) => { ToolbarPersistence::RuntimeUi(Runtime::ToolbarPresetToasts) } + ToolbarEvent::ToggleIdleFade(_) => { + ToolbarPersistence::RuntimeUi(Runtime::ToolbarIdleFade) + } ToolbarEvent::ToggleToolPreview(_) => { ToolbarPersistence::RuntimeUi(Runtime::ToolbarToolPreview) } diff --git a/src/ui/toolbar/model/settings.rs b/src/ui/toolbar/model/settings.rs index 9a9e3f94..0a9353e2 100644 --- a/src/ui/toolbar/model/settings.rs +++ b/src/ui/toolbar/model/settings.rs @@ -86,6 +86,13 @@ impl ToolbarSettingsModel { ToolbarEvent::TogglePresetToasts(!snapshot.show_preset_toasts), "Preset toasts: apply/save/clear.", ), + ToolbarSettingsToggle::new( + ToolbarControlId::SettingsIdleFade, + "Dim toolbar when idle", + snapshot.idle_fade, + ToolbarEvent::ToggleIdleFade(!snapshot.idle_fade), + "Fade the top bar to 55% after a few seconds without drawing. Turn off to keep it fully visible.", + ), ToolbarSettingsToggle::new( ToolbarControlId::SettingsInputHud, "Input HUD", diff --git a/src/ui/toolbar/snapshot/build.rs b/src/ui/toolbar/snapshot/build.rs index 86cce0da..bf12070d 100644 --- a/src/ui/toolbar/snapshot/build.rs +++ b/src/ui/toolbar/snapshot/build.rs @@ -159,6 +159,7 @@ impl ToolbarSnapshot { show_boards_section, show_marker_opacity_section: state.show_marker_opacity_section, show_preset_toasts: state.show_preset_toasts, + idle_fade: state.idle_fade, show_presets: state.show_presets, show_step_section, show_text_controls: state.show_text_controls, diff --git a/src/ui/toolbar/snapshot/fade.rs b/src/ui/toolbar/snapshot/fade.rs index e0976e25..19b62b10 100644 --- a/src/ui/toolbar/snapshot/fade.rs +++ b/src/ui/toolbar/snapshot/fade.rs @@ -35,11 +35,14 @@ pub struct TopStripFadeInputs { /// Minimized restore tab, micro chip, or hidden strip: minimal chrome /// never fades. pub reduced_chrome: bool, + /// Authored/runtime preference: when false the strip stays at full + /// opacity and the idle timer is not scheduled. + pub idle_fade_enabled: bool, } impl TopStripFadeInputs { fn dim_candidate(&self) -> bool { - !self.pointer_near && !self.menus_open && !self.reduced_chrome + self.idle_fade_enabled && !self.pointer_near && !self.menus_open && !self.reduced_chrome } fn wants_dim(&self) -> bool { @@ -128,6 +131,7 @@ mod tests { pointer_near: false, menus_open: false, reduced_chrome: false, + idle_fade_enabled: true, } } @@ -267,4 +271,36 @@ mod tests { assert_eq!(fade.update(&near, start), 1.0); assert!(!fade.animating()); } + + #[test] + fn disabled_idle_fade_stays_full_and_does_not_schedule_a_wakeup() { + let _motion = override_motion_for_test(true); + let mut fade = TopStripFade::new(); + let start = Instant::now(); + let mut disabled = inputs(30); + disabled.idle_fade_enabled = false; + + assert_eq!(fade.update(&disabled, start), 1.0); + assert!(!fade.animating()); + assert_eq!(fade.wake_after(&disabled), None); + } + + #[test] + fn disabling_idle_fade_restores_a_dimmed_strip() { + let _motion = override_motion_for_test(true); + let mut fade = TopStripFade::new(); + let start = Instant::now(); + fade.update(&inputs(10), start); + fade.update(&inputs(10), start + TOP_STRIP_FADE_OUT); + assert_eq!(fade.value(), TOP_STRIP_DIM_LEVEL); + + let mut disabled = inputs(10); + disabled.idle_fade_enabled = false; + fade.update(&disabled, start + TOP_STRIP_FADE_OUT); + assert_eq!( + fade.update(&disabled, start + TOP_STRIP_FADE_OUT + TOP_STRIP_RESTORE), + 1.0 + ); + assert_eq!(fade.wake_after(&disabled), None); + } } diff --git a/src/ui/toolbar/snapshot/types.rs b/src/ui/toolbar/snapshot/types.rs index 2b30a596..74b64dd1 100644 --- a/src/ui/toolbar/snapshot/types.rs +++ b/src/ui/toolbar/snapshot/types.rs @@ -315,6 +315,8 @@ pub struct ToolbarSnapshot { pub show_marker_opacity_section: bool, /// Whether to show preset action toasts pub show_preset_toasts: bool, + /// Whether the top strip dims after a few seconds without drawing + pub idle_fade: bool, /// Whether to show the Presets section pub show_presets: bool, /// Whether to show the Step Undo/Redo section