From c21bd8dad19cfc16f85e2deb0fcbdbfff602bbe7 Mon Sep 17 00:00:00 2001 From: devmobasa <4170275+devmobasa@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:15:30 +0200 Subject: [PATCH 1/2] feat(toolbar): remove legacy side palette --- CHANGELOG.md | 8 + README.md | 15 +- config.example.toml | 76 +- configurator/README.md | 11 +- configurator/src/app/pages/ui/toolbar.rs | 30 +- .../src/app/pages/ui/toolbar_visibility.rs | 11 +- configurator/src/app/search/terms.rs | 10 +- configurator/src/app/update/config/status.rs | 8 + configurator/src/app/update/fields/ui.rs | 12 +- configurator/src/app/update/mod.rs | 3 - configurator/src/messages.rs | 4 +- .../src/models/config/draft/from_config.rs | 10 +- configurator/src/models/config/draft/mod.rs | 7 +- configurator/src/models/config/setters.rs | 4 - configurator/src/models/config/tests.rs | 34 - .../src/models/config/to_config/ui.rs | 15 - .../src/models/config/toolbar_overrides.rs | 3 - configurator/src/models/fields/mod.rs | 2 +- configurator/src/models/fields/toggles.rs | 3 - configurator/src/models/fields/toolbar.rs | 43 +- configurator/src/models/mod.rs | 4 +- docs/CONFIG.md | 127 ++- docs/codebase-overview.md | 28 +- docs/toolbar-item-id-compatibility.md | 135 ++++ examples/session-manager.md | 4 +- .../wayland/backend/state_init/input_state.rs | 12 +- src/backend/wayland/handlers/keyboard/mod.rs | 4 +- src/backend/wayland/handlers/pointer/axis.rs | 15 +- .../wayland/handlers/pointer/cursor.rs | 2 - src/backend/wayland/handlers/pointer/press.rs | 6 +- .../wayland/handlers/relative_pointer.rs | 6 +- src/backend/wayland/runtime_ui_state.rs | 4 +- .../wayland/runtime_ui_state/coordinator.rs | 7 +- .../wayland/runtime_ui_state/live_state.rs | 33 - .../wayland/runtime_ui_state/positions.rs | 16 +- .../wayland/runtime_ui_state/rollback.rs | 48 +- src/backend/wayland/runtime_ui_state/seeds.rs | 73 +- src/backend/wayland/runtime_ui_state/tests.rs | 45 +- .../tests/board_pin_resets.rs | 5 +- .../runtime_ui_state/tests/board_pins.rs | 44 +- .../runtime_ui_state/tests/drag_previews.rs | 91 +-- .../runtime_ui_state/tests/layout_state.rs | 53 +- .../tests/preference_actions.rs | 5 +- .../runtime_ui_state/tests/preferences.rs | 5 +- .../runtime_ui_state/tests/recovery.rs | 63 +- .../runtime_ui_state/tests/visibility.rs | 174 +--- .../tests/visibility_recovery.rs | 36 +- .../wayland/runtime_ui_state/wayland.rs | 40 +- src/backend/wayland/state.rs | 18 +- src/backend/wayland/state/boards.rs | 2 +- src/backend/wayland/state/core/init.rs | 13 +- src/backend/wayland/state/data.rs | 40 +- src/backend/wayland/state/gtk_toolbar.rs | 138 +--- src/backend/wayland/state/helpers.rs | 16 +- src/backend/wayland/state/onboarding.rs | 51 -- src/backend/wayland/state/onboarding/tests.rs | 29 +- src/backend/wayland/state/render/ui.rs | 2 +- src/backend/wayland/state/toolbar.rs | 2 +- .../wayland/state/toolbar/drag/base.rs | 65 +- .../wayland/state/toolbar/drag/clamp.rs | 94 +-- .../wayland/state/toolbar/drag/handoff.rs | 19 +- .../wayland/state/toolbar/drag/move_drag.rs | 84 +- .../wayland/state/toolbar/drag/relative.rs | 20 +- src/backend/wayland/state/toolbar/events.rs | 77 +- .../wayland/state/toolbar/events/feedback.rs | 14 - .../wayland/state/toolbar/events/session.rs | 9 +- .../wayland/state/toolbar/events/tests.rs | 59 -- src/backend/wayland/state/toolbar/geometry.rs | 145 +--- .../wayland/state/toolbar/gtk_feedback.rs | 94 +-- .../wayland/state/toolbar/inline/drag.rs | 17 +- .../wayland/state/toolbar/inline/focus.rs | 129 +-- .../wayland/state/toolbar/inline/input.rs | 133 +-- .../wayland/state/toolbar/inline/mod.rs | 110 +-- .../wayland/state/toolbar/inline/render.rs | 180 +---- src/backend/wayland/state/toolbar/scroll.rs | 56 ++ .../wayland/state/toolbar/side_pane.rs | 113 --- .../state/toolbar/visibility/access.rs | 27 +- .../wayland/state/toolbar/visibility/sync.rs | 76 +- src/backend/wayland/toolbar/AGENTS.md | 13 +- src/backend/wayland/toolbar/events.rs | 20 - src/backend/wayland/toolbar/hit.rs | 116 +-- src/backend/wayland/toolbar/layout/mod.rs | 15 - .../wayland/toolbar/layout/spec/mod.rs | 1 - .../toolbar/layout/spec/side/constants.rs | 106 --- .../wayland/toolbar/layout/spec/side/mod.rs | 2 - .../wayland/toolbar/layout/spec/side/sizes.rs | 311 ------- .../layout/spec/side/sizes/sections.rs | 279 ------- .../toolbar/layout/tests/collapsible.rs | 274 ------- .../wayland/toolbar/layout/tests/mod.rs | 485 +---------- src/backend/wayland/toolbar/main/input.rs | 68 +- src/backend/wayland/toolbar/main/lifecycle.rs | 20 - src/backend/wayland/toolbar/main/render.rs | 63 +- src/backend/wayland/toolbar/main/state.rs | 66 +- src/backend/wayland/toolbar/main/structs.rs | 26 +- src/backend/wayland/toolbar/mod.rs | 14 +- src/backend/wayland/toolbar/render.rs | 2 - src/backend/wayland/toolbar/render/paint.rs | 32 +- .../toolbar/render/side_palette/actions.rs | 303 ------- .../render/side_palette/actions/helpers.rs | 394 --------- .../toolbar/render/side_palette/arrow.rs | 118 --- .../toolbar/render/side_palette/boards.rs | 165 ---- .../toolbar/render/side_palette/colors.rs | 307 ------- .../render/side_palette/colors/helpers.rs | 508 ------------ .../toolbar/render/side_palette/header.rs | 283 ------- .../toolbar/render/side_palette/marker.rs | 160 ---- .../toolbar/render/side_palette/mod.rs | 436 ---------- .../toolbar/render/side_palette/pages.rs | 137 ---- .../render/side_palette/presets/format.rs | 125 --- .../render/side_palette/presets/header.rs | 66 -- .../render/side_palette/presets/mod.rs | 72 -- .../side_palette/presets/slot/content.rs | 151 ---- .../side_palette/presets/slot/feedback.rs | 61 -- .../render/side_palette/presets/slot/mod.rs | 246 ------ .../render/side_palette/presets/widgets.rs | 95 --- .../render/side_palette/section_header.rs | 96 --- .../toolbar/render/side_palette/session.rs | 442 ---------- .../toolbar/render/side_palette/settings.rs | 490 ----------- .../render/side_palette/step/custom_rows.rs | 250 ------ .../render/side_palette/step/delay_sliders.rs | 136 ---- .../toolbar/render/side_palette/step/mod.rs | 135 ---- .../render/side_palette/step_marker.rs | 90 --- .../toolbar/render/side_palette/text.rs | 228 ------ .../toolbar/render/side_palette/thickness.rs | 338 -------- .../toolbar/render/widgets/background.rs | 17 +- .../wayland/toolbar/render/widgets/buttons.rs | 18 - .../wayland/toolbar/render/widgets/color.rs | 189 ----- .../wayland/toolbar/render/widgets/labels.rs | 15 +- .../wayland/toolbar/render/widgets/mod.rs | 8 +- src/backend/wayland/toolbar/rows.rs | 81 +- src/backend/wayland/toolbar/view/node.rs | 17 +- src/backend/wayland/toolbar/view/top.rs | 4 +- src/backend/wayland/toolbar/view/top/build.rs | 2 +- src/backend/wayland/toolbar/view/top/menus.rs | 93 ++- src/backend/wayland/toolbar/view/top/tests.rs | 44 +- src/config/AGENTS.md | 1 + src/config/document.rs | 45 +- src/config/mod.rs | 8 +- src/config/tests/document.rs | 359 +++++++++ src/config/tests/file_io.rs | 24 +- src/config/tests/load.rs | 31 - src/config/types/mod.rs | 4 +- src/config/types/presenter_mode.rs | 6 +- src/config/types/toolbar/config.rs | 72 +- src/config/types/toolbar/display.rs | 29 - src/config/types/toolbar/ids.rs | 40 - src/config/types/toolbar/items.rs | 77 +- src/config/types/toolbar/items/definitions.rs | 276 ++----- src/config/types/toolbar/items/order.rs | 126 +-- src/config/types/toolbar/items/tests.rs | 79 +- src/config/types/toolbar/mod.rs | 2 +- src/config/types/toolbar/overrides.rs | 10 +- src/config/types/toolbar/visibility.rs | 39 +- src/input/state/actions/action_ui.rs | 24 +- src/input/state/core/base/state/init.rs | 9 - src/input/state/core/base/state/structs.rs | 34 +- src/input/state/core/base/types.rs | 9 +- src/input/state/core/tool_controls/toolbar.rs | 173 +--- src/input/state/core/tour.rs | 8 +- src/input/state/core/utility/focus_mode.rs | 3 - src/input/state/core/utility/light_mode.rs | 3 - src/input/state/core/utility/pending.rs | 6 +- .../state/core/utility/presenter_mode.rs | 10 +- src/input/state/tests/basics.rs | 48 -- src/input/state/tests/light_mode.rs | 4 - src/input/state/tests/modal.rs | 2 - src/input/state/tests/presenter_mode.rs | 10 - src/input/state/tests/status_hud.rs | 9 +- src/input/state/tests/toolbar_display.rs | 110 +-- src/input/tool/profile.rs | 2 +- src/label_format.rs | 4 +- src/runtime_ui_state/store/tests.rs | 8 +- src/runtime_ui_state/tests.rs | 12 +- src/runtime_ui_state/tests/permits.rs | 42 +- src/runtime_ui_state/tests/pipeline.rs | 10 +- .../tests/recovery_cleanup.rs | 6 +- .../tests/recovery_conflicts.rs | 6 +- src/runtime_ui_state/tests/reset_preview.rs | 10 +- src/runtime_ui_state/types.rs | 25 +- src/runtime_ui_state/wire/tests.rs | 171 +++- src/runtime_ui_state/wire/v1.rs | 79 +- src/toolbar_gtk/bridge.rs | 29 +- src/toolbar_gtk/css.rs | 62 +- src/toolbar_gtk/icons.rs | 9 - src/toolbar_gtk/mod.rs | 39 +- src/toolbar_gtk/view/drag.rs | 12 +- src/toolbar_gtk/view/mod.rs | 29 +- src/toolbar_gtk/view/sections/actions.rs | 221 ----- src/toolbar_gtk/view/sections/arrow.rs | 76 -- src/toolbar_gtk/view/sections/boards.rs | 51 -- src/toolbar_gtk/view/sections/canvas_pane.rs | 17 +- src/toolbar_gtk/view/sections/colors.rs | 762 ------------------ src/toolbar_gtk/view/sections/colors/tests.rs | 110 --- src/toolbar_gtk/view/sections/marker.rs | 73 -- src/toolbar_gtk/view/sections/mod.rs | 260 +----- src/toolbar_gtk/view/sections/pages.rs | 37 - src/toolbar_gtk/view/sections/presets.rs | 591 -------------- src/toolbar_gtk/view/sections/session_pane.rs | 55 +- .../view/sections/settings_pane.rs | 57 +- src/toolbar_gtk/view/sections/step_marker.rs | 45 -- src/toolbar_gtk/view/sections/step_undo.rs | 26 +- .../view/sections/text_controls.rs | 132 --- src/toolbar_gtk/view/sections/thickness.rs | 202 ----- src/toolbar_gtk/view/side_bar.rs | 250 ------ src/toolbar_gtk/view/side_bar/chrome.rs | 147 ---- src/toolbar_gtk/view/side_bar/drag.rs | 225 ------ src/toolbar_gtk/view/side_bar/palette.rs | 161 ---- src/toolbar_gtk/view/side_bar/structure.rs | 105 --- src/toolbar_gtk/view/side_bar/tests.rs | 78 -- src/toolbar_gtk/view/top_bar.rs | 10 +- src/toolbar_gtk/view/top_bar/controls.rs | 4 +- src/toolbar_gtk/view/top_bar/popovers.rs | 4 +- src/toolbar_gtk/view/top_bar/tests.rs | 21 +- src/toolbar_gtk/widgets.rs | 71 +- src/toolbar_icons/controls.rs | 4 - src/toolbar_icons/mod.rs | 3 +- src/toolbar_icons/svg.rs | 16 +- src/ui/theme.rs | 12 +- src/ui/toolbar/AGENTS.md | 5 +- src/ui/toolbar/apply/layout.rs | 101 +-- src/ui/toolbar/apply/mod.rs | 13 +- src/ui/toolbar/apply/tools.rs | 40 - src/ui/toolbar/events.rs | 224 +---- src/ui/toolbar/mod.rs | 5 +- src/ui/toolbar/model/actions.rs | 265 ++---- src/ui/toolbar/model/activation.rs | 79 +- src/ui/toolbar/model/control.rs | 7 +- src/ui/toolbar/model/event_policy.rs | 40 +- src/ui/toolbar/model/header.rs | 143 +--- src/ui/toolbar/model/mod.rs | 363 +++++---- src/ui/toolbar/model/session.rs | 61 +- src/ui/toolbar/model/settings.rs | 43 +- src/ui/toolbar/model/settings/helpers.rs | 85 +- src/ui/toolbar/model/tools.rs | 81 +- src/ui/toolbar/model/top_spec/control.rs | 8 +- src/ui/toolbar/model/top_spec/spec.rs | 11 +- .../toolbar/model/top_spec/tests/behavior.rs | 2 +- .../toolbar/model/top_spec/tests/structure.rs | 6 +- src/ui/toolbar/session_format.rs | 78 ++ src/ui/toolbar/snapshot/build.rs | 23 +- src/ui/toolbar/snapshot/types.rs | 80 +- 240 files changed, 2374 insertions(+), 16919 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docs/toolbar-item-id-compatibility.md create mode 100644 src/backend/wayland/state/toolbar/scroll.rs delete mode 100644 src/backend/wayland/state/toolbar/side_pane.rs delete mode 100644 src/backend/wayland/toolbar/layout/spec/side/constants.rs delete mode 100644 src/backend/wayland/toolbar/layout/spec/side/mod.rs delete mode 100644 src/backend/wayland/toolbar/layout/spec/side/sizes.rs delete mode 100644 src/backend/wayland/toolbar/layout/spec/side/sizes/sections.rs delete mode 100644 src/backend/wayland/toolbar/layout/tests/collapsible.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/actions.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/actions/helpers.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/arrow.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/boards.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/colors.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/colors/helpers.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/header.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/marker.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/mod.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/pages.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/presets/format.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/presets/header.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/presets/mod.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/presets/slot/content.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/presets/slot/feedback.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/presets/slot/mod.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/presets/widgets.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/section_header.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/session.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/settings.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/step/custom_rows.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/step/delay_sliders.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/step/mod.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/step_marker.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/text.rs delete mode 100644 src/backend/wayland/toolbar/render/side_palette/thickness.rs delete mode 100644 src/backend/wayland/toolbar/render/widgets/color.rs delete mode 100644 src/toolbar_gtk/view/sections/actions.rs delete mode 100644 src/toolbar_gtk/view/sections/arrow.rs delete mode 100644 src/toolbar_gtk/view/sections/boards.rs delete mode 100644 src/toolbar_gtk/view/sections/colors.rs delete mode 100644 src/toolbar_gtk/view/sections/colors/tests.rs delete mode 100644 src/toolbar_gtk/view/sections/marker.rs delete mode 100644 src/toolbar_gtk/view/sections/pages.rs delete mode 100644 src/toolbar_gtk/view/sections/presets.rs delete mode 100644 src/toolbar_gtk/view/sections/step_marker.rs delete mode 100644 src/toolbar_gtk/view/sections/text_controls.rs delete mode 100644 src/toolbar_gtk/view/sections/thickness.rs delete mode 100644 src/toolbar_gtk/view/side_bar.rs delete mode 100644 src/toolbar_gtk/view/side_bar/chrome.rs delete mode 100644 src/toolbar_gtk/view/side_bar/drag.rs delete mode 100644 src/toolbar_gtk/view/side_bar/palette.rs delete mode 100644 src/toolbar_gtk/view/side_bar/structure.rs delete mode 100644 src/toolbar_gtk/view/side_bar/tests.rs create mode 100644 src/ui/toolbar/session_format.rs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..3b29891a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## Unreleased + +### Breaking (Rust source) + +- The unified top toolbar is now the only layout. Panel-era typed config fields and order groups are removed from the serde model (`side_*` placement/pin/pane keys, `show_settings_section` / mode overrides, and `ui.toolbar.items.order.{side_sections,actions,pages,boards,presets,tool_options,sessions}`). Authored values at those exact paths remain in `config.toml` as retired settings and no longer affect the overlay. Matching keys under `runtime-ui.toml`'s recognized `item_order` map are pruned on rewrite. +- Public Rust types that described the side palette / panel-only toolbar order groups are gone. Downstream crates that constructed those fields must drop them; the in-repo configurator already matches this shape. diff --git a/README.md b/README.md index 5505fc62..623aba5b 100644 --- a/README.md +++ b/README.md @@ -963,11 +963,16 @@ quitting Wayscriber never changes the file on its own. An incidental preference the current run and offers a route to the configurator screen that owns its default. Toolbar pins (updated by the pin buttons and by the toolbar show/hide keybinding, so the next -start matches what was on screen), dragged positions, the top strip's display form, -minimized/pane/collapsed state, individual toolbar item visibility/order, and board pins changed -in the overlay are direct interaction state and are saved separately in the generated +start matches what was on screen), dragged top-strip positions, the top strip's display form and +minimized state, individual toolbar item visibility/order, and board pins changed in the overlay +are direct interaction state and are saved separately in the generated `$XDG_DATA_HOME/wayscriber/runtime-ui.toml` file. Inspect, recover, or reset those runtime -preferences from the overlay Settings panel; see [Configuration Guide](docs/CONFIG.md#configured-defaults-and-runtime-ui-preferences). +preferences from the overlay Settings popover; see [Configuration Guide](docs/CONFIG.md#configured-defaults-and-runtime-ui-preferences). + +The unified top toolbar is the only layout. Older panel keys such as `side_layout` remain +readable and are preserved on save, but they no longer affect the running overlay; the +configurator reports them as retired settings you can remove. Historically named `side.*` +item IDs still customize top-toolbar controls and must not be renamed. ### Configurator (GUI) @@ -1081,7 +1086,7 @@ See [Session manager examples](examples/session-manager.md) for complete CLI, ov - Config values seed startup defaults. When `restore_tool_state` is enabled (default), the last-used tool settings saved in the session (including arrow head placement) override those config defaults on startup. Run `wayscriber --clear-tool-state` to remove only that saved tool layer so config defaults apply next startup while saved boards/history remain. In a running overlay, use Command Palette → Reset Tool Defaults to clear the saved layer and immediately apply config defaults to the active tools. - `--session-file` uses exactly the selected file, implies persistence for that overlay run, rejects directories/symlinks/special files, and does not create missing parent directories. A running daemon can launch a hidden overlay with a named target; if the overlay is already visible, hide it before switching to a different named session. -- The overlay Session panel lives in the side toolbar's Settings drawer. It can open an existing named session, save the current overlay as another named session, show session info, clear the active session, reopen recent named sessions, and jump to the configurator. The Open/Save As dialogs use `zenity` or `kdialog`; Save As appends `.wayscriber-session` when no extension is supplied and asks before replacing existing session artifacts. +- The overlay Session controls live in the top toolbar's Session popover (overflow menu → Session...). They can open an existing named session, save the current overlay as another named session, show session info, clear the active session, reopen recent named sessions, and jump to the configurator. The Open/Save As dialogs use `zenity` or `kdialog`; Save As appends `.wayscriber-session` when no extension is supplied and asks before replacing existing session artifacts. - The configurator Session tab manages recent named sessions recorded when named-session targets are opened or saved from the CLI, daemon, or overlay. It can rename catalog labels, reveal files, and forget metadata without touching files. Clear Tool State removes only the saved tool layer; Clear Saved Data removes session files. Duplicate, Move, Clear Tool State, and Clear are disabled while an overlay, manually started daemon, or background service is active. diff --git a/config.example.toml b/config.example.toml index 7a49283a..704b992a 100644 --- a/config.example.toml +++ b/config.example.toml @@ -518,14 +518,10 @@ layout_mode = "regular" # choice top_pinned = true -# Show side toolbar on startup (pinned) -side_pinned = true - -# Start toolbars minimized to their edge restore tabs -# The minimize button updates these; a minimized bar always leaves a +# Start the top toolbar minimized to its edge restore tab +# The minimize button updates this; a minimized bar always leaves a # clickable tab on screen to bring it back top_minimized = false -side_minimized = false # Display form of the top strip restored at startup: "full" or "micro" # (one 44px round chip: active tool glyph + a ring in the current color). @@ -535,24 +531,15 @@ side_minimized = false # runtime-only and comes back per top_pinned top_display_mode = "full" -# Where the side-palette functions live: "pill" (default) or "panel". -# "pill" retires the standalone side palette: drawing properties live in -# the top strip's contextual style pill, canvas management in the status -# HUD and board picker, and the Session/Settings panes in popovers opened -# from the top strip's overflow menu ("Session..." / "Settings..."). -# "panel" is the legacy escape hatch restoring the classic four-pane side -# palette (side_active_pane, collapsed_sections, side_pinned, and -# side_minimized apply only under "panel"); it is deprecated and planned -# for removal one release after the pill default -side_layout = "pill" - -# Side-palette pane restored at startup: "draw", "canvas", "session", or "settings" -# The overlay updates this as you switch panes (legacy side_layout = "panel" only) -side_active_pane = "draw" - -# Side-palette sections collapsed to their header row -# The overlay updates this as you collapse/expand sections -collapsed_sections = [] +# The unified top toolbar is the only layout. Drawing properties live in +# the top strip's contextual style pill; canvas management in the status +# HUD, board picker, and Canvas overflow popover; Session/Settings in +# popovers from the top strip's overflow menu. Older keys such as +# side_layout, side_pinned, side_minimized, side_active_pane, +# collapsed_sections, side_offset(_x), show_settings_section, and the +# retired items.order lists (side_sections, actions, pages, boards, +# presets, tool_options, sessions) remain readable and are preserved on +# save, but they no longer affect the running overlay. # Use icons instead of text labels in toolbars # Icons are more compact; text labels are more explicit @@ -565,7 +552,7 @@ scale = 1.0 # When enabled, displays additional color options beyond the basic 8 show_more_colors = false -# Show basic actions (undo/redo/clear) in side toolbar +# Show basic actions (undo/redo/clear) in the Canvas overflow popover show_actions_section = true # Show advanced actions (undo all, delay, freeze, etc.) @@ -591,7 +578,7 @@ show_pages_section = true # Show board controls section (prev/next/new/del) show_boards_section = true -# Show presets section in side toolbar +# Show presets island in the top strip show_presets = true # Show step undo/redo section @@ -600,15 +587,11 @@ show_step_section = false # Keep text controls visible even when text is inactive show_text_controls = true -# Deprecated compatibility mirror. Settings navigation is always reachable; -# this value is preserved for older Wayscriber versions but is otherwise ignored. -show_settings_section = true - -# Show timing delay sliders in side toolbar +# Show timing delay sliders in the Canvas popover's Step section # Allows configuring delayed undo/redo all timing show_delay_sliders = false -# Show marker opacity slider in side toolbar even when the marker tool isn't active +# Keep the marker opacity control available in the style pill even when the marker tool isn't active # Enable to keep marker transparency control visible at all times show_marker_opacity_section = false @@ -623,17 +606,13 @@ show_preset_toasts = true # Show cursor tool preview bubble near the pointer show_tool_preview = false -# Authored default toolbar offsets (layer-shell/inline). Dragging a toolbar -# persists its position to runtime-ui.toml instead of rewriting these; +# Authored default top-toolbar offsets (layer-shell/inline). Dragging the +# strip persists its position to runtime-ui.toml instead of rewriting these; # editing these values again takes over from the saved drag. # Horizontal offset for the top toolbar top_offset = 0.0 # Vertical offset for the top toolbar (move top bar down/up) top_offset_y = 0.0 -# Vertical offset for the side toolbar -side_offset = 0.0 -# Horizontal offset for the side toolbar (move side bar left/right) -side_offset_x = 0.0 # Force inline toolbars even when layer-shell is available force_inline = false @@ -663,8 +642,8 @@ shown = [] [ui.toolbar.items.order] # Optional order overrides. Empty lists use the built-in order. # Known IDs omitted from a non-empty list are appended in the default order. -# Side section ordering uses runtime block representatives; detailed sections -# such as eraser-mode, polygon-sides, and font remain visibility-only IDs. +# Historically named side.* item IDs still customize top-toolbar controls +# (Canvas/Session/Settings popovers, section visibility); do not rename them. top_tools = [ "top.tool.select", "top.tool.pen", @@ -684,21 +663,6 @@ top_controls = [ "top.utility.clear-canvas", "top.utility.highlight", ] -side_sections = [ - "side.group.colors", - "side.group.presets", - "side.group.thickness", - "side.group.arrow-labels", - "side.group.step-markers", - "side.group.marker-opacity", - "side.group.text-size", - "side.group.actions", - "side.group.boards", - "side.group.pages", - "side.group.step-undo", - "side.group.session", - "side.group.settings", -] # ─────────────────────────────────────────────────────────────────────────────── # Status Bar Styling @@ -815,7 +779,7 @@ hide_toolbars = true hide_tool_preview = true # What hide_toolbars does to the top strip: "hidden" removes it entirely, -# "micro" collapses it to the 44px micro chip (side toolbars always hide) +# "micro" collapses it to the 44px micro chip toolbar_mode = "hidden" # Close the help overlay when entering presenter mode diff --git a/configurator/README.md b/configurator/README.md index aa300ccb..9fe7c57e 100644 --- a/configurator/README.md +++ b/configurator/README.md @@ -43,12 +43,11 @@ Destinations are `ui/toolbar`, `ui/toolbar-visibility`, `ui/status-bar`, `ui/cli search box filled. `--help` prints the same list. An unknown destination falls back to the normal initial screen and says so in the status banner. -Toolbar pins, placement offsets, the top strip's display form, item visibility/order, pane state, -and board pin controls are labeled as configured defaults because the running overlay can store -later customizations in the separate generated -`$XDG_DATA_HOME/wayscriber/runtime-ui.toml` file. The configurator edits only `config.toml`; it does -not overwrite or reset runtime preferences. Use the overlay Settings panel to inspect or reset that -state. +Toolbar pin, top placement offsets, the top strip's display form, item visibility/order, and board +pin controls are labeled as configured defaults because the running overlay can store later +customizations in the separate generated `$XDG_DATA_HOME/wayscriber/runtime-ui.toml` file. The +configurator edits only `config.toml`; it does not overwrite or reset runtime preferences. Use the +overlay Settings popover to inspect or reset that state. Config, daemon-setup, and saved-session filesystem/process operations run through a bounded Tokio blocking-job adapter. Two jobs may run concurrently; existing request ordering and busy-state gates diff --git a/configurator/src/app/pages/ui/toolbar.rs b/configurator/src/app/pages/ui/toolbar.rs index c2232a30..aecae8ad 100644 --- a/configurator/src/app/pages/ui/toolbar.rs +++ b/configurator/src/app/pages/ui/toolbar.rs @@ -3,7 +3,7 @@ use relm4::ComponentSender; use crate::messages::Message; use crate::models::{ OverrideOption, TabId, TextField, ToggleField, ToolbarLayoutModeOption, ToolbarOverrideField, - ToolbarRebindModifierOption, ToolbarSideLayoutOption, ZoomChipDisplayOption, + ToolbarRebindModifierOption, ZoomChipDisplayOption, }; use super::super::super::state::ConfiguratorApp; @@ -13,8 +13,6 @@ use super::{note, options}; pub(super) fn build(sender: &ComponentSender) -> BuiltPage { let (layout_modes, layout_labels) = options(ToolbarLayoutModeOption::list(), |value| value.label()); - let (side_layouts, side_layout_labels) = - options(ToolbarSideLayoutOption::list(), |value| value.label()); let (zoom_chips, zoom_chip_labels) = options(ZoomChipDisplayOption::list(), |value| value.label()); let (rebinds, rebind_labels) = options(ToolbarRebindModifierOption::ALL.to_vec(), |value| { @@ -26,7 +24,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { let mut page = PageBuilder::new(sender, TabId::Ui); page.group("Toolbar").custom(¬e( - "These settings are configured defaults. Toolbar pins, position, display form, item visibility/order, pane state, and board pins changed in the overlay are saved separately as runtime preferences.", + "These settings are configured defaults. Toolbar pin, position, display form, item visibility/order, and board pins changed in the overlay are saved separately as runtime preferences.", )); page.group("Layout") @@ -38,14 +36,6 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { |app| app.draft.ui_toolbar_layout_mode, Message::ToolbarLayoutModeChanged, ) - .combo_row( - "Side layout", - "Pill (the default) retires the side palette: drawing properties live in the top strip's style pill, canvas management in the status HUD and board picker, and Session/Settings in popovers on the top strip's overflow menu. Panel is the legacy escape hatch restoring the classic side palette; it is deprecated and planned for removal one release after the pill default.", - side_layouts, - side_layout_labels, - |app| app.draft.ui_toolbar_side_layout, - Message::ToolbarSideLayoutChanged, - ) .combo_row( "Zoom chip", "", @@ -74,12 +64,6 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { |app| app.draft.ui_toolbar_top_pinned, |value| Message::ToggleChanged(ToggleField::UiToolbarTopPinned, value), ) - .switch_row( - "Configured default: pin side toolbar", - "", - |app| app.draft.ui_toolbar_side_pinned, - |value| Message::ToggleChanged(ToggleField::UiToolbarSidePinned, value), - ) .switch_row( "Use icon-only buttons", "", @@ -213,16 +197,6 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { |app| app.draft.ui_toolbar_top_offset_y.clone(), |value| Message::TextChanged(TextField::ToolbarTopOffsetY, value), ) - .entry_row( - "Side offset Y (px)", - |app| app.draft.ui_toolbar_side_offset.clone(), - |value| Message::TextChanged(TextField::ToolbarSideOffset, value), - ) - .entry_row( - "Side offset X (px)", - |app| app.draft.ui_toolbar_side_offset_x.clone(), - |value| Message::TextChanged(TextField::ToolbarSideOffsetX, value), - ) .custom(¬e( "Configured defaults. Dragging a toolbar in the overlay saves that position as a runtime preference; editing a value here takes over from the saved drag.", )); diff --git a/configurator/src/app/pages/ui/toolbar_visibility.rs b/configurator/src/app/pages/ui/toolbar_visibility.rs index d9e4c768..3687f43f 100644 --- a/configurator/src/app/pages/ui/toolbar_visibility.rs +++ b/configurator/src/app/pages/ui/toolbar_visibility.rs @@ -219,9 +219,7 @@ pub(super) fn build(sender: &ComponentSender) -> BuiltPage { fn item_sections(built_in: &ResolvedToolbarItems) -> Vec { let mut sections: Vec = Vec::new(); for definition in toolbar_item_definitions() { - if definition.id == toolbar_item_ids::SIDE_GROUP_SETTINGS - || definition.id == toolbar_item_ids::TOP_CHROME_OVERFLOW - { + if definition.id == toolbar_item_ids::TOP_CHROME_OVERFLOW { continue; } @@ -277,10 +275,6 @@ fn configurator_order_group( ToolbarItemOrderGroup::TopControls => { Some((ToolbarItemOrderGroup::TopControls, "Controls")) } - ToolbarItemOrderGroup::SideSections => { - Some((ToolbarItemOrderGroup::SideSections, "Sections")) - } - _ => None, } } @@ -333,7 +327,7 @@ fn visibility_label(visible: bool) -> &'static str { fn surface_label(surface: ToolbarItemSurface) -> &'static str { match surface { ToolbarItemSurface::Top => "Top toolbar", - ToolbarItemSurface::Side => "Side toolbar", + ToolbarItemSurface::Popover => "Popover sections", } } @@ -348,6 +342,5 @@ fn category_label(category: ToolbarItemCategory) -> &'static str { ToolbarItemCategory::Board => "Boards", ToolbarItemCategory::Setting => "Settings", ToolbarItemCategory::Session => "Sessions", - ToolbarItemCategory::ToolOption => "Tool options", } } diff --git a/configurator/src/app/search/terms.rs b/configurator/src/app/search/terms.rs index 9a9e9a59..6ca7b008 100644 --- a/configurator/src/app/search/terms.rs +++ b/configurator/src/app/search/terms.rs @@ -176,10 +176,7 @@ pub(super) const UI_GENERAL_TERMS: &[&str] = &[ pub(super) const UI_TOOLBAR_TERMS: &[&str] = &[ "toolbar", "layout mode", - "side layout", - "side palette", "style pill", - "panel", "zoom chip", "zoom chip display", "show zoom chip", @@ -188,9 +185,7 @@ pub(super) const UI_TOOLBAR_TERMS: &[&str] = &[ "shortcut edit click", "toolbar rebind modifier", "pin top toolbar", - "pin side toolbar", "configured default pin top toolbar", - "configured default pin side toolbar", "runtime preferences", "use icon-only buttons", "show extended colors", @@ -212,8 +207,6 @@ pub(super) const UI_TOOLBAR_TERMS: &[&str] = &[ "placement offsets", "top offset x", "top offset y", - "side offset x", - "side offset y", ]; pub(super) const UI_TOOLBAR_VISIBILITY_TERMS: &[&str] = &[ "toolbar visibility", @@ -227,7 +220,7 @@ pub(super) const UI_TOOLBAR_VISIBILITY_TERMS: &[&str] = &[ "show toolbar buttons", "visible toolbar buttons", "top toolbar", - "side toolbar", + "popover sections", "toolbar controls", "tools", "utilities", @@ -238,7 +231,6 @@ pub(super) const UI_TOOLBAR_VISIBILITY_TERMS: &[&str] = &[ "presets", "settings", "sessions", - "tool options", ]; pub(super) const UI_STATUS_BAR_TERMS: &[&str] = &[ "status bar", diff --git a/configurator/src/app/update/config/status.rs b/configurator/src/app/update/config/status.rs index 6b9f2c9d..ff95de3f 100644 --- a/configurator/src/app/update/config/status.rs +++ b/configurator/src/app/update/config/status.rs @@ -30,6 +30,7 @@ pub(super) fn config_document_status(document: &ConfigDocument, success: &str) - let mut message = success.to_string(); let mut unknown = Vec::new(); + let mut retired = Vec::new(); let mut conflicts = Vec::new(); let mut invalid = Vec::new(); let mut skipped_defaults = Vec::new(); @@ -39,6 +40,7 @@ pub(super) fn config_document_status(document: &ConfigDocument, success: &str) - for diagnostic in diagnostics { match diagnostic.kind() { ConfigDiagnosticKind::UnknownSetting => unknown.push(diagnostic.path().to_string()), + ConfigDiagnosticKind::RetiredSetting => retired.push(diagnostic.path().to_string()), // Every keybinding kind is resolved in memory only, so the file // the editor is showing still contains them: carry the diagnostic's // own wording, which names the actions, instead of just the path. @@ -56,6 +58,12 @@ pub(super) fn config_document_status(document: &ConfigDocument, success: &str) - list_with_overflow(&borrowed(&unknown), ", ") )); } + if !retired.is_empty() { + message.push_str(&format!( + "\nRetired toolbar settings were preserved but are no longer used and can be removed: {}.", + list_with_overflow(&borrowed(&retired), ", ") + )); + } if !invalid.is_empty() { message.push_str(&format!( "\nShortcuts that could not be parsed are ignored for the running session; the file still has them: {}.", diff --git a/configurator/src/app/update/fields/ui.rs b/configurator/src/app/update/fields/ui.rs index 6075612c..abbeb980 100644 --- a/configurator/src/app/update/fields/ui.rs +++ b/configurator/src/app/update/fields/ui.rs @@ -3,7 +3,7 @@ use wayscriber::config::{ToolbarItemId, ToolbarItemOrderGroup}; use crate::models::{ InputHudModeOption, InputHudPositionOption, OverrideOption, ReducedMotionOption, StatusPositionOption, ToolbarLayoutModeOption, ToolbarOverrideField, - ToolbarRebindModifierOption, ToolbarSideLayoutOption, UiThemeOption, ZoomChipDisplayOption, + ToolbarRebindModifierOption, UiThemeOption, ZoomChipDisplayOption, }; use super::super::super::effects::Effect; @@ -70,16 +70,6 @@ impl ConfiguratorApp { Vec::new() } - pub(in crate::app::update) fn handle_toolbar_side_layout_changed( - &mut self, - option: ToolbarSideLayoutOption, - ) -> Vec { - self.status = StatusMessage::idle(); - self.draft.ui_toolbar_side_layout = option; - self.refresh_dirty_flag(); - Vec::new() - } - pub(in crate::app::update) fn handle_toolbar_zoom_chip_display_changed( &mut self, option: ZoomChipDisplayOption, diff --git a/configurator/src/app/update/mod.rs b/configurator/src/app/update/mod.rs index 6dc52fc9..48c5896d 100644 --- a/configurator/src/app/update/mod.rs +++ b/configurator/src/app/update/mod.rs @@ -138,9 +138,6 @@ impl ConfiguratorApp { Message::ToolbarLayoutModeChanged(option) => { self.handle_toolbar_layout_mode_changed(option) } - Message::ToolbarSideLayoutChanged(option) => { - self.handle_toolbar_side_layout_changed(option) - } Message::ToolbarZoomChipDisplayChanged(option) => { self.handle_toolbar_zoom_chip_display_changed(option) } diff --git a/configurator/src/messages.rs b/configurator/src/messages.rs index c459868e..4d8d8603 100644 --- a/configurator/src/messages.rs +++ b/configurator/src/messages.rs @@ -15,8 +15,7 @@ use crate::models::{ RenderProfileTextField, SessionCatalogActionResult, SessionCatalogItem, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption, TabId, TextField, ToggleField, ToolOption, ToolbarLayoutModeOption, ToolbarOverrideField, - ToolbarRebindModifierOption, ToolbarSideLayoutOption, UiTabId, UiThemeOption, - ZoomChipDisplayOption, + ToolbarRebindModifierOption, UiTabId, UiThemeOption, ZoomChipDisplayOption, }; #[cfg(feature = "tablet-input")] use crate::models::{PressureThicknessEditModeOption, PressureThicknessEntryModeOption}; @@ -113,7 +112,6 @@ pub enum Message { UiThemeChanged(UiThemeOption), UiReducedMotionChanged(ReducedMotionOption), ToolbarLayoutModeChanged(ToolbarLayoutModeOption), - ToolbarSideLayoutChanged(ToolbarSideLayoutOption), ToolbarZoomChipDisplayChanged(ZoomChipDisplayOption), ToolbarRebindModifierChanged(ToolbarRebindModifierOption), ToolbarOverrideModeChanged(ToolbarLayoutModeOption), diff --git a/configurator/src/models/config/draft/from_config.rs b/configurator/src/models/config/draft/from_config.rs index ff488826..44afe3f4 100644 --- a/configurator/src/models/config/draft/from_config.rs +++ b/configurator/src/models/config/draft/from_config.rs @@ -1,4 +1,5 @@ use super::super::super::color::{ColorInput, ColorQuadInput}; +use super::super::super::fields::ZoomChipDisplayOption; use super::super::super::fields::{ EraserModeOption, FontStyleOption, FontWeightOption, InputHudModeOption, InputHudPositionOption, PdfFitModeOption, PdfLabelContentModeOption, PdfLabelPositionOption, @@ -11,7 +12,6 @@ use super::super::super::fields::{ use super::super::super::fields::{ PressureThicknessEditModeOption, PressureThicknessEntryModeOption, }; -use super::super::super::fields::{ToolbarSideLayoutOption, ZoomChipDisplayOption}; use super::super::super::keybindings::KeybindingsDraft; use super::super::super::util::format_float; use super::super::boards::BoardsDraft; @@ -38,7 +38,6 @@ impl ConfigDraft { show_presets: config.ui.toolbar.show_presets, show_step_section: config.ui.toolbar.show_step_section, show_text_controls: config.ui.toolbar.show_text_controls, - show_settings_section: config.ui.toolbar.show_settings_section, }; legacy_toolbar_visibility.apply_mode_override( config @@ -146,16 +145,12 @@ impl ConfigDraft { .command_palette_toast_duration_ms .to_string(), ui_toolbar_top_pinned: config.ui.toolbar.top_pinned, - ui_toolbar_side_pinned: config.ui.toolbar.side_pinned, 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_layout_mode: ToolbarLayoutModeOption::from_mode( config.ui.toolbar.layout_mode, ), - ui_toolbar_side_layout: ToolbarSideLayoutOption::from_config( - config.ui.toolbar.side_layout, - ), ui_toolbar_zoom_chip_display: ZoomChipDisplayOption::from_config( config.ui.toolbar.zoom_chip_display, ), @@ -172,15 +167,12 @@ impl ConfigDraft { ui_toolbar_show_boards_section: toolbar_visibility.show_boards_section, ui_toolbar_show_step_section: toolbar_visibility.show_step_section, ui_toolbar_show_text_controls: toolbar_visibility.show_text_controls, - ui_toolbar_show_settings_section: toolbar_visibility.show_settings_section, ui_toolbar_show_delay_sliders: config.ui.toolbar.show_delay_sliders, ui_toolbar_show_marker_opacity_section: config.ui.toolbar.show_marker_opacity_section, ui_toolbar_show_tool_preview: config.ui.toolbar.show_tool_preview, ui_toolbar_force_inline: config.ui.toolbar.force_inline, ui_toolbar_top_offset: format_float(config.ui.toolbar.top_offset), ui_toolbar_top_offset_y: format_float(config.ui.toolbar.top_offset_y), - ui_toolbar_side_offset: format_float(config.ui.toolbar.side_offset), - ui_toolbar_side_offset_x: format_float(config.ui.toolbar.side_offset_x), ui_toolbar_mode_overrides: ToolbarModeOverridesDraft::from_config( &config.ui.toolbar.mode_overrides, ), diff --git a/configurator/src/models/config/draft/mod.rs b/configurator/src/models/config/draft/mod.rs index 307252b3..dbc71784 100644 --- a/configurator/src/models/config/draft/mod.rs +++ b/configurator/src/models/config/draft/mod.rs @@ -1,6 +1,7 @@ mod from_config; use super::super::color::{ColorInput, ColorQuadInput}; +use super::super::fields::ZoomChipDisplayOption; use super::super::fields::{ EraserModeOption, FontStyleOption, FontWeightOption, InputHudModeOption, InputHudPositionOption, PdfFitModeOption, PdfLabelContentModeOption, PdfLabelPositionOption, @@ -11,7 +12,6 @@ use super::super::fields::{ }; #[cfg(feature = "tablet-input")] use super::super::fields::{PressureThicknessEditModeOption, PressureThicknessEntryModeOption}; -use super::super::fields::{ToolbarSideLayoutOption, ZoomChipDisplayOption}; use super::super::keybindings::KeybindingsDraft; use super::boards::BoardsDraft; use super::presets::PresetsDraft; @@ -99,12 +99,10 @@ pub struct ConfigDraft { pub ui_xdg_keep_on_focus_loss: bool, pub ui_command_palette_toast_duration_ms: String, pub ui_toolbar_top_pinned: bool, - pub ui_toolbar_side_pinned: bool, pub ui_toolbar_use_icons: bool, pub ui_toolbar_show_more_colors: bool, pub ui_toolbar_show_preset_toasts: bool, pub ui_toolbar_layout_mode: ToolbarLayoutModeOption, - pub ui_toolbar_side_layout: ToolbarSideLayoutOption, pub ui_toolbar_zoom_chip_display: ZoomChipDisplayOption, pub ui_toolbar_show_zoom_chip: bool, pub ui_toolbar_rebind_modifier: ToolbarRebindModifierOption, @@ -117,15 +115,12 @@ pub struct ConfigDraft { pub ui_toolbar_show_boards_section: bool, pub ui_toolbar_show_step_section: bool, pub ui_toolbar_show_text_controls: bool, - pub ui_toolbar_show_settings_section: bool, pub ui_toolbar_show_delay_sliders: bool, pub ui_toolbar_show_marker_opacity_section: bool, pub ui_toolbar_show_tool_preview: bool, pub ui_toolbar_force_inline: bool, pub ui_toolbar_top_offset: String, pub ui_toolbar_top_offset_y: String, - pub ui_toolbar_side_offset: String, - pub ui_toolbar_side_offset_x: String, pub ui_toolbar_mode_overrides: ToolbarModeOverridesDraft, pub ui_status_position: StatusPositionOption, pub status_font_size: String, diff --git a/configurator/src/models/config/setters.rs b/configurator/src/models/config/setters.rs index f32d06fd..9aa87d88 100644 --- a/configurator/src/models/config/setters.rs +++ b/configurator/src/models/config/setters.rs @@ -32,7 +32,6 @@ impl ConfigDraft { self.ui_toolbar_show_presets = visibility.show_presets; self.ui_toolbar_show_step_section = visibility.show_step_section; self.ui_toolbar_show_text_controls = visibility.show_text_controls; - self.ui_toolbar_show_settings_section = visibility.show_settings_section; } fn set_toolbar_section_visible(&mut self, flag: ToolbarSectionFlag, visible: bool) { @@ -187,7 +186,6 @@ impl ConfigDraft { ToggleField::UiXdgFullscreen => self.ui_xdg_fullscreen = value, ToggleField::UiXdgKeepOnFocusLoss => self.ui_xdg_keep_on_focus_loss = value, ToggleField::UiToolbarTopPinned => self.ui_toolbar_top_pinned = value, - ToggleField::UiToolbarSidePinned => self.ui_toolbar_side_pinned = value, 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, @@ -385,8 +383,6 @@ impl ConfigDraft { TextField::ExportPdfLabelPaddingY => self.export_pdf_label_padding_y = value, TextField::ToolbarTopOffset => self.ui_toolbar_top_offset = value, TextField::ToolbarTopOffsetY => self.ui_toolbar_top_offset_y = value, - TextField::ToolbarSideOffset => self.ui_toolbar_side_offset = value, - TextField::ToolbarSideOffsetX => self.ui_toolbar_side_offset_x = value, TextField::BoardsMaxCount => self.boards.max_count = value, TextField::SessionCustomDirectory => self.session_custom_directory = value, TextField::SessionMaxShapesPerFrame => self.session_max_shapes_per_frame = value, diff --git a/configurator/src/models/config/tests.rs b/configurator/src/models/config/tests.rs index ce317eb9..d0c4c62c 100644 --- a/configurator/src/models/config/tests.rs +++ b/configurator/src/models/config/tests.rs @@ -893,40 +893,6 @@ fn section_toggle_replaces_overlay_item_override_on_save() { ); } -#[test] -fn config_draft_round_trips_toolbar_side_layout() { - use super::super::fields::ToolbarSideLayoutOption; - use wayscriber::config::ToolbarSideLayout; - - // The default draft mirrors the config default: pill (the - // Session/Settings panes live in top-strip overflow popovers). - let default_draft = ConfigDraft::from_config(&Config::default()); - assert_eq!( - default_draft.ui_toolbar_side_layout, - ToolbarSideLayoutOption::Pill - ); - let saved = default_draft - .to_config(&Config::default()) - .expect("default draft should convert"); - assert_eq!(saved.ui.toolbar.side_layout, ToolbarSideLayout::Pill); - - // The pick list leads with the default and the legacy escape hatch - // round-trips through the draft. - assert_eq!( - ToolbarSideLayoutOption::list(), - vec![ - ToolbarSideLayoutOption::Pill, - ToolbarSideLayoutOption::Panel, - ] - ); - let mut config = Config::default(); - config.ui.toolbar.side_layout = ToolbarSideLayout::Panel; - let draft = ConfigDraft::from_config(&config); - assert_eq!(draft.ui_toolbar_side_layout, ToolbarSideLayoutOption::Panel); - let saved = draft.to_config(&config).expect("draft should convert"); - assert_eq!(saved.ui.toolbar.side_layout, ToolbarSideLayout::Panel); -} - #[test] fn config_draft_round_trips_zoom_chip_display() { use super::super::fields::ZoomChipDisplayOption; diff --git a/configurator/src/models/config/to_config/ui.rs b/configurator/src/models/config/to_config/ui.rs index cdcd1252..23703ab8 100644 --- a/configurator/src/models/config/to_config/ui.rs +++ b/configurator/src/models/config/to_config/ui.rs @@ -44,12 +44,10 @@ impl ConfigDraft { |value| config.ui.command_palette_toast_duration_ms = value, ); config.ui.toolbar.top_pinned = self.ui_toolbar_top_pinned; - config.ui.toolbar.side_pinned = self.ui_toolbar_side_pinned; 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.layout_mode = self.ui_toolbar_layout_mode.to_mode(); - config.ui.toolbar.side_layout = self.ui_toolbar_side_layout.to_config(); 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; config.ui.toolbar.rebind_modifier = self.ui_toolbar_rebind_modifier.to_config(); @@ -68,7 +66,6 @@ impl ConfigDraft { config.ui.toolbar.show_boards_section = visibility.show_boards_section; config.ui.toolbar.show_step_section = visibility.show_step_section; config.ui.toolbar.show_text_controls = visibility.show_text_controls; - config.ui.toolbar.show_settings_section = visibility.show_settings_section; config.ui.toolbar.show_delay_sliders = self.ui_toolbar_show_delay_sliders; config.ui.toolbar.show_marker_opacity_section = self.ui_toolbar_show_marker_opacity_section; config.ui.toolbar.show_tool_preview = self.ui_toolbar_show_tool_preview; @@ -85,18 +82,6 @@ impl ConfigDraft { errors, |value| config.ui.toolbar.top_offset_y = value, ); - parse_field( - &self.ui_toolbar_side_offset, - "ui.toolbar.side_offset", - errors, - |value| config.ui.toolbar.side_offset = value, - ); - parse_field( - &self.ui_toolbar_side_offset_x, - "ui.toolbar.side_offset_x", - errors, - |value| config.ui.toolbar.side_offset_x = value, - ); config.ui.status_bar_position = self.ui_status_position.to_status_position(); parse_field( &self.status_font_size, diff --git a/configurator/src/models/config/toolbar_overrides.rs b/configurator/src/models/config/toolbar_overrides.rs index 0e149aa3..eb0ca896 100644 --- a/configurator/src/models/config/toolbar_overrides.rs +++ b/configurator/src/models/config/toolbar_overrides.rs @@ -11,7 +11,6 @@ pub struct ToolbarModeOverrideDraft { pub show_boards_section: OverrideOption, pub show_step_section: OverrideOption, pub show_text_controls: OverrideOption, - pub show_settings_section: OverrideOption, } impl ToolbarModeOverrideDraft { @@ -25,7 +24,6 @@ impl ToolbarModeOverrideDraft { show_boards_section: OverrideOption::from_option(override_cfg.show_boards_section), show_step_section: OverrideOption::from_option(override_cfg.show_step_section), show_text_controls: OverrideOption::from_option(override_cfg.show_text_controls), - show_settings_section: OverrideOption::from_option(override_cfg.show_settings_section), } } @@ -39,7 +37,6 @@ impl ToolbarModeOverrideDraft { show_presets: self.show_presets.to_option(), show_step_section: self.show_step_section.to_option(), show_text_controls: self.show_text_controls.to_option(), - show_settings_section: self.show_settings_section.to_option(), } } diff --git a/configurator/src/models/fields/mod.rs b/configurator/src/models/fields/mod.rs index 30ae83c2..228d610c 100644 --- a/configurator/src/models/fields/mod.rs +++ b/configurator/src/models/fields/mod.rs @@ -29,7 +29,7 @@ pub use toggles::{PresetTextField, PresetToggleField, QuadField, TextField, Togg pub use tool::{DragColorOption, DragMouseButton, DragToolField, DragToolOption, ToolOption}; pub use toolbar::{ OverrideOption, ToolbarLayoutModeOption, ToolbarOverrideField, ToolbarRebindModifierOption, - ToolbarSideLayoutOption, ZoomChipDisplayOption, + ZoomChipDisplayOption, }; #[cfg(test)] diff --git a/configurator/src/models/fields/toggles.rs b/configurator/src/models/fields/toggles.rs index 0069db5d..2b014355 100644 --- a/configurator/src/models/fields/toggles.rs +++ b/configurator/src/models/fields/toggles.rs @@ -20,7 +20,6 @@ pub enum ToggleField { UiXdgFullscreen, UiXdgKeepOnFocusLoss, UiToolbarTopPinned, - UiToolbarSidePinned, UiToolbarUseIcons, UiToolbarShowMoreColors, UiToolbarPresetToasts, @@ -150,8 +149,6 @@ pub enum TextField { ExportPdfLabelPaddingY, ToolbarTopOffset, ToolbarTopOffsetY, - ToolbarSideOffset, - ToolbarSideOffsetX, BoardsMaxCount, SessionCustomDirectory, SessionMaxShapesPerFrame, diff --git a/configurator/src/models/fields/toolbar.rs b/configurator/src/models/fields/toolbar.rs index c40220c8..a4ac5e30 100644 --- a/configurator/src/models/fields/toolbar.rs +++ b/configurator/src/models/fields/toolbar.rs @@ -1,6 +1,4 @@ -use wayscriber::config::{ - ToolbarLayoutMode, ToolbarRebindModifier, ToolbarSideLayout, ZoomChipDisplay, -}; +use wayscriber::config::{ToolbarLayoutMode, ToolbarRebindModifier, ZoomChipDisplay}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ToolbarRebindModifierOption { @@ -104,45 +102,6 @@ impl std::fmt::Display for ToolbarLayoutModeOption { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ToolbarSideLayoutOption { - Pill, - Panel, -} - -impl ToolbarSideLayoutOption { - pub fn list() -> Vec { - vec![Self::Pill, Self::Panel] - } - - pub fn label(&self) -> &'static str { - match self { - Self::Pill => "Pill (default)", - Self::Panel => "Panel (legacy)", - } - } - - pub fn to_config(self) -> ToolbarSideLayout { - match self { - Self::Pill => ToolbarSideLayout::Pill, - Self::Panel => ToolbarSideLayout::Panel, - } - } - - pub fn from_config(value: ToolbarSideLayout) -> Self { - match value { - ToolbarSideLayout::Pill => Self::Pill, - ToolbarSideLayout::Panel => Self::Panel, - } - } -} - -impl std::fmt::Display for ToolbarSideLayoutOption { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.label()) - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ZoomChipDisplayOption { Always, diff --git a/configurator/src/models/mod.rs b/configurator/src/models/mod.rs index bd6862e7..948dd09e 100644 --- a/configurator/src/models/mod.rs +++ b/configurator/src/models/mod.rs @@ -30,8 +30,8 @@ pub use fields::{ PresenterToolbarModeOption, PresetEraserKindOption, PresetEraserModeOption, PresetTextField, PresetToggleField, QuadField, ReducedMotionOption, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption, TextField, ToggleField, ToolOption, - ToolbarLayoutModeOption, ToolbarOverrideField, ToolbarRebindModifierOption, - ToolbarSideLayoutOption, UiThemeOption, ZoomChipDisplayOption, + ToolbarLayoutModeOption, ToolbarOverrideField, ToolbarRebindModifierOption, UiThemeOption, + ZoomChipDisplayOption, }; #[cfg(feature = "tablet-input")] pub use fields::{PressureThicknessEditModeOption, PressureThicknessEntryModeOption}; diff --git a/docs/CONFIG.md b/docs/CONFIG.md index d872fbfa..86271c0e 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -14,14 +14,11 @@ All settings are optional. If the configuration file doesn't exist or settings a `config.toml` is the authored source for configured defaults. Some direct overlay customizations are saved separately so moving through the UI does not rewrite unrelated configuration: -- top/side toolbar pin and minimized state (the toolbar show/hide keybinding updates both - remembered pins at once, so the next start matches what was on screen; implicit hides — +- top toolbar pin and minimized state (the toolbar show/hide keybinding updates the + remembered pin, so the next start matches what was on screen; implicit hides — presenter mode, focus mode — remain run-only); - the top strip's display form reached with the cycle keybinding or the micro chip; - the top toolbar's dragged position; -- the side toolbar's dragged position (a side drag also records the top strip's reconciled - horizontal offset, because moving the palette can change where the strip rests); -- the active side pane and collapsed side sections; - 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, @@ -158,11 +155,10 @@ Both `config.toml` mechanisms leave a timestamped `.bak` beside the file. | You do this | It is saved to | By | | --- | --- | --- | -| Drag the top or side toolbar | `runtime-ui.toml` | Runtime-UI writer | +| Drag the top toolbar | `runtime-ui.toml` | Runtime-UI writer | | Cycle the top strip full ⇄ micro (F2 or the micro chip) | `runtime-ui.toml` | Runtime-UI writer | -| Show or hide the toolbars (F9) | `runtime-ui.toml` — both toolbar pins at once | Runtime-UI writer | -| Pin, unpin, or minimize a toolbar | `runtime-ui.toml` | Runtime-UI writer | -| Switch the side pane or collapse a side section | `runtime-ui.toml` | Runtime-UI writer | +| Show or hide the toolbar (F9) | `runtime-ui.toml` — top pin | Runtime-UI writer | +| Pin, unpin, or minimize the top toolbar | `runtime-ui.toml` | Runtime-UI writer | | Hide, show, or reorder an individual toolbar item | `runtime-ui.toml` | Runtime-UI writer | | Pin a board | `runtime-ui.toml` | Runtime-UI writer | | Switch layout mode (Simple/Regular/Advanced) in the overlay — the Settings segments or the strip's layout button | `runtime-ui.toml` | Runtime-UI writer | @@ -373,7 +369,7 @@ drag_tool = "default" - **Eraser mode**: Use Ctrl+Shift+E to toggle brush vs stroke erasing - **Blur style**: Run **Cycle Blur Style** from the command palette to step through blur → pixelate → secure → black out (unbound by default; bind `cycle_blur_style`) - **Marker opacity**: Use Ctrl+Alt + / -- **Regular polygon sides**: Use the side toolbar Sides control (range: 3-12) +- **Regular polygon sides**: Use the Shapes popover Sides control (range: 3-12) - **Font size**: Use Ctrl+Shift++/Ctrl+Shift+- or Shift + scroll (range: 8-72px) **Defaults:** @@ -521,7 +517,7 @@ custom_redo_steps = 5 **Notes:** - `undo_all_delay_ms` / `redo_all_delay_ms` drive the "Undo all (delay)" and "Redo all (delay)" toolbar actions. -- `custom_section_enabled` reveals the Step buttons in the side toolbar; those buttons use the custom delays and step counts above. +- `custom_section_enabled` reveals the Step buttons in the Canvas overflow popover; those buttons use the custom delays and step counts above. ### `[performance]` - Performance Tuning @@ -956,8 +952,8 @@ restores the previous value on exit; while it is forced, the manual toggle is ignored (the same contract `enable_click_highlight` follows). **Toolbar mode options** (what `hide_toolbars` does to the top strip): -- `"hidden"` (default): hide the top strip along with the side toolbars -- `"micro"`: collapse the top strip to the 44px micro chip (active tool glyph in a ring of the current color); side toolbars still hide +- `"hidden"` (default): hide the top strip +- `"micro"`: collapse the top strip to the 44px micro chip (active tool glyph in a ring of the current color) **Tool behavior options:** - `"keep"`: Leave the active tool unchanged @@ -983,7 +979,7 @@ Use `--light-draw-on` on key/button press and `--light-draw-off` on release for ### `[ui.toolbar]` - Floating Toolbars -Controls the top and side toolbars (F9 toggles both; F2 cycles the top strip full → micro → hidden). +Controls the unified top toolbar (F9 toggles visibility; F2 cycles the top strip full → micro → hidden). ```toml [ui.toolbar] @@ -1031,12 +1027,8 @@ layout_mode = "regular" # Show top toolbar on startup (pinned) top_pinned = true -# Show side toolbar on startup (pinned) -side_pinned = true - -# Start toolbars minimized to their edge restore tabs +# Start the top toolbar minimized to its edge restore tab top_minimized = false -side_minimized = false # Authored default display form of the top strip: "full" or "micro". # "hidden" is accepted but treated as "full" (startup visibility is @@ -1044,20 +1036,12 @@ side_minimized = false # form to runtime-ui.toml instead of rewriting this value top_display_mode = "full" -# Where the side-palette functions live: "pill" (default, supported) or -# "panel" ("pill" retires the side palette — drawing props live in the style -# pill, canvas management in the "Canvas…" overflow popover + bottom-right zoom -# chip + status-bar board picker, presets in the top presets island, and -# Session/Settings in top-strip overflow popovers; "panel" is the deprecated -# legacy escape hatch, scheduled for removal one release after the pill default) -side_layout = "pill" - -# Side-palette pane restored at startup: "draw", "canvas", "session", or "settings" -# (legacy side_layout = "panel" only) -side_active_pane = "draw" - -# Side-palette sections collapsed to their header row -collapsed_sections = [] +# The unified top toolbar is the only layout. Older panel keys such as +# side_layout, side_pinned, side_minimized, side_active_pane, +# collapsed_sections, side_offset(_x), show_settings_section, and the +# retired items.order lists (side_sections, actions, pages, boards, +# presets, tool_options, sessions) remain readable and are preserved on +# save, but they no longer affect the running overlay. # Use icons instead of text labels in toolbars use_icons = true @@ -1068,7 +1052,7 @@ scale = 1.0 # Show extended color palette in the top toolbar show_more_colors = false -# Show basic actions (undo/redo/clear) in the side toolbar +# Show basic actions (undo/redo/clear) in the Canvas overflow popover show_actions_section = true # Show advanced actions (undo all, delay, freeze, etc.) @@ -1094,7 +1078,7 @@ show_pages_section = true # Show board controls section (prev/next/new/del) show_boards_section = true -# Show presets section in the side toolbar +# Show presets island in the top strip show_presets = true # Show Step Undo/Redo section @@ -1103,14 +1087,10 @@ show_step_section = false # Keep text controls visible even when text is inactive show_text_controls = true -# Deprecated compatibility mirror. Settings navigation is always reachable; -# this value is preserved for older Wayscriber versions but is otherwise ignored. -show_settings_section = true - -# Show delayed undo/redo sliders in the side toolbar +# Show delayed undo/redo sliders in the Canvas popover's Step section show_delay_sliders = false -# Show the marker opacity slider at the bottom of the side toolbar even when the marker tool isn't selected +# Keep the marker opacity control available in the style pill even when the marker tool isn't selected show_marker_opacity_section = false # Enable context-aware UI that shows/hides controls based on the active tool @@ -1122,12 +1102,10 @@ show_preset_toasts = true # Show cursor tool preview bubble show_tool_preview = false -# Authored default toolbar offsets (layer-shell/inline). Dragging a toolbar -# saves its position to runtime-ui.toml instead of rewriting these +# Authored default top-toolbar offsets (layer-shell/inline). Dragging the +# strip saves its position to runtime-ui.toml instead of rewriting these top_offset = 0.0 top_offset_y = 0.0 -side_offset = 0.0 -side_offset_x = 0.0 # Force inline toolbars even when layer-shell is available force_inline = false @@ -1137,8 +1115,10 @@ force_inline = false rebind_modifier = "ctrl_shift" [ui.toolbar.items] -# Hide individual toolbar items or whole side sections by stable ID. +# Hide individual toolbar items or whole sections by stable ID. # Unknown IDs are warned about but preserved across toolbar saves. +# Historically named side.* ids still customize top-toolbar controls +# (Canvas/Session/Settings popovers and section visibility); do not rename them. # Section-level ids (side.group.*) are explicit overrides that beat the # layout-mode baseline and survive mode switches. hidden = [ @@ -1156,8 +1136,6 @@ shown = [] [ui.toolbar.items.order] # Optional order overrides. Empty lists use the built-in order. # Known IDs omitted from a non-empty list append in the default order. -# Side section ordering uses runtime block representatives; detailed sections -# such as eraser-mode, polygon-sides, and font remain visibility-only IDs. top_tools = [ "top.tool.select", "top.tool.pen", @@ -1172,15 +1150,6 @@ top_controls = [ "top.utility.clear-canvas", "top.utility.highlight", ] -side_sections = [ - "side.group.colors", - "side.group.presets", - "side.group.thickness", - "side.group.actions", - "side.group.pages", - "side.group.boards", - "side.group.settings", -] ``` **Behavior:** @@ -1188,39 +1157,39 @@ side_sections = [ - **Scale**: `scale` multiplies toolbar UI sizing (useful for HiDPI when output scale=1). - **Colors**: `show_more_colors` toggles the extended palette row. - **Layout**: `layout_mode` picks a preset complexity level; `mode_overrides` lets you customize each mode. -- **Actions**: `show_actions_section` shows the basic actions row; `show_actions_advanced` reveals the extended actions. -- **Zoom actions**: `show_zoom_actions` toggles the zoom controls in the Canvas drawer. -- **Pages**: `show_pages_section` toggles the page navigation block. -- **Boards**: `show_boards_section` toggles the board navigation block. -- **Presets**: `show_presets` hides/shows the preset slots section. +- **Actions**: `show_actions_section` controls the basic Undo/Redo/Clear group in the Canvas popover; `show_actions_advanced` controls the separate advanced action group. +- **Zoom actions**: `show_zoom_actions` toggles the zoom controls in the Canvas popover. +- **Pages**: `show_pages_section` toggles the page navigation block in the Canvas popover. +- **Boards**: `show_boards_section` toggles the board navigation block in the Canvas popover. +- **Presets**: `show_presets` hides/shows the top-strip preset slots. - **Text controls**: `show_text_controls` keeps font size/family visible even when text isn’t active. -- **Multi-step undo/redo**: `show_step_section` hides/shows the Step Undo/Redo section. -- **Settings**: the Settings pane is always reachable. The serialized `show_settings_section` key and matching per-mode override are deprecated compatibility fields and no longer hide navigation. -- **Delays**: `show_delay_sliders` shows the timed undo/redo-all sliders in the side panel. +- **Multi-step undo/redo**: `show_step_section` hides/shows the Step Undo/Redo block in the Canvas popover. +- **Settings**: Settings is always reachable from the top-strip overflow popover. +- **Delays**: `show_delay_sliders` shows the timed undo/redo-all sliders in the Canvas popover's Step section. - **Marker opacity**: the marker opacity slider appears when the marker tool is active; `show_marker_opacity_section` keeps it visible even when using other tools. - **Polygon tools**: Full mode shows Triangle, Parallelogram, Rhombus, Regular Polygon, and Freeform Polygon under the compact Polygons picker. Simple mode exposes them in the Shapes picker. - **Context-aware UI**: `context_aware_ui` shows/hides tool-specific controls (colors, thickness, arrow labels, etc.) based on the active tool; disable to always show all controls. - **Preset toasts**: `show_preset_toasts` enables toast confirmations for preset apply/save/clear. - **Tool preview**: `show_tool_preview` toggles the cursor bubble. -- **Offsets**: `top_offset`, `top_offset_y`, `side_offset`, `side_offset_x` are the authored default toolbar positions. Dragging a toolbar saves its position as a runtime preference in `runtime-ui.toml` and leaves these untouched; editing one here again takes over from the saved drag. A side drag also records the top strip's reconciled horizontal offset, because moving the palette can change where the strip rests. +- **Offsets**: `top_offset` and `top_offset_y` are the authored default top-toolbar position. Dragging the strip saves its position as a runtime preference in `runtime-ui.toml` and leaves these untouched; editing one here again takes over from the saved drag. - **Force inline**: `force_inline` (or `WAYSCRIBER_FORCE_INLINE_TOOLBARS`) skips layer-shell toolbars. - **Shortcut editing**: hold `rebind_modifier` while clicking a bindable toolbar action to capture a replacement shortcut. The command palette also exposes edit, unbind, and reset controls for each configurable action (Ctrl+E, Ctrl+Delete, Ctrl+R). An accepted edit is written back to `config.toml` — only that action's `[keybindings]` entry, with the previous file copied to a timestamped `.bak` — so it survives a restart. The write runs on a background worker and the rebind takes effect as soon as it answers, so editing a shortcut never stalls drawing, and a chord the file has meanwhile given to another action is refused rather than applied and then taken back. Reset writes the shipped default out explicitly rather than removing the key, and when the action already resolves to that default — usually because the file omits it — there is nothing to write, so nothing is written and the toast says the action already uses the default shortcut. Conflicting shortcuts are rejected, naming the action that already owns the chord, and nothing is written; that includes a chord another action has been given in the file since this run started, which is refused rather than applied. If the file cannot be written the shortcut still changes for the run and the toast says the save failed. Ctrl+Shift+E on a palette row opens the same shortcut in the configurator's Keybindings screen. -- **Backend**: `backend` (or `WAYSCRIBER_TOOLBAR_BACKEND`) picks the toolbar frontend. `auto` uses the GTK4 bars exactly where the built-in bars would own separate layer surfaces (layer-shell present, no forced inline, no overlay-layer canvas) and falls back to the built-in Cairo bars everywhere else, including at runtime if GTK fails to start. `gtk` warns when unsupported and then falls back; `builtin` always uses the Cairo bars. -- **Pinned**: `top_pinned`/`side_pinned` are the authored defaults for whether each toolbar opens on startup. Pinning or unpinning in the overlay saves to `runtime-ui.toml` and leaves these values alone. The show/hide keybinding (`toggle_toolbar`, default F9) updates both remembered pins at once, so the next start matches what was on screen. -- **Minimize**: the toolbar minimize button (the dash that replaced the X) collapses a bar to a small edge tab instead of hiding it, so there is always an on-screen way back; `top_minimized`/`side_minimized` are the authored defaults, and the state you leave a 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 pins (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. +- **Backend**: `backend` (or `WAYSCRIBER_TOOLBAR_BACKEND`) picks the toolbar frontend. `auto` uses the GTK4 top bar exactly where the built-in bars would own a separate layer surface (layer-shell present, no forced inline, no overlay-layer canvas) and falls back to the built-in Cairo top bar everywhere else, including at runtime if GTK fails to start. `gtk` warns when unsupported and then falls back; `builtin` always uses the Cairo bars. +- **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. -- **Side layout**: `side_layout` picks where the side-palette functions live, and the top-only re-homing is now complete. The default `"pill"` is the **supported layout**: the standalone side palette is fully retired — its surface is never created (layer-shell, inline fallback, or GTK) — and every pane has a concrete new home. Drawing properties (colors included) live in the top strip's contextual style pill; canvas management lives in the **"Canvas…" overflow popover** (opened from the top strip's `⋯` overflow — boards, pages, zoom, advanced, and step controls) plus the **bottom-right zoom chip** and the **status-bar board picker**; presets live in the **top-strip presets island**; and the Session/Settings panes live in popovers opened from the overflow menu (the "Session..." / "Settings..." entries; the popovers expose the same controls the panes did). `"panel"` is the **deprecated legacy escape hatch** restoring the classic four-pane side palette; it is deprecated and planned for removal one release after the pill default. Panel-mode users see a once-per-session notice pointing at these new homes. (The original plan document called this key `layout_mode = "panel"`, but `layout_mode` is an orthogonal complexity preset — Simple/Regular/Advanced — so the switch lives under its own `side_layout` key instead.) -- **Side panes**: `side_active_pane` restores the last side-palette pane (`draw`, `canvas`, `session`, `settings`); `collapsed_sections` remembers which sections are collapsed to their header row (e.g. `["colors", "step-undo"]`). Both are authored seeds: as you use the overlay it records the current pane and collapsed set in `runtime-ui.toml` rather than rewriting these keys. Unknown ids are ignored at runtime but preserved across saves. Both keys (and `side_pinned`/`side_minimized`) only take effect under the deprecated legacy `side_layout = "panel"`; under the default pill layout they are inert. -- **Session/Settings popovers**: under the default pill layout the top strip's overflow menu always carries "Session..." and "Settings..." entries (they also appear under the legacy panel layout — the popovers are transient quick surfaces, not a second pinned pane). Opening one closes the other and the overflow menu; Escape and clicking away dismiss it. Content taller than the popover cap scrolls internally. +- **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. - **Shown items**: `ui.toolbar.items.shown` pins sections visible against the layout-mode baseline. Together with `hidden` these are the single visibility store: the `show_*` booleans are written as read-only mirrors for older versions, and legacy configs fold into explicit overrides at load. -- **Layout modes are non-destructive presets**: switching Simple/Regular/Advanced re-baselines section visibility without erasing your explicit toggles; Advanced is selectable from the overlay's Settings pane, and the top strip's chrome-island layout button cycles Simple → Regular → Advanced one click at a time. The section ids `side.group.actions-advanced`, `side.group.zoom-actions`, and `side.group.text-controls` carry the advanced/zoom/persistent-text overrides. Switching modes from the overlay re-baselines the current run only; set the durable `layout_mode` in the configurator. Sections you pinned through `items.shown`/`items.hidden` keep their override under every mode. -- **Item order**: `ui.toolbar.items.order.top_tools`, `top_controls`, and `side_sections` reorder supported toolbar items. `side_sections` orders runtime block representatives; `side.group.eraser-mode`, `side.group.polygon-sides`, and `side.group.font` can be hidden individually but are not independently orderable. Unknown future IDs and wrong-group IDs are ignored at runtime but preserved across saves. -- **Live customization**: the overlay Customize tab supports show/hide, move up/down, and drag reorder for supported groups. The configurator supports the same saved order with up/down controls. -- **Top strip items**: `top.group.quick-colors` (the swatch row + current-color chip) and `top.utility.undo`/`top.utility.redo` are hideable ids. `top.chrome.overflow` is a structural affordance that appears whenever its menu has content — which is always: the menu anchors Clear (`top.utility.clear-canvas`, unless that item is hidden), anything width pressure moves into it, and the non-hideable "Session..." / "Settings..." popover entries. The icon/text mode toggle lives in the Settings surface (the side palette's Settings pane under the legacy panel layout, the Settings popover under pill). +- **Layout modes are non-destructive presets**: switching Simple/Regular/Advanced re-baselines section visibility without erasing your explicit toggles; Advanced is selectable from the overlay's Settings popover, and the top strip's chrome-island layout button cycles Simple → Regular → Advanced one click at a time. The section ids `side.group.actions-advanced`, `side.group.zoom-actions`, and `side.group.text-controls` carry the advanced/zoom/persistent-text overrides. Switching modes from the overlay re-baselines the current run only; set the durable `layout_mode` in the configurator. Sections you pinned through `items.shown`/`items.hidden` keep their override under every mode. +- **Item order**: `ui.toolbar.items.order.top_tools` and `top_controls` reorder supported top-strip items. Unknown future IDs and wrong-group IDs are ignored at runtime but preserved across saves. Panel-era order lists (`actions`, `pages`, `boards`, `presets`, `tool_options`, `sessions`, `side_sections`) are retired: authored `config.toml` values stay as retired settings, while matching keys under `runtime-ui.toml`'s recognized `item_order` map are pruned on rewrite. +- **Historical item IDs**: retained and removed `side.*` IDs are classified in the [toolbar item ID compatibility inventory](toolbar-item-id-compatibility.md). Retained spellings remain active configuration contracts even though the side palette is gone. +- **Live customization**: the overlay Customize surface supports show/hide, move up/down, and drag reorder for supported top groups. The configurator supports the same saved order with up/down controls. +- **Top strip items**: `top.group.quick-colors` (the swatch row + current-color chip) and `top.utility.undo`/`top.utility.redo` are hideable ids. `top.chrome.overflow` is a structural affordance that appears whenever its menu has content — which is always: the menu anchors Clear (`top.utility.clear-canvas`, unless that item is hidden), anything width pressure moves into it, and the non-hideable "Session..." / "Settings..." popover entries. The icon/text mode toggle lives in the Settings popover. - **Clear canvas**: Clear lives in the top strip's overflow (⋯) menu. A plain click clears and shows a short "Cleared — Undo?" toast; Shift+click clears instantly without the toast. The `clear_canvas` keyboard action is always instant and shows no toast. -- **Recoloring a swatch**: right-clicking any quick-color swatch (style pill or side palette) opens the color picker bound to that palette slot, titled "Recolor <slot>". The swatch tracks the gradient live, OK applies the color to that slot and writes it back to `config.toml` — only that one `[[drawing.quick_colors]]` entry, with the previous file copied to a timestamped `.bak` — and Cancel/Escape restores it. Recoloring a slot the file only implies writes the palette out as far as that slot and no further, so the slots after it keep tracking the shipped defaults. If the file cannot be written the color still applies for the run and the toast says the save failed; picking the color the slot already paints writes nothing and says so. The slot keeps its label and shortcut, so R still selects the red slot after you point it at a different red. Recoloring the swatch you are currently drawing with moves the live color with it; recoloring any other slot leaves your current color alone. Left-clicking a swatch still just selects it, and the leftmost chip still opens the picker for the active tool's own color. +- **Recoloring a swatch**: right-clicking any quick-color swatch in the style pill opens the color picker bound to that palette slot, titled "Recolor <slot>". The swatch tracks the gradient live, OK applies the color to that slot and writes it back to `config.toml` — only that one `[[drawing.quick_colors]]` entry, with the previous file copied to a timestamped `.bak` — and Cancel/Escape restores it. Recoloring a slot the file only implies writes the palette out as far as that slot and no further, so the slots after it keep tracking the shipped defaults. If the file cannot be written the color still applies for the run and the toast says the save failed; picking the color the slot already paints writes nothing and says so. The slot keeps its label and shortcut, so R still selects the red slot after you point it at a different red. Recoloring the swatch you are currently drawing with moves the live color with it; recoloring any other slot leaves your current color alone. Left-clicking a swatch still just selects it, and the leftmost chip still opens the picker for the active tool's own color. - **Restoring a swatch's shipped color**: while recoloring a slot, the picker adds a **Default** button next to OK/Cancel that loads the color wayscriber ships for that slot. It stages the color like any other pick — the swatch previews it, OK applies and saves it, Cancel backs out — so it is not a separate destructive action. The button only appears for the eleven built-in slots; extra slots you added past them have no shipped default, and the tool-color picker never shows it. Restoring sets the built-in value in your palette rather than deleting the entry, so the slot keeps its identity. - **Shapes popover options**: the Fill checkbox (`top.utility.fill`) remains available in the Shapes popover whenever that item is enabled, even while another tool is active, so it can configure the next fill-capable shape. The polygon side count appears only while Regular Polygon is active. These controls live in the popover instead of a permanently reserved mini-checkbox lane under the bar, keeping the bar 58px tall. The highlight-ring row still appears under the Highlight button, but only while the highlight tool is active. - **Screenshot toolbar button**: `top.utility.screenshot` is hidden by default; remove it from `ui.toolbar.items.hidden` or enable it in the configurator/overlay customization to show it. @@ -1554,7 +1523,7 @@ Config values seed startup defaults. When `restore_tool_state = true`, the saved The configurator Session tab exposes the same distinction for recent named sessions: Clear Tool State preserves saved boards/history while removing only persisted tool settings; Clear Saved Data removes saved session files. Offline catalog actions are disabled while an overlay, manually started daemon, or background service is active. Use the command palette for the active overlay session. -The overlay Session panel lives in the side toolbar's Settings drawer: +The overlay Session panel lives in the top strip's overflow **"Session..."** popover: - `Open` loads an existing named session, saves dirty current data first when needed, and records the target in the recent catalog. - `Save As` writes the current overlay to another named session and switches the active target. It appends `.wayscriber-session` when no extension is supplied and asks before replacing existing session artifacts. diff --git a/docs/codebase-overview.md b/docs/codebase-overview.md index e0b07d85..6a6923d6 100644 --- a/docs/codebase-overview.md +++ b/docs/codebase-overview.md @@ -162,8 +162,10 @@ Notifications are sent via `notification::send_notification_async`, keeping all and surface input regions. - `src/toolbar_gtk/view/top_bar/` exhaustively adapts the same contract to GTK widgets while retaining GTK sizing, CSS, updater closures, drag gestures, and popover lifecycle. -- Shape-picker compound rows and all side-palette layout remain frontend-specific. Their existing - tool/section ordering still comes from the shared toolbar model. +- Shape-picker compound rows remain frontend-specific adapters over the shared top model. Canvas, + Session, and Settings live in top popovers built from the same shared models; there is no side + palette layout path. The [toolbar item ID compatibility inventory](toolbar-item-id-compatibility.md) + records which historical `side.*` serialized IDs still feed those models. --- @@ -383,18 +385,20 @@ Notifications are sent via `notification::send_notification_async`, keeping all - `src/runtime_ui_state/` owns the versioned wire model, seed/override reconciliation, guarded mutation pipeline, exact source revisions, pinned-directory store operations, recovery barriers, cancellation capabilities, and per-mutation durability outcomes. -- Every seed target is runtime-owned: pins, minimize, side pane, collapsed sections, item - visibility/order, board pins, both toolbar positions, and the top strip's display form. Authored - `config.toml` values are the seeds; direct manipulation writes overrides. `top_position`, - `side_position`, and `top_display_mode` were added to wire V1 additively — an older build decodes - them as unknown keys and preserves them verbatim, which a version bump would not allow. +- Every live seed target is runtime-owned: top pin/minimize, item visibility/order, board pins, the + top toolbar position, and the top strip's display form. Authored `config.toml` values are the + seeds; direct manipulation writes overrides. `top_position` and `top_display_mode` were added to + wire V1 additively — an older build decodes them as unknown keys and preserves them verbatim, + which a version bump would not allow. Retired panel keys such as `side_position`, side pane, and + collapsed sections stay only as raw passthrough (or are pruned from recognized maps) and are not + modeled seeds. - The persisted display form is `full`/`micro` only. The cycle action's `hidden` rung and presenter mode's forced mapping stay live-only; the override is computed with `TopDisplayMode::persisted()` and presenter-restore precedence so neither can be written. -- A committed side drag stages both position overrides in one mutation scope because completing it - reconciles the top strip's horizontal base. Retained position overrides are applied on top of the - authored seeds at startup and clamped on the first apply against real output geometry, not on - load, so an override recorded on a disconnected monitor degrades instead of being discarded. +- A committed top-toolbar drag stages the top position override. Retained position overrides are + applied on top of the authored seeds at startup and clamped on the first apply against real + output geometry, not on load, so an override recorded on a disconnected monitor degrades instead + of being discarded. - `src/backend/wayland/runtime_ui_state.rs` adapts toolbar and board interactions to that controller. `coordinator.rs` owns previews and writer transport, `lifecycle.rs` retains the exact active incident/recovery capabilities and publishes safe toolbar diagnostics, and `wayland.rs` applies @@ -418,7 +422,7 @@ Notifications are sent via `notification::send_notification_async`, keeping all **Modules:** - `src/session/`: target options, primary-file validation, snapshot load/save, sidecars, clear/recovery markers, saved tool-state reset, locks, catalog metadata, and inactive file operations. - `src/backend/wayland/session/`: runtime Open, Save As, Clear, and saved tool-state reset transactions for the active overlay. -- `src/backend/wayland/state/toolbar/events/session.rs`: overlay Session panel routing for Open, Save As, Info, Clear, recent sessions, and configurator launch. +- `src/backend/wayland/state/toolbar/events/session.rs`: overlay Session popover routing for Open, Save As, Info, Clear, recent sessions, and configurator launch. - `src/daemon/`: accepts daemon-toggle requests that carry an optional named session target. **Flow:** diff --git a/docs/toolbar-item-id-compatibility.md b/docs/toolbar-item-id-compatibility.md new file mode 100644 index 00000000..73e58535 --- /dev/null +++ b/docs/toolbar-item-id-compatibility.md @@ -0,0 +1,135 @@ +# Toolbar Item ID Compatibility Inventory + +This inventory records the consumer-based decision for every historical +`side.*` toolbar item ID that existed when the legacy side palette was removed. +The serialized spelling is a configuration contract: an ID remains recognized +only when the unified top toolbar still consumes it. + +The code sources of truth are +[`ids.rs`](../src/config/types/toolbar/ids.rs) and +[`items/definitions.rs`](../src/config/types/toolbar/items/definitions.rs). +Do not add, remove, or rename a historical ID without updating this inventory +and the owning model test. + +Proof references used below: + +- **Definitions** — `toolbar_item_definitions_are_unique_parseable_and_labeled` + proves every retained ID is defined and parsed. +- **Sections** — `config::types::toolbar::visibility::tests` proves explicit + section visibility overrides beat layout-mode baselines. +- **Models** — the action, session, and settings model tests exhaustively map + commands to retained IDs; visibility tests also prove that retained IDs hide + their corresponding unified-top controls. +- **Document** — `retired_toolbar_settings_are_preserved_and_diagnosed` proves + representative retained overrides survive an unrelated document save. + +## Retained IDs + +| Serialized ID | Unified-top consumer | Proof | +| --- | --- | --- | +| `side.group.step-undo` | Canvas popover Step Undo/Redo section | Definitions, Sections | +| `side.group.actions` | Canvas popover basic Actions section | Definitions, Sections, Models | +| `side.group.pages` | Canvas popover Pages section | Definitions, Sections, Models | +| `side.group.boards` | Canvas popover Boards section | Definitions, Sections, Models | +| `side.group.presets` | Top-strip presets island | Definitions, Sections, Document | +| `side.group.actions-advanced` | Canvas popover Advanced section | Definitions, Sections, Models | +| `side.group.zoom-actions` | Canvas popover Zoom section | Definitions, Sections, Models | +| `side.group.text-controls` | Contextual style-pill text controls | Definitions, Sections | +| `side.actions.zoom-in` | Canvas Zoom command | Definitions, Models, Document | +| `side.actions.zoom-out` | Canvas Zoom command | Definitions, Models | +| `side.actions.reset-zoom` | Canvas Zoom command | Definitions, Models | +| `side.actions.toggle-zoom-lock` | Canvas Zoom command | Definitions, Models | +| `side.actions.undo-all` | Canvas Advanced command | Definitions, Models | +| `side.actions.redo-all` | Canvas Advanced command | Definitions, Models | +| `side.actions.undo-all-delayed` | Canvas Advanced command | Definitions, Models | +| `side.actions.redo-all-delayed` | Canvas Advanced command | Definitions, Models | +| `side.actions.freeze` | Canvas Advanced command | Definitions, Models, Document | +| `side.pages.previous` | Canvas Pages command | Definitions, Models | +| `side.pages.next` | Canvas Pages command | Definitions, Models | +| `side.pages.new` | Canvas Pages command | Definitions, Models | +| `side.pages.duplicate` | Canvas Pages command | Definitions, Models | +| `side.pages.delete` | Canvas Pages command | Definitions, Models, Document | +| `side.boards.picker` | Canvas Boards picker command | Definitions, Models, Document | +| `side.boards.previous` | Canvas Boards command | Definitions, Models | +| `side.boards.next` | Canvas Boards command | Definitions, Models | +| `side.boards.new` | Canvas Boards command | Definitions, Models | +| `side.boards.duplicate` | Canvas Boards command | Definitions, Models | +| `side.boards.delete` | Canvas Boards command | Definitions, Models, Document | +| `side.settings.context-aware-ui` | Settings popover toggle | Definitions, Models | +| `side.settings.text-controls` | Settings popover toggle | Definitions, Models | +| `side.settings.status-bar` | Settings popover toggle | Definitions, Models | +| `side.settings.status-board-badge` | Settings popover toggle | Definitions, Models | +| `side.settings.status-page-badge` | Settings popover toggle | Definitions, Models | +| `side.settings.floating-badge-always` | Settings popover toggle | Definitions, Models | +| `side.settings.preset-toasts` | Settings popover toggle | Definitions, Models | +| `side.settings.presets` | Settings popover toggle | Definitions, Models | +| `side.settings.actions` | Settings popover toggle | Definitions, Models | +| `side.settings.zoom-actions` | Settings popover toggle | Definitions, Models | +| `side.settings.advanced-actions` | Settings popover toggle | Definitions, Models | +| `side.settings.boards` | Settings popover toggle | Definitions, Models | +| `side.settings.pages` | Settings popover toggle | Definitions, Models | +| `side.settings.step-controls` | Settings popover toggle | Definitions, Models | +| `side.settings.command-palette` | Settings popover command | Definitions, Models | +| `side.settings.configurator` | Settings popover command | Definitions, Models | +| `side.settings.config-file` | Settings popover command | Definitions, Models | +| `side.settings.about` | Settings popover command | Definitions, Models, Document | +| `side.session.open` | Session popover command | Definitions, Models | +| `side.session.save-as` | Session popover command | Definitions, Models | +| `side.session.info` | Session popover command | Definitions, Models, Document | +| `side.session.clear` | Session popover command | Definitions, Models | +| `side.session.manager` | Session popover configurator command | Definitions, Models | + +## Removed IDs + +Removed IDs are deliberately unknown to the active parser. Authored strings in +`items.hidden` and `items.shown` still round-trip as unknown raw values; they do +not control runtime UI. + +| Serialized ID | Consumer trace and disposition | +| --- | --- | +| `side.group.colors` | Panel Draw card only; the style pill uses `top.group.quick-colors`. | +| `side.group.thickness` | Panel Draw card only; thickness is contextual style-pill state. | +| `side.group.eraser-mode` | Panel Draw card only; eraser mode is contextual style-pill state. | +| `side.group.polygon-sides` | Panel Draw card only; polygon sides are contextual style-pill state. | +| `side.group.arrow-labels` | Panel Draw card only; arrow labels are contextual style-pill state. | +| `side.group.step-markers` | Panel Draw card only; step-marker reset is contextual style-pill state. | +| `side.group.marker-opacity` | Panel Draw card only; opacity is contextual style-pill state. | +| `side.group.text-size` | Panel Draw card only; text size is contextual style-pill state. | +| `side.group.font` | Panel Draw card only; font controls are contextual style-pill state. | +| `side.group.settings` | Panel container only; Settings is an unconditional top popover host. | +| `side.group.session` | Panel container only; Session is an unconditional top popover host. | +| `side.actions.undo` | Panel button only; the top control uses `top.utility.undo`. | +| `side.actions.redo` | Panel button only; the top control uses `top.utility.redo`. | +| `side.actions.clear-canvas` | Panel button only; the top control uses `top.utility.clear-canvas`. | +| `side.boards.rename` | No unified-top button or toolbar event consumer. | +| `side.tool-options.color` | Panel Draw control only; the style pill uses its typed tool model. | +| `side.tool-options.thickness` | Panel Draw control only; the style pill uses its typed tool model. | +| `side.tool-options.marker-opacity` | Panel Draw control only; the style pill uses its typed tool model. | +| `side.tool-options.eraser-mode` | Panel Draw control only; the style pill uses its typed tool model. | +| `side.tool-options.font-size` | Panel Draw control only; the style pill uses its typed tool model. | +| `side.tool-options.font-family` | Panel Draw control only; the style pill uses its typed tool model. | +| `side.tool-options.polygon-sides` | Panel Draw control only; the style pill uses its typed tool model. | +| `side.tool-options.arrow-labels` | Panel Draw control only; the style pill uses its typed tool model. | +| `side.tool-options.step-marker-reset` | Panel Draw control only; the style pill uses its typed tool model. | + +## Retired order fields + +The unified toolbar has only the `top_tools` and `top_controls` order groups. +The following panel-era fields are preserved as authored raw TOML, reported by +their exact path as `RetiredSetting`, and omitted from the typed schema and +configurator. They have no aliases. + +| Source TOML path | Former consumer | Disposition and proof | +| --- | --- | --- | +| `ui.toolbar.items.order.side_sections` | Side-palette section renderer and its reorder UI | Retired with the panel; covered before and after an unrelated save by `retired_toolbar_settings_are_preserved_and_diagnosed`. | +| `ui.toolbar.items.order.actions` | Typed order resolver and runtime preference map; no unified-top renderer or reorder UI consumed it | Retired raw; covered by the same preservation/diagnostic fixture. | +| `ui.toolbar.items.order.pages` | Typed order resolver and runtime preference map; no unified-top renderer or reorder UI consumed it | Retired raw; covered by the same preservation/diagnostic fixture. | +| `ui.toolbar.items.order.boards` | Typed order resolver and runtime preference map; no unified-top renderer or reorder UI consumed it | Retired raw; covered by the same preservation/diagnostic fixture. | +| `ui.toolbar.items.order.presets` | Typed order resolver and runtime preference map; no unified-top renderer or reorder UI consumed it | Retired raw; `side.group.presets` remains a separate active visibility ID. Covered by the same preservation/diagnostic fixture. | +| `ui.toolbar.items.order.tool_options` | Typed order resolver and runtime preference map; no unified-top renderer or reorder UI consumed it | Retired raw with the unused `side.tool-options.*` namespace; covered by the same preservation/diagnostic fixture. | +| `ui.toolbar.items.order.sessions` | Typed order resolver and runtime preference map; no unified-top renderer or reorder UI consumed it | Retired raw; covered by the same preservation/diagnostic fixture. | + +The exact-path classifier lives in +[`document.rs`](../src/config/document.rs). The document fixture also proves +that active `top_tools` and `top_controls` orders retain their runtime effect; +the shared-model test proves both orders reach their unified-top controls. diff --git a/examples/session-manager.md b/examples/session-manager.md index 8d006342..5bd5c755 100644 --- a/examples/session-manager.md +++ b/examples/session-manager.md @@ -37,8 +37,8 @@ the parent directory is already gone. ## Overlay Workflow -Open Wayscriber with any persisted session target, then use the side toolbar's -Settings drawer: +Open Wayscriber with any persisted session target, then use the top strip overflow +**"Session..."** popover: - `Open` loads an existing named session and records it in the recent catalog. - `Save As` writes the current overlay to another named session and switches the diff --git a/src/backend/wayland/backend/state_init/input_state.rs b/src/backend/wayland/backend/state_init/input_state.rs index 6eb9eb05..2081e125 100644 --- a/src/backend/wayland/backend/state_init/input_state.rs +++ b/src/backend/wayland/backend/state_init/input_state.rs @@ -87,7 +87,6 @@ pub(super) fn build_input_state(config: &Config) -> InputState { config.ui.toolbar.mode_overrides.clone(), config.ui.toolbar.items.clone(), config.ui.toolbar.top_pinned, - config.ui.toolbar.side_pinned, config.ui.toolbar.use_icons, config.ui.toolbar.scale, config.ui.toolbar.show_more_colors, @@ -100,25 +99,16 @@ pub(super) fn build_input_state(config: &Config) -> InputState { config.ui.toolbar.show_step_section, config.ui.toolbar.show_text_controls, config.ui.toolbar.context_aware_ui, - config.ui.toolbar.show_settings_section, config.ui.toolbar.show_delay_sliders, config.ui.toolbar.show_marker_opacity_section, config.ui.toolbar.show_preset_toasts, config.ui.toolbar.show_tool_preview, ); - input_state.init_toolbar_minimized_from_config( - config.ui.toolbar.top_minimized, - config.ui.toolbar.side_minimized, - ); + input_state.init_toolbar_minimized_from_config(config.ui.toolbar.top_minimized); input_state.init_toolbar_display_mode_from_config(config.ui.toolbar.top_display_mode); - input_state.init_toolbar_side_layout_from_config(config.ui.toolbar.side_layout); input_state.zoom_chip_display = config.ui.toolbar.zoom_chip_display; input_state.show_zoom_chip = config.ui.toolbar.show_zoom_chip; input_state.init_toolbar_rebind_modifier_from_config(config.ui.toolbar.rebind_modifier); - input_state.init_toolbar_side_panes_from_config( - &config.ui.toolbar.side_active_pane, - &config.ui.toolbar.collapsed_sections, - ); input_state.init_presets_from_config(&config.presets); input_state diff --git a/src/backend/wayland/handlers/keyboard/mod.rs b/src/backend/wayland/handlers/keyboard/mod.rs index ad98af1d..df370d79 100644 --- a/src/backend/wayland/handlers/keyboard/mod.rs +++ b/src/backend/wayland/handlers/keyboard/mod.rs @@ -31,8 +31,8 @@ impl KeyboardHandler for WaylandState { self.clear_xdg_close_guard(); self.set_last_activation_serial(Some(serial)); self.maybe_retry_activation(qh); - if let Some(target) = self.toolbar.focus_target_for_surface(surface) { - self.set_toolbar_focus_target(Some(target)); + if self.toolbar.is_focusable_surface(surface) { + self.set_toolbar_focus_active(true); } else { self.clear_toolbar_focus(); } diff --git a/src/backend/wayland/handlers/pointer/axis.rs b/src/backend/wayland/handlers/pointer/axis.rs index f04a2b65..c567dc49 100644 --- a/src/backend/wayland/handlers/pointer/axis.rs +++ b/src/backend/wayland/handlers/pointer/axis.rs @@ -73,15 +73,12 @@ impl WaylandState { return; } if on_toolbar || self.pointer_over_toolbar() { - if scroll_direction != 0 { - if self.wheel_over_side_toolbar(&event.surface, event.position) { - self.scroll_side_pane_by_wheel(scroll_direction); - } else if self.wheel_over_top_toolbar(&event.surface, event.position) { - // With a Canvas/Session/Settings popover open, the wheel scrolls - // its capped viewport; otherwise a top-strip wheel stays a - // no-op (it never falls through to thickness/zoom). - self.scroll_top_popover_by_wheel(scroll_direction); - } + if scroll_direction != 0 && self.wheel_over_top_toolbar(&event.surface, event.position) + { + // With a Canvas/Session/Settings popover open, the wheel scrolls + // its capped viewport; otherwise a top-strip wheel stays a + // no-op (it never falls through to thickness/zoom). + self.scroll_top_popover_by_wheel(scroll_direction); } return; } diff --git a/src/backend/wayland/handlers/pointer/cursor.rs b/src/backend/wayland/handlers/pointer/cursor.rs index a1c685fa..e388ba1e 100644 --- a/src/backend/wayland/handlers/pointer/cursor.rs +++ b/src/backend/wayland/handlers/pointer/cursor.rs @@ -132,7 +132,6 @@ impl WaylandState { return match hint { ToolbarCursorHint::Pointer => CursorIcon::Pointer, ToolbarCursorHint::Grab => CursorIcon::Grab, - ToolbarCursorHint::Crosshair => CursorIcon::Crosshair, ToolbarCursorHint::Default => CursorIcon::Default, }; } @@ -143,7 +142,6 @@ impl WaylandState { return match hint { ToolbarCursorHint::Pointer => CursorIcon::Pointer, ToolbarCursorHint::Grab => CursorIcon::Grab, - ToolbarCursorHint::Crosshair => CursorIcon::Crosshair, ToolbarCursorHint::Default => CursorIcon::Default, }; } diff --git a/src/backend/wayland/handlers/pointer/press.rs b/src/backend/wayland/handlers/pointer/press.rs index 273307aa..6643cbee 100644 --- a/src/backend/wayland/handlers/pointer/press.rs +++ b/src/backend/wayland/handlers/pointer/press.rs @@ -174,11 +174,7 @@ impl WaylandState { { handled = true; let toolbar_event = intent_to_event(intent, self.toolbar.last_snapshot()); - if matches!( - toolbar_event, - ToolbarEvent::MoveTopToolbar { .. } | ToolbarEvent::MoveSideToolbar { .. } - ) && drag - { + if matches!(toolbar_event, ToolbarEvent::MoveTopToolbar { .. }) && drag { self.lock_pointer_for_drag(qh, &event.surface); } log::info!( diff --git a/src/backend/wayland/handlers/relative_pointer.rs b/src/backend/wayland/handlers/relative_pointer.rs index 93d531d1..d960c635 100644 --- a/src/backend/wayland/handlers/relative_pointer.rs +++ b/src/backend/wayland/handlers/relative_pointer.rs @@ -31,15 +31,13 @@ impl RelativePointerHandler for WaylandState { drag_log(|| { format!( - "relative drag: kind={:?}, delta=({:.3}, {:.3}), utime={}, offsets=({}, {})/({}, {})", + "relative drag: kind={:?}, delta=({:.3}, {:.3}), utime={}, offsets=({}, {})", kind, event.delta.0, event.delta.1, event.utime, self.toolbar_top_offset(), - self.toolbar_top_offset_y(), - self.toolbar_side_offset_x(), - self.toolbar_side_offset() + self.toolbar_top_offset_y() ) }); diff --git a/src/backend/wayland/runtime_ui_state.rs b/src/backend/wayland/runtime_ui_state.rs index 561abca0..64a56455 100644 --- a/src/backend/wayland/runtime_ui_state.rs +++ b/src/backend/wayland/runtime_ui_state.rs @@ -4,7 +4,7 @@ //! toolbar target conversion, preview lifetimes, and the writer transport; UI //! models and `InputState` never see storage or controller details. -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeMap; use std::path::PathBuf; use anyhow::{Context, Result}; @@ -17,7 +17,6 @@ use crate::config::{ use crate::input::InputState; use crate::runtime_ui_state::*; use crate::ui::toolbar::model::ToolbarRuntimeUiPersistenceTarget; -use crate::ui::toolbar::{SidePane, ToolbarSideSection}; mod board; mod coordinator; @@ -48,7 +47,6 @@ use lifecycle::RuntimeUiLifecycleState; #[derive(Debug, Clone, Copy, PartialEq)] pub(in crate::backend::wayland) struct ToolbarPositionSnapshot { pub top: (f64, f64), - pub side: (f64, f64), } #[derive(Debug)] diff --git a/src/backend/wayland/runtime_ui_state/coordinator.rs b/src/backend/wayland/runtime_ui_state/coordinator.rs index 18ae2a82..feb54fc1 100644 --- a/src/backend/wayland/runtime_ui_state/coordinator.rs +++ b/src/backend/wayland/runtime_ui_state/coordinator.rs @@ -63,8 +63,7 @@ impl ToolbarRuntimeState { apply_live_toolbar_state(input, self.controller.live_state(), |_| true); apply_live_board_state(input, self.controller.live_state(), |_| true); input.toolbar_top_visible = input.toolbar_top_pinned; - input.toolbar_side_visible = input.toolbar_side_pinned; - input.toolbar_visible = input.toolbar_top_visible || input.toolbar_side_visible; + input.toolbar_visible = input.toolbar_top_visible; } /// Layer retained position overrides on top of the authored seeds the @@ -310,9 +309,7 @@ impl ToolbarRuntimeState { .expect("aborted position drag was just observed") .session, ); - // A side drag is guarded by both position seeds because its final - // save can reconcile the top X offset. If only one guard changes, - // the changed target must come from the new live authority while + // A changed guard must come from the new live authority while // every other previewed target returns to its pre-drag value. rollback .values diff --git a/src/backend/wayland/runtime_ui_state/live_state.rs b/src/backend/wayland/runtime_ui_state/live_state.rs index f9a5f5d8..60620980 100644 --- a/src/backend/wayland/runtime_ui_state/live_state.rs +++ b/src/backend/wayland/runtime_ui_state/live_state.rs @@ -194,38 +194,11 @@ pub(super) fn apply_live_toolbar_state( { input.toolbar_top_pinned = value; } - if include(&InteractionSeedTarget::SidePinned) - && let Some(value) = bool_value(InteractionSeedTarget::SidePinned) - { - input.toolbar_side_pinned = value; - } if include(&InteractionSeedTarget::TopMinimized) && let Some(value) = bool_value(InteractionSeedTarget::TopMinimized) { input.apply_toolbar_set_top_minimized(value); } - if include(&InteractionSeedTarget::SideMinimized) - && let Some(value) = bool_value(InteractionSeedTarget::SideMinimized) - { - input.toolbar_side_minimized = value; - } - if include(&InteractionSeedTarget::SidePane) - && let Some(InteractionSeedValue::SidePane(pane)) = - live.get(&InteractionSeedTarget::SidePane) - { - input.apply_toolbar_set_side_pane(*pane); - } - for section in ToolbarSideSection::ALL { - let target = InteractionSeedTarget::CollapsedSection(section); - if !include(&target) { - continue; - } - if bool_value(target) == Some(true) { - input.toolbar_collapsed_side_sections.insert(section); - } else { - input.toolbar_collapsed_side_sections.remove(§ion); - } - } for id in resettable_individual_toolbar_item_ids() { let target = InteractionSeedTarget::ItemVisibility(id); if !include(&target) { @@ -256,12 +229,6 @@ pub(super) fn apply_live_toolbar_positions( { positions.top = (position.x.get(), position.y.get()); } - if include(&InteractionSeedTarget::SidePosition) - && let Some(InteractionSeedValue::Position(position)) = - live.get(&InteractionSeedTarget::SidePosition) - { - positions.side = (position.x.get(), position.y.get()); - } } pub(super) fn apply_live_board_state( diff --git a/src/backend/wayland/runtime_ui_state/positions.rs b/src/backend/wayland/runtime_ui_state/positions.rs index 072611db..ff38d7ed 100644 --- a/src/backend/wayland/runtime_ui_state/positions.rs +++ b/src/backend/wayland/runtime_ui_state/positions.rs @@ -1,21 +1,19 @@ use super::*; -/// One of the two toolbar positions a move drag can write. +/// The toolbar position a move drag can write. /// -/// Position drags only ever touch these two overrides. Naming them as their own -/// type keeps the seed target and the snapshot field that feeds it in lockstep, -/// so neither has to be recovered from the other at runtime. +/// Naming it as its own type keeps the seed target and the snapshot field that +/// feeds it in lockstep, so neither has to be recovered from the other at +/// runtime. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum PositionDragTarget { Top, - Side, } impl PositionDragTarget { fn offsets(self, positions: ToolbarPositionSnapshot) -> (f64, f64) { match self { Self::Top => positions.top, - Self::Side => positions.side, } } } @@ -24,20 +22,14 @@ impl From for InteractionSeedTarget { fn from(target: PositionDragTarget) -> Self { match target { PositionDragTarget::Top => Self::TopPosition, - PositionDragTarget::Side => Self::SidePosition, } } } /// The override targets a toolbar drag of `kind` may write. -/// -/// A side drag can change whether the side palette overlaps the top strip, and -/// drag completion reconciles the top strip's X offset against that new base, -/// so it owns both position targets in one mutation scope. fn position_drag_targets(kind: MoveDragKind) -> &'static [PositionDragTarget] { match kind { MoveDragKind::Top => &[PositionDragTarget::Top], - MoveDragKind::Side => &[PositionDragTarget::Top, PositionDragTarget::Side], } } diff --git a/src/backend/wayland/runtime_ui_state/rollback.rs b/src/backend/wayland/runtime_ui_state/rollback.rs index ed781239..09bdf1de 100644 --- a/src/backend/wayland/runtime_ui_state/rollback.rs +++ b/src/backend/wayland/runtime_ui_state/rollback.rs @@ -14,23 +14,9 @@ pub(in crate::backend::wayland) fn apply_toolbar_runtime_rollback( for (target, value) in &rollback.values { match target { Target::TopPinned => set_bool(value, |v| input.toolbar_top_pinned = v), - Target::SidePinned => set_bool(value, |v| input.toolbar_side_pinned = v), Target::TopMinimized => set_bool(value, |v| { input.apply_toolbar_set_top_minimized(v); }), - Target::SideMinimized => set_bool(value, |v| input.toolbar_side_minimized = v), - Target::SidePane => { - if let InteractionSeedValue::SidePane(pane) = value { - input.apply_toolbar_set_side_pane(*pane); - } - } - Target::CollapsedSection(section) => set_bool(value, |collapsed| { - if collapsed { - input.toolbar_collapsed_side_sections.insert(*section); - } else { - input.toolbar_collapsed_side_sections.remove(section); - } - }), Target::ItemVisibility(id) => { if let InteractionSeedValue::Visibility(setting) = value { input.set_toolbar_item_visibility_setting(*id, *setting); @@ -49,11 +35,6 @@ pub(in crate::backend::wayland) fn apply_toolbar_runtime_rollback( positions.top = (position.x.get(), position.y.get()); } } - Target::SidePosition => { - if let InteractionSeedValue::Position(position) = value { - positions.side = (position.x.get(), position.y.get()); - } - } Target::TopDisplayMode => { if let InteractionSeedValue::TopDisplayMode(mode) = value { apply_persisted_top_display_mode(input, *mode); @@ -99,22 +80,19 @@ pub(in crate::backend::wayland) fn apply_toolbar_runtime_rollback( } } } - // Today only the visibility toggle batches both pins into one snapshot; - // the pin buttons persist single-pin scopes and are deliberately - // decoupled from live visibility, so exactly the both-pins shape must - // re-derive the live visibility flags — otherwise a rolled-back toggle - // leaves the screen disagreeing with the restored pins (and with the - // next start). The toggle arm skips no-op pin writes, so every snapshot - // that lands here crossed a genuine pin transition and the derived - // flags are exactly the startup (pin-derived) form of the pre-toggle - // screen. The display mode is deliberately not restored: the persisted - // type has no `Hidden`, and none is needed — after a hide-rollback the - // still-`Hidden` live mode reproduces the pre-toggle screen, after a - // show-rollback the strip is hidden either way, and any future show - // unfolds `Hidden` to `Full` regardless. - if rollback.values.contains_key(&Target::TopPinned) - && rollback.values.contains_key(&Target::SidePinned) - { + // The visibility toggle persists the top pin; the pin button persists the + // same key but is deliberately decoupled from live visibility, so a + // rolled-back toggle must re-derive the live visibility flag — otherwise + // the screen disagrees with the restored pin (and with the next start). + // The toggle arm skips no-op pin writes, so every snapshot that lands here + // crossed a genuine pin transition and the derived flag is exactly the + // startup (pin-derived) form of the pre-toggle screen. The display mode is + // deliberately not restored: the persisted type has no `Hidden`, and none + // is needed — after a hide-rollback the still-`Hidden` live mode + // reproduces the pre-toggle screen, after a show-rollback the strip is + // hidden either way, and any future show unfolds `Hidden` to `Full` + // regardless. + if rollback.values.contains_key(&Target::TopPinned) { input.derive_toolbar_visibility_from_pins(); } input.needs_redraw = true; diff --git a/src/backend/wayland/runtime_ui_state/seeds.rs b/src/backend/wayland/runtime_ui_state/seeds.rs index 76e4f140..7415913f 100644 --- a/src/backend/wayland/runtime_ui_state/seeds.rs +++ b/src/backend/wayland/runtime_ui_state/seeds.rs @@ -15,18 +15,10 @@ pub(super) fn runtime_seeds_from_config( InteractionSeedTarget::TopPinned, InteractionSeedValue::Bool(config.ui.toolbar.top_pinned), )?; - insert( - InteractionSeedTarget::SidePinned, - InteractionSeedValue::Bool(config.ui.toolbar.side_pinned), - )?; insert( InteractionSeedTarget::TopMinimized, InteractionSeedValue::Bool(config.ui.toolbar.top_minimized), )?; - insert( - InteractionSeedTarget::SideMinimized, - InteractionSeedValue::Bool(config.ui.toolbar.side_minimized), - )?; insert( InteractionSeedTarget::StatusBar, InteractionSeedValue::Bool(config.ui.show_status_bar), @@ -85,25 +77,6 @@ pub(super) fn runtime_seeds_from_config( InteractionSeedValue::Bool(config.ui.status_bar_item_visible(item)), )?; } - insert( - InteractionSeedTarget::SidePane, - InteractionSeedValue::SidePane( - SidePane::from_config_id(&config.ui.toolbar.side_active_pane).unwrap_or_default(), - ), - )?; - let collapsed = config - .ui - .toolbar - .collapsed_sections - .iter() - .filter_map(|raw| ToolbarSideSection::from_config_id(raw)) - .collect::>(); - for section in ToolbarSideSection::ALL { - insert( - InteractionSeedTarget::CollapsedSection(section), - InteractionSeedValue::Bool(collapsed.contains(§ion)), - )?; - } let resolved_items = resolved_toolbar_item_seeds(config); insert( InteractionSeedTarget::FloatingBadge, @@ -153,16 +126,6 @@ pub(super) fn runtime_seeds_from_config( .context("top toolbar position seed is not finite")?, ), )?; - insert( - InteractionSeedTarget::SidePosition, - InteractionSeedValue::Position( - ToolbarPositionSeed::new( - config.ui.toolbar.side_offset_x, - config.ui.toolbar.side_offset, - ) - .context("side toolbar position seed is not finite")?, - ), - )?; insert( InteractionSeedTarget::TopDisplayMode, InteractionSeedValue::TopDisplayMode(PersistedTopDisplayMode::from_display_mode( @@ -217,7 +180,6 @@ fn resolved_toolbar_item_seeds(config: &Config) -> crate::config::ResolvedToolba show_presets: toolbar.show_presets, show_step_section: toolbar.show_step_section, show_text_controls: toolbar.show_text_controls, - show_settings_section: toolbar.show_settings_section, }; legacy.apply_mode_override(toolbar.mode_overrides.for_mode(toolbar.layout_mode)); fold_legacy_section_flags( @@ -239,22 +201,10 @@ pub(super) fn toolbar_values( InteractionSeedTarget::TopPinned, InteractionSeedValue::Bool(input.toolbar_top_pinned), ), - Target::SidePinned => RuntimeUiMutationValues::one( - InteractionSeedTarget::SidePinned, - InteractionSeedValue::Bool(input.toolbar_side_pinned), - ), Target::TopMinimized => RuntimeUiMutationValues::one( InteractionSeedTarget::TopMinimized, InteractionSeedValue::Bool(input.toolbar_top_minimized), ), - Target::SideMinimized => RuntimeUiMutationValues::one( - InteractionSeedTarget::SideMinimized, - InteractionSeedValue::Bool(input.toolbar_side_minimized), - ), - Target::SidePane => RuntimeUiMutationValues::one( - InteractionSeedTarget::SidePane, - InteractionSeedValue::SidePane(input.toolbar_side_pane), - ), Target::TopDisplayMode => top_display_mode_values(input.toolbar_top_display_mode, input), Target::StatusBar => RuntimeUiMutationValues::one( InteractionSeedTarget::StatusBar, @@ -335,10 +285,6 @@ pub(super) fn toolbar_values( InteractionSeedTarget::StatusBarItem(item), InteractionSeedValue::Bool(input.status_bar_item_visible(item)), ), - Target::CollapsedSection(section) => RuntimeUiMutationValues::one( - InteractionSeedTarget::CollapsedSection(section), - InteractionSeedValue::Bool(input.toolbar_collapsed_side_sections.contains(§ion)), - ), Target::ItemVisibility { id, .. } => RuntimeUiMutationValues::one( InteractionSeedTarget::ItemVisibility(id), InteractionSeedValue::Visibility(item_visibility_setting( @@ -367,19 +313,12 @@ pub(super) fn toolbar_values( ) })) } - // The keyboard toggle drives both pins as one batched mutation: the - // same wire keys the pin buttons write, settled through one accepted - // revision so a restart cannot observe half a toggle. - Target::ToolbarVisibility => RuntimeUiMutationValues::batch([ - ( - InteractionSeedTarget::TopPinned, - InteractionSeedValue::Bool(input.toolbar_top_pinned), - ), - ( - InteractionSeedTarget::SidePinned, - InteractionSeedValue::Bool(input.toolbar_side_pinned), - ), - ]), + // The keyboard toggle drives the pin through the same wire key the pin + // button writes, settled through one accepted revision. + Target::ToolbarVisibility => RuntimeUiMutationValues::one( + InteractionSeedTarget::TopPinned, + InteractionSeedValue::Bool(input.toolbar_top_pinned), + ), } } diff --git a/src/backend/wayland/runtime_ui_state/tests.rs b/src/backend/wayland/runtime_ui_state/tests.rs index 7450a819..6998b910 100644 --- a/src/backend/wayland/runtime_ui_state/tests.rs +++ b/src/backend/wayland/runtime_ui_state/tests.rs @@ -25,16 +25,9 @@ fn input_from_config(config: &Config) -> InputState { input.toolbar_items = config.ui.toolbar.items.clone(); input.resolved_toolbar_items = input.toolbar_items.resolved(); input.toolbar_top_pinned = config.ui.toolbar.top_pinned; - input.toolbar_side_pinned = config.ui.toolbar.side_pinned; input.toolbar_top_minimized = config.ui.toolbar.top_minimized; - input.toolbar_side_minimized = config.ui.toolbar.side_minimized; input.toolbar_top_visible = config.ui.toolbar.top_pinned; - input.toolbar_side_visible = config.ui.toolbar.side_pinned; - input.toolbar_visible = input.toolbar_top_visible || input.toolbar_side_visible; - input.init_toolbar_side_panes_from_config( - &config.ui.toolbar.side_active_pane, - &config.ui.toolbar.collapsed_sections, - ); + input.toolbar_visible = input.toolbar_top_visible; input } @@ -167,10 +160,6 @@ fn commit_board_pin_toggle( fn config_positions(config: &Config) -> ToolbarPositionSnapshot { ToolbarPositionSnapshot { top: (config.ui.toolbar.top_offset, config.ui.toolbar.top_offset_y), - side: ( - config.ui.toolbar.side_offset_x, - config.ui.toolbar.side_offset, - ), } } @@ -229,34 +218,22 @@ fn commit_display_mode( } /// The pre-toggle pins as the visibility toggle's write path batches them. -fn pins_rollback_values(top: bool, side: bool) -> RuntimeUiMutationValues { - RuntimeUiMutationValues::batch([ - ( - InteractionSeedTarget::TopPinned, - InteractionSeedValue::Bool(top), - ), - ( - InteractionSeedTarget::SidePinned, - InteractionSeedValue::Bool(side), - ), - ]) +fn pins_rollback_values(top: bool) -> RuntimeUiMutationValues { + RuntimeUiMutationValues::batch([( + InteractionSeedTarget::TopPinned, + InteractionSeedValue::Bool(top), + )]) .expect("distinct pin targets batch") } /// The pre-toggle pins as the visibility toggle's rollback snapshot carries /// them (visibility itself is never persisted, so it is not in there). -fn pins_rollback(top: bool, side: bool) -> PreviewRollbackSnapshot { +fn pins_rollback(top: bool) -> PreviewRollbackSnapshot { PreviewRollbackSnapshot { - values: BTreeMap::from([ - ( - InteractionSeedTarget::TopPinned, - InteractionSeedValue::Bool(top), - ), - ( - InteractionSeedTarget::SidePinned, - InteractionSeedValue::Bool(side), - ), - ]), + values: BTreeMap::from([( + InteractionSeedTarget::TopPinned, + InteractionSeedValue::Bool(top), + )]), } } diff --git a/src/backend/wayland/runtime_ui_state/tests/board_pin_resets.rs b/src/backend/wayland/runtime_ui_state/tests/board_pin_resets.rs index 404bdd05..89ff1980 100644 --- a/src/backend/wayland/runtime_ui_state/tests/board_pin_resets.rs +++ b/src/backend/wayland/runtime_ui_state/tests/board_pin_resets.rs @@ -64,10 +64,7 @@ fn global_runtime_reset_clears_board_pin_override_and_live_value() { let drain = settle_runtime(&mut runtime); assert!(drain.rollbacks.is_empty()); assert!(drain.rebuild_live); - let mut positions = ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }; + let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; runtime.apply_live_state(&mut input, &mut positions); assert!(!board_pinned(&input, "whiteboard")); assert!(!runtime_path.exists()); diff --git a/src/backend/wayland/runtime_ui_state/tests/board_pins.rs b/src/backend/wayland/runtime_ui_state/tests/board_pins.rs index 2d540545..75696f29 100644 --- a/src/backend/wayland/runtime_ui_state/tests/board_pins.rs +++ b/src/backend/wayland/runtime_ui_state/tests/board_pins.rs @@ -61,10 +61,7 @@ value = true input .boards .sync_pin_seeds_from_config(&config.resolved_boards()); - let mut positions = ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }; + let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; let refresh = runtime.refresh_config_seeds(&config, &mut input, &mut positions); assert!(refresh.applied); @@ -94,10 +91,7 @@ value = true let config = Config::default(); let mut input = input_from_config(&config); let mut runtime = test_runtime(&config, &runtime_path); - let mut positions = ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }; + let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; let refresh = runtime.refresh_config_seeds(&config, &mut input, &mut positions); assert!(refresh.applied); @@ -137,13 +131,7 @@ value = true runtime.restore_board_identity(&config, &mut input, board_id.clone(), pin_seed, pinned); assert!(finish.is_none()); assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); - runtime.apply_live_state( - &mut input, - &mut ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }, - ); + runtime.apply_live_state(&mut input, &mut ToolbarPositionSnapshot { top: (0.0, 0.0) }); assert!(!board_pinned(&input, &board_id)); runtime.shutdown_blocking(); @@ -240,13 +228,7 @@ fn restored_board_pin_is_replayed_after_same_authority_recovery() { Some(PersistenceRecoveryResult::Recovered { .. }) )); assert!(rebuild_live); - runtime.apply_live_state( - &mut input, - &mut ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }, - ); + runtime.apply_live_state(&mut input, &mut ToolbarPositionSnapshot { top: (0.0, 0.0) }); assert!(!board_pinned(&input, &board_id)); let finishes = runtime.finish_deferred_board_pin_restores(&mut input); @@ -292,13 +274,7 @@ fn deferred_board_pin_restore_is_discarded_when_reset_changes_authority() { assert!(drain.rebuild_live); assert!(runtime.controller.active_barrier().is_none()); assert_ne!(runtime.controller.authority_epoch(), original_epoch); - runtime.apply_live_state( - &mut input, - &mut ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }, - ); + runtime.apply_live_state(&mut input, &mut ToolbarPositionSnapshot { top: (0.0, 0.0) }); assert!(!board_pinned(&input, &board_id)); assert!( @@ -329,10 +305,7 @@ fn delayed_delete_and_same_id_reuse_cannot_resurrect_old_board_pin() { runtime.restore_board_identity(&config, &mut input, "whiteboard".to_string(), false, false); assert!(finish.is_none()); assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); - let mut positions = ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }; + let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; runtime.apply_live_state(&mut input, &mut positions); assert!(!board_pinned(&input, "whiteboard")); runtime.shutdown_blocking(); @@ -367,10 +340,7 @@ fn stale_deferred_board_pin_is_rejected_after_authored_pin_reload() { input .boards .sync_pin_seeds_from_config(&config_b.resolved_boards()); - let mut positions = ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }; + let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; let refresh = runtime.refresh_config_seeds(&config_b, &mut input, &mut positions); assert!(refresh.applied); assert!(board_pinned(&input, "whiteboard")); diff --git a/src/backend/wayland/runtime_ui_state/tests/drag_previews.rs b/src/backend/wayland/runtime_ui_state/tests/drag_previews.rs index 7e732dd0..f2fbe541 100644 --- a/src/backend/wayland/runtime_ui_state/tests/drag_previews.rs +++ b/src/backend/wayland/runtime_ui_state/tests/drag_previews.rs @@ -6,10 +6,7 @@ fn item_drag_commit_accepts_one_revision_and_cancel_accepts_none() { let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); let mut input = input_from_config(&config); - let mut positions = ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }; + let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; let mut runtime = test_runtime(&config, &runtime_path); let before = runtime.controller.pipeline().latest_accepted(); @@ -65,10 +62,7 @@ fn unavailable_persistence_item_drag_cancel_restores_original_order() { .order .ordered_ids(ToolbarItemOrderGroup::TopTools) .to_vec(); - let mut positions = ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }; + let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; assert!(previews.begin_item_drag(ToolbarItemOrderGroup::TopTools, &input)); assert!(input.start_toolbar_item_drag(ToolbarItemOrderGroup::TopTools, ids::TOP_TOOL_PEN,)); @@ -101,16 +95,11 @@ fn unavailable_persistence_position_drag_cancel_restores_starting_offsets() { let mut previews = UnavailablePersistencePreviews::default(); let original = ToolbarPositionSnapshot { top: (config.ui.toolbar.top_offset, config.ui.toolbar.top_offset_y), - side: ( - config.ui.toolbar.side_offset_x, - config.ui.toolbar.side_offset, - ), }; let mut positions = original; - assert!(previews.begin_position_drag(MoveDragKind::Side, positions)); - positions.top.0 = 42.0; - positions.side = (43.0, 44.0); + assert!(previews.begin_position_drag(MoveDragKind::Top, positions)); + positions.top = (42.0, 43.0); let finish = previews.finish_position_drag(false); apply_finish(&mut input, &mut positions, finish); @@ -131,10 +120,6 @@ fn persistence_barrier_blocks_updates_without_consuming_untouched_drag_sessions( .to_vec(); let original_positions = ToolbarPositionSnapshot { top: (config.ui.toolbar.top_offset, config.ui.toolbar.top_offset_y), - side: ( - config.ui.toolbar.side_offset_x, - config.ui.toolbar.side_offset, - ), }; let positions = original_positions; let mut runtime = controller_only_runtime(&config, &runtime_path); @@ -172,10 +157,6 @@ fn relevant_reload_aborts_item_and_position_previews_without_restoring_old_seed( config_a.ui.toolbar.top_offset, config_a.ui.toolbar.top_offset_y, ), - side: ( - config_a.ui.toolbar.side_offset_x, - config_a.ui.toolbar.side_offset, - ), }; let mut runtime = test_runtime(&config_a, &runtime_path); let accepted_before = runtime.controller.pipeline().latest_accepted(); @@ -232,43 +213,6 @@ fn relevant_reload_aborts_item_and_position_previews_without_restoring_old_seed( runtime.shutdown_blocking(); } -#[test] -fn side_drag_top_seed_reload_restores_only_the_uncommitted_side_preview() { - let temp = crate::test_temp::tempdir().unwrap(); - let runtime_path = temp.path().join("runtime-ui.toml"); - let config_a = Config::default(); - let original_side = ( - config_a.ui.toolbar.side_offset_x, - config_a.ui.toolbar.side_offset, - ); - let mut positions = ToolbarPositionSnapshot { - top: ( - config_a.ui.toolbar.top_offset, - config_a.ui.toolbar.top_offset_y, - ), - side: original_side, - }; - let mut input = input_from_config(&config_a); - let mut runtime = test_runtime(&config_a, &runtime_path); - - assert!(runtime.begin_position_drag(MoveDragKind::Side, positions)); - positions.side = (42.0, 43.0); - - let mut config_b = config_a; - config_b.ui.toolbar.top_offset = 100.0; - config_b.ui.toolbar.top_offset_y = 101.0; - let refresh = runtime.refresh_config_seeds(&config_b, &mut input, &mut positions); - - assert!(refresh.applied); - assert!(refresh.position_drag_aborted); - assert_eq!(positions.top, (100.0, 101.0)); - assert_eq!( - positions.side, original_side, - "a top-seed reload must not leave the invalidated side preview live" - ); - runtime.shutdown_blocking(); -} - #[test] fn unrelated_position_reload_preserves_preview_and_cancel_only_restores_its_scope() { let temp = crate::test_temp::tempdir().unwrap(); @@ -278,21 +222,14 @@ fn unrelated_position_reload_preserves_preview_and_cancel_only_restores_its_scop config_a.ui.toolbar.top_offset, config_a.ui.toolbar.top_offset_y, ); - let mut positions = ToolbarPositionSnapshot { - top: original_top, - side: ( - config_a.ui.toolbar.side_offset_x, - config_a.ui.toolbar.side_offset, - ), - }; + let mut positions = ToolbarPositionSnapshot { top: original_top }; let mut input = input_from_config(&config_a); let mut runtime = test_runtime(&config_a, &runtime_path); assert!(runtime.begin_position_drag(MoveDragKind::Top, positions)); positions.top = (42.0, 43.0); let mut config_b = config_a; - config_b.ui.toolbar.side_offset_x = 120.0; - config_b.ui.toolbar.side_offset = 121.0; + config_b.ui.toolbar.top_minimized = !config_b.ui.toolbar.top_minimized; let refresh = runtime.refresh_config_seeds(&config_b, &mut input, &mut positions); assert!(refresh.applied); @@ -302,16 +239,10 @@ fn unrelated_position_reload_preserves_preview_and_cancel_only_restores_its_scop (42.0, 43.0), "unrelated reload keeps preview" ); - assert_eq!(positions.side, (120.0, 121.0)); let finish = runtime.finish_position_drag(false, positions); apply_finish(&mut input, &mut positions, finish); assert_eq!(positions.top, original_top); - assert_eq!( - positions.side, - (120.0, 121.0), - "top-drag rollback must not restore an unrelated side seed" - ); runtime.shutdown_blocking(); } @@ -326,10 +257,7 @@ fn release_during_barrier_is_consumed_once_and_never_replayed() { .order .ordered_ids(ToolbarItemOrderGroup::TopTools) .to_vec(); - let mut positions = ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }; + let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; let mut runtime = controller_only_runtime(&config, &runtime_path); assert!(runtime.begin_item_drag(ToolbarItemOrderGroup::TopTools, &input)); assert!(input.start_toolbar_item_drag(ToolbarItemOrderGroup::TopTools, ids::TOP_TOOL_PEN,)); @@ -394,10 +322,7 @@ fn external_source_conflict_rebuilds_live_toolbar_from_external_authority() { let config = Config::default(); assert!(config.ui.toolbar.top_pinned); let mut input = input_from_config(&config); - let mut positions = ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }; + let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; let mut runtime = controller_only_runtime(&config, &runtime_path); let original_order = input .resolved_toolbar_items diff --git a/src/backend/wayland/runtime_ui_state/tests/layout_state.rs b/src/backend/wayland/runtime_ui_state/tests/layout_state.rs index 91d3fb44..5e356019 100644 --- a/src/backend/wayland/runtime_ui_state/tests/layout_state.rs +++ b/src/backend/wayland/runtime_ui_state/tests/layout_state.rs @@ -23,11 +23,6 @@ fn committed_top_drag_writes_runtime_state_and_leaves_config_untouched() { stored_position(&runtime, InteractionSeedTarget::TopPosition), Some((42.5, -7.25)) ); - assert_eq!( - stored_position(&runtime, InteractionSeedTarget::SidePosition), - None, - "a top drag never claims the side position" - ); assert_eq!(fs::read(&config_path).unwrap(), AUTHORED); let stored = fs::read_to_string(&runtime_path).unwrap(); assert!(stored.contains("top_position"), "{stored}"); @@ -38,45 +33,9 @@ fn committed_top_drag_writes_runtime_state_and_leaves_config_untouched() { let mut restored = config_positions(&config); restarted.apply_startup_positions(&mut restored); assert_eq!(restored.top, (42.5, -7.25)); - assert_eq!(restored.side, config_positions(&config).side); assert_eq!(fs::read(&config_path).unwrap(), AUTHORED); } -#[test] -fn committed_side_drag_stores_both_position_overrides_in_one_write() { - let temp = crate::test_temp::tempdir().unwrap(); - let runtime_path = temp.path().join("runtime-ui.toml"); - let config = Config::default(); - let mut runtime = test_runtime(&config, &runtime_path); - let mut positions = config_positions(&config); - let accepted_before = runtime.controller.pipeline().latest_accepted(); - - assert!(runtime.begin_position_drag(MoveDragKind::Side, positions)); - // A side drag reconciles the top strip's X base before it commits. - positions.top = (16.0, positions.top.1); - positions.side = (-30.0, 12.0); - assert!(matches!( - runtime.finish_position_drag(true, positions), - ToolbarRuntimeFinish::KeepPreview - )); - assert_eq!( - runtime.controller.pipeline().latest_accepted().get(), - accepted_before.get() + 1, - "both overrides settle through one accepted revision" - ); - assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); - - assert_eq!( - stored_position(&runtime, InteractionSeedTarget::TopPosition), - Some((16.0, config.ui.toolbar.top_offset_y)) - ); - assert_eq!( - stored_position(&runtime, InteractionSeedTarget::SidePosition), - Some((-30.0, 12.0)) - ); - runtime.shutdown_blocking(); -} - #[test] fn a_drag_back_to_the_authored_position_deletes_its_override() { let temp = crate::test_temp::tempdir().unwrap(); @@ -149,15 +108,17 @@ fn a_drag_without_a_runtime_store_is_process_only_and_never_touches_config() { let seeded = config_positions(&config); let mut positions = seeded; - assert!(previews.begin_position_drag(MoveDragKind::Side, positions)); - positions.top.0 = 42.0; - positions.side = (43.0, 44.0); + assert!(previews.begin_position_drag(MoveDragKind::Top, positions)); + positions.top = (42.0, 43.0); let finish = previews.finish_position_drag(true); assert!(matches!(finish, ToolbarRuntimeFinish::KeepPreview)); apply_finish(&mut input, &mut positions, finish); - assert_eq!(positions.top.0, 42.0, "the committed drag stays on screen"); - assert_eq!(positions.side, (43.0, 44.0)); + assert_eq!( + positions.top, + (42.0, 43.0), + "the committed drag stays on screen" + ); assert_eq!(fs::read(&config_path).unwrap(), AUTHORED); } 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 be68c85e..9b882e94 100644 --- a/src/backend/wayland/runtime_ui_state/tests/preference_actions.rs +++ b/src/backend/wayland/runtime_ui_state/tests/preference_actions.rs @@ -135,10 +135,7 @@ fn a_rollback_restores_every_durable_chrome_preference() { let config = Config::default(); let mut input = input_from_config(&config); - let mut positions = ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }; + let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; // Move every preference away from its configured value, then roll back to // exactly the values the run started with. diff --git a/src/backend/wayland/runtime_ui_state/tests/preferences.rs b/src/backend/wayland/runtime_ui_state/tests/preferences.rs index 0b2ef8af..e4dad34a 100644 --- a/src/backend/wayland/runtime_ui_state/tests/preferences.rs +++ b/src/backend/wayland/runtime_ui_state/tests/preferences.rs @@ -162,10 +162,7 @@ fn a_seed_refresh_does_not_prune_persisted_preference_overrides() { assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); // Whatever else the run does, the seed baseline stays authored. - let mut positions = ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }; + let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; runtime.refresh_config_seeds(&config, &mut input, &mut positions); assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); runtime.shutdown_blocking(); diff --git a/src/backend/wayland/runtime_ui_state/tests/recovery.rs b/src/backend/wayland/runtime_ui_state/tests/recovery.rs index af650f19..34ca6945 100644 --- a/src/backend/wayland/runtime_ui_state/tests/recovery.rs +++ b/src/backend/wayland/runtime_ui_state/tests/recovery.rs @@ -8,12 +8,8 @@ fn toolbar_seed_registry_covers_every_runtime_routed_target() { for target in [ InteractionSeedTarget::TopPinned, - InteractionSeedTarget::SidePinned, InteractionSeedTarget::TopMinimized, - InteractionSeedTarget::SideMinimized, - InteractionSeedTarget::SidePane, InteractionSeedTarget::TopPosition, - InteractionSeedTarget::SidePosition, InteractionSeedTarget::TopDisplayMode, InteractionSeedTarget::ToolbarLayoutMode, InteractionSeedTarget::ClickHighlight, @@ -31,13 +27,6 @@ fn toolbar_seed_registry_covers_every_runtime_routed_target() { "missing section seed for {flag:?}" ); } - for section in ToolbarSideSection::ALL { - assert!( - seeds - .get(&InteractionSeedTarget::CollapsedSection(section)) - .is_some() - ); - } for id in resettable_individual_toolbar_item_ids() { assert!( seeds @@ -81,7 +70,7 @@ fn toolbar_section_visibility_is_not_seeded_into_runtime_state() { } #[test] -fn runtime_rebuild_reuses_minimize_and_pane_transition_cleanup() { +fn runtime_rebuild_reuses_minimize_transition_cleanup() { let temp = crate::test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); @@ -98,30 +87,16 @@ fn runtime_rebuild_reuses_minimize_and_pane_transition_cleanup() { )); assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); - let pane = runtime - .begin_toolbar_mutation(ToolbarRuntimeUiPersistenceTarget::SidePane, &source) - .expect("side-pane permit"); - source.toolbar_side_pane = SidePane::Canvas; - assert!(matches!( - runtime.finish_toolbar_mutation(pane, true, &source), - ToolbarRuntimeFinish::KeepPreview - )); - assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); - let mut rebuilt = input_from_config(&config); rebuilt.toolbar_shapes_expanded = true; rebuilt.toolbar_top_overflow_open = true; rebuilt.toolbar_session_popover_open = true; rebuilt.toolbar_settings_popover_open = true; rebuilt.toolbar_canvas_popover_open = true; - rebuilt.toolbar_side_pane = SidePane::Settings; rebuilt.toolbar_customize_items_open = true; rebuilt.toolbar_customize_items_group = - Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::SideSections); - let mut positions = ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }; + Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::TopTools); + let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; runtime.apply_live_state(&mut rebuilt, &mut positions); @@ -131,7 +106,6 @@ fn runtime_rebuild_reuses_minimize_and_pane_transition_cleanup() { assert!(!rebuilt.toolbar_session_popover_open); assert!(!rebuilt.toolbar_settings_popover_open); assert!(!rebuilt.toolbar_canvas_popover_open); - assert_eq!(rebuilt.toolbar_side_pane, SidePane::Canvas); assert!(!rebuilt.toolbar_customize_items_open); assert!(rebuilt.toolbar_customize_items_group.is_none()); runtime.shutdown_blocking(); @@ -175,10 +149,7 @@ fn supported_runtime_reset_returns_live_state_to_configured_defaults() { ); assert!(!runtime_path.exists()); - let mut positions = ToolbarPositionSnapshot { - top: (0.0, 0.0), - side: (0.0, 0.0), - }; + let mut positions = ToolbarPositionSnapshot { top: (0.0, 0.0) }; runtime.apply_live_state(&mut input, &mut positions); assert_eq!(input.toolbar_top_pinned, config.ui.toolbar.top_pinned); } @@ -376,10 +347,6 @@ fn cancelling_read_only_recovery_rebuilds_a_staged_seed_reload() { config_a.ui.toolbar.top_offset, config_a.ui.toolbar.top_offset_y, ), - side: ( - config_a.ui.toolbar.side_offset_x, - config_a.ui.toolbar.side_offset, - ), }; let mut runtime = test_runtime_allow_startup_incident(&config_a, &runtime_path); @@ -410,7 +377,8 @@ fn cancelling_read_only_recovery_rebuilds_a_staged_seed_reload() { #[test] fn runtime_toolbar_routes_leave_authored_config_bytes_exactly_unchanged() { - const AUTHORED: &[u8] = b"# keep this formatting and comment\n[ui.toolbar]\ntop_pinned = true\nside_pinned = true\n"; + const AUTHORED: &[u8] = + b"# keep this formatting and comment\n[ui.toolbar]\ntop_pinned = true\n"; let temp = crate::test_temp::tempdir().unwrap(); let config_path = temp.path().join("config.toml"); let runtime_path = temp.path().join("data/runtime-ui.toml"); @@ -421,11 +389,7 @@ fn runtime_toolbar_routes_leave_authored_config_bytes_exactly_unchanged() { let mutations = [ ToolbarRuntimeUiPersistenceTarget::TopPinned, - ToolbarRuntimeUiPersistenceTarget::SidePinned, ToolbarRuntimeUiPersistenceTarget::TopMinimized, - ToolbarRuntimeUiPersistenceTarget::SideMinimized, - ToolbarRuntimeUiPersistenceTarget::SidePane, - ToolbarRuntimeUiPersistenceTarget::CollapsedSection(ToolbarSideSection::Colors), ToolbarRuntimeUiPersistenceTarget::TopDisplayMode, ToolbarRuntimeUiPersistenceTarget::ToolbarVisibility, ]; @@ -434,24 +398,13 @@ fn runtime_toolbar_routes_leave_authored_config_bytes_exactly_unchanged() { .begin_toolbar_mutation(target, &input) .expect("runtime mutation permit"); match target { - ToolbarRuntimeUiPersistenceTarget::TopPinned => input.toolbar_top_pinned = false, - ToolbarRuntimeUiPersistenceTarget::SidePinned => input.toolbar_side_pinned = false, - ToolbarRuntimeUiPersistenceTarget::ToolbarVisibility => { + ToolbarRuntimeUiPersistenceTarget::TopPinned + | ToolbarRuntimeUiPersistenceTarget::ToolbarVisibility => { input.toolbar_top_pinned = false; - input.toolbar_side_pinned = false; } ToolbarRuntimeUiPersistenceTarget::TopMinimized => { input.toolbar_top_minimized = true; } - ToolbarRuntimeUiPersistenceTarget::SideMinimized => { - input.toolbar_side_minimized = true; - } - ToolbarRuntimeUiPersistenceTarget::SidePane => { - input.toolbar_side_pane = SidePane::Settings; - } - ToolbarRuntimeUiPersistenceTarget::CollapsedSection(section) => { - input.toolbar_collapsed_side_sections.insert(section); - } ToolbarRuntimeUiPersistenceTarget::TopDisplayMode => { input.set_top_display_mode(crate::config::TopDisplayMode::Micro); } diff --git a/src/backend/wayland/runtime_ui_state/tests/visibility.rs b/src/backend/wayland/runtime_ui_state/tests/visibility.rs index 3ad3a97e..521625e9 100644 --- a/src/backend/wayland/runtime_ui_state/tests/visibility.rs +++ b/src/backend/wayland/runtime_ui_state/tests/visibility.rs @@ -12,7 +12,7 @@ fn keyboard_visibility_toggle_persists_both_pins_and_startup_hides_the_toolbar() let temp = crate::test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); - assert!(config.ui.toolbar.top_pinned && config.ui.toolbar.side_pinned); + assert!(config.ui.toolbar.top_pinned); let mut input = input_from_config(&config); let mut runtime = test_runtime(&config, &runtime_path); let accepted_before = runtime.controller.pipeline().latest_accepted(); @@ -21,25 +21,18 @@ fn keyboard_visibility_toggle_persists_both_pins_and_startup_hides_the_toolbar() // applied, so the drained entry carries the pre-toggle pins, which // supply the write's rollback. input.handle_action(Action::ToggleToolbar); - assert!(!input.toolbar_top_pinned && !input.toolbar_side_pinned); + assert!(!input.toolbar_top_pinned); assert_eq!( input.take_pending_toolbar_persistence(), vec![PendingToolbarPersistence::Visibility { previous_top_pinned: true, - previous_side_pinned: true, }], "F9 must queue exactly one visibility entry carrying the pre-toggle pins" ); - let rollback = RuntimeUiMutationValues::batch([ - ( - InteractionSeedTarget::TopPinned, - InteractionSeedValue::Bool(true), - ), - ( - InteractionSeedTarget::SidePinned, - InteractionSeedValue::Bool(true), - ), - ]) + let rollback = RuntimeUiMutationValues::batch([( + InteractionSeedTarget::TopPinned, + InteractionSeedValue::Bool(true), + )]) .unwrap(); let prepared = runtime .begin_toolbar_mutation_with_rollback( @@ -54,17 +47,13 @@ fn keyboard_visibility_toggle_persists_both_pins_and_startup_hides_the_toolbar() assert_eq!( runtime.controller.pipeline().latest_accepted().get(), accepted_before.get() + 1, - "both pin overrides settle through one accepted revision" + "the pin override settles through one accepted revision" ); assert!(settle_runtime(&mut runtime).rollbacks.is_empty()); assert_eq!( stored_bool(&runtime, InteractionSeedTarget::TopPinned), Some(false) ); - assert_eq!( - stored_bool(&runtime, InteractionSeedTarget::SidePinned), - Some(false) - ); runtime.shutdown_blocking(); let mut restarted_input = input_from_config(&config); @@ -72,12 +61,9 @@ fn keyboard_visibility_toggle_persists_both_pins_and_startup_hides_the_toolbar() let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state(&mut restarted_input); assert!(!restarted_input.toolbar_top_pinned); - assert!(!restarted_input.toolbar_side_pinned); assert!( - !restarted_input.toolbar_visible - && !restarted_input.toolbar_top_visible - && !restarted_input.toolbar_side_visible, - "both-pins-false overrides start the toolbar hidden" + !restarted_input.toolbar_visible && !restarted_input.toolbar_top_visible, + "a pin-false override starts the toolbar hidden" ); restarted.shutdown_blocking(); } @@ -90,126 +76,40 @@ fn a_rolled_back_hide_toggle_restores_live_visibility_from_the_pins() { let config = Config::default(); let mut input = input_from_config(&config); let mut positions = config_positions(&config); - // Post-toggle live state: everything hidden, both pins driven false. + // Post-toggle live state: everything hidden, the pin driven false. assert!(input.set_toolbar_visible(false)); input.toolbar_top_pinned = false; - input.toolbar_side_pinned = false; - apply_toolbar_runtime_rollback(&mut input, &mut positions, &pins_rollback(true, true)); + apply_toolbar_runtime_rollback(&mut input, &mut positions, &pins_rollback(true)); - assert!(input.toolbar_top_pinned && input.toolbar_side_pinned); + assert!(input.toolbar_top_pinned); assert!( - input.toolbar_visible && input.toolbar_top_visible && input.toolbar_side_visible, + input.toolbar_visible && input.toolbar_top_visible, "restored pins must bring the live toolbar back" ); } /// The reverse direction: rolling back a show must hide the toolbar again, -/// not leave it visible over both-pins-false. +/// not leave it visible over a false pin. #[test] fn a_rolled_back_show_toggle_re_hides_the_toolbar() { let mut config = Config::default(); config.ui.toolbar.top_pinned = false; - config.ui.toolbar.side_pinned = false; let mut input = input_from_config(&config); let mut positions = config_positions(&config); - // Post-toggle live state: everything shown, both pins driven true. + // Post-toggle live state: everything shown, the pin driven true. assert!(input.set_toolbar_visible(true)); input.toolbar_top_pinned = true; - input.toolbar_side_pinned = true; - apply_toolbar_runtime_rollback(&mut input, &mut positions, &pins_rollback(false, false)); + apply_toolbar_runtime_rollback(&mut input, &mut positions, &pins_rollback(false)); - assert!(!input.toolbar_top_pinned && !input.toolbar_side_pinned); + assert!(!input.toolbar_top_pinned); assert!( - !input.toolbar_visible && !input.toolbar_top_visible && !input.toolbar_side_visible, + !input.toolbar_visible && !input.toolbar_top_visible, "restored pins must hide the live toolbar again" ); } -/// A hide pressed while the top strip sat on the cycle's hidden rung: the -/// rollback re-derives the visibility flags but must not unfold the still -/// live `Hidden` mode — that combination *is* the pre-toggle screen (side -/// palette back, top strip cycle-hidden), and any future show unfolds it. -/// -/// Driven through the real actions: only the deprecated Panel side layout -/// reaches this state with a persisted toggle, because its still-visible -/// side palette makes F9 a genuine HIDE (pins true→false). Under the -/// shipping pill layout the same press is a show whose pins do not change, -/// and the toggle arm queues no persistence at all -/// (`cycle_hidden_show_with_unchanged_pins_queues_no_persistence` in the -/// input-state tests). -#[test] -fn a_rolled_back_hide_toggle_keeps_a_cycle_hidden_strip_folded() { - use crate::domain::Action; - use crate::input::state::PendingToolbarPersistence; - - let config = Config::default(); - let mut input = input_from_config(&config); - let mut positions = config_positions(&config); - input.init_toolbar_side_layout_from_config(crate::config::ToolbarSideLayout::Panel); - input.handle_action(Action::CycleToolbarDisplay); // micro - input.handle_action(Action::CycleToolbarDisplay); // hidden - assert_eq!(input.toolbar_top_display_mode, TopDisplayMode::Hidden); - input.take_pending_toolbar_persistence(); // drain the cycle's display-mode write - assert!( - input.toolbar_visible(), - "the Panel side palette keeps a surface visible under a cycle-hidden strip" - ); - - input.handle_action(Action::ToggleToolbar); // a real HIDE - assert!(!input.toolbar_visible()); - assert!(!input.toolbar_top_pinned && !input.toolbar_side_pinned); - assert_eq!( - input.take_pending_toolbar_persistence(), - vec![PendingToolbarPersistence::Visibility { - previous_top_pinned: true, - previous_side_pinned: true, - }], - "a pin-changing hide queues its persistence" - ); - - apply_toolbar_runtime_rollback(&mut input, &mut positions, &pins_rollback(true, true)); - - assert!(input.toolbar_top_pinned && input.toolbar_side_pinned); - assert!(input.toolbar_visible && input.toolbar_top_visible && input.toolbar_side_visible); - assert_eq!( - input.toolbar_top_display_mode, - TopDisplayMode::Hidden, - "the rollback must not unfold the cycle-hidden strip" - ); - // The pre-toggle screen: side palette back, top strip still cycle-hidden. - assert!(input.toolbar_side_visible() && !input.toolbar_top_visible()); -} - -/// Control: the pin buttons persist single-pin scopes and are deliberately -/// decoupled from live visibility, so a single-pin rollback restores that -/// pin and leaves every visibility flag untouched. -#[test] -fn a_single_pin_rollback_never_touches_live_visibility() { - let config = Config::default(); - let mut input = input_from_config(&config); - let mut positions = config_positions(&config); - // Pins and visibility legitimately disagree: an implicit hide (focus - // mode, presenter mode) clears the flags without touching the pins. - assert!(input.set_toolbar_visible(false)); - input.toolbar_side_pinned = false; - - let rollback = PreviewRollbackSnapshot { - values: BTreeMap::from([( - InteractionSeedTarget::SidePinned, - InteractionSeedValue::Bool(true), - )]), - }; - apply_toolbar_runtime_rollback(&mut input, &mut positions, &rollback); - - assert!(input.toolbar_side_pinned); - assert!( - !input.toolbar_visible && !input.toolbar_top_visible && !input.toolbar_side_visible, - "a pin-button rollback must not re-derive visibility" - ); -} - /// End-to-end through the store's forced-rollback path: a visibility toggle /// abandoned behind a failed reset barrier resolves to a drained rollback, /// and applying it restores the pre-toggle screen, not just the pins. @@ -218,21 +118,15 @@ fn visibility_toggle_rollback_through_a_failed_reset_restores_the_screen() { let temp = crate::test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); - assert!(config.ui.toolbar.top_pinned && config.ui.toolbar.side_pinned); + assert!(config.ui.toolbar.top_pinned); let mut input = input_from_config(&config); let mut positions = config_positions(&config); let mut runtime = controller_only_runtime(&config, &runtime_path); - let rollback = RuntimeUiMutationValues::batch([ - ( - InteractionSeedTarget::TopPinned, - InteractionSeedValue::Bool(input.toolbar_top_pinned), - ), - ( - InteractionSeedTarget::SidePinned, - InteractionSeedValue::Bool(input.toolbar_side_pinned), - ), - ]) + let rollback = RuntimeUiMutationValues::batch([( + InteractionSeedTarget::TopPinned, + InteractionSeedValue::Bool(input.toolbar_top_pinned), + )]) .unwrap(); let prepared = runtime .begin_toolbar_mutation_with_rollback( @@ -242,7 +136,6 @@ fn visibility_toggle_rollback_through_a_failed_reset_restores_the_screen() { .expect("visibility permit"); assert!(input.set_toolbar_visible(false)); input.toolbar_top_pinned = false; - input.toolbar_side_pinned = false; let reset = match runtime.controller.request_supported_reset() { RequestResetResult::Started { .. } => runtime @@ -269,9 +162,9 @@ fn visibility_toggle_rollback_through_a_failed_reset_restores_the_screen() { let drain = runtime.drain_writer_completions(); assert_eq!(drain.rollbacks.len(), 1); apply_toolbar_runtime_rollback(&mut input, &mut positions, &drain.rollbacks[0]); - assert!(input.toolbar_top_pinned && input.toolbar_side_pinned); + assert!(input.toolbar_top_pinned); assert!( - input.toolbar_visible && input.toolbar_top_visible && input.toolbar_side_visible, + input.toolbar_visible && input.toolbar_top_visible, "the forced rollback must leave the screen matching the restored pins" ); } @@ -292,7 +185,7 @@ fn a_barrier_defers_queued_visibility_persistence_instead_of_dropping_it() { let temp = crate::test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); - assert!(config.ui.toolbar.top_pinned && config.ui.toolbar.side_pinned); + assert!(config.ui.toolbar.top_pinned); let mut input = input_from_config(&config); let mut runtime = controller_only_runtime(&config, &runtime_path); @@ -314,7 +207,7 @@ fn a_barrier_defers_queued_visibility_persistence_instead_of_dropping_it() { runtime .begin_toolbar_mutation_with_rollback( ToolbarRuntimeUiPersistenceTarget::ToolbarVisibility, - pins_rollback_values(true, true), + pins_rollback_values(true), ) .is_none(), "the barrier refuses new mutations, which is why the drain defers" @@ -347,14 +240,13 @@ fn a_barrier_defers_queued_visibility_persistence_instead_of_dropping_it() { input.take_pending_toolbar_persistence(), vec![PendingToolbarPersistence::Visibility { previous_top_pinned: true, - previous_side_pinned: true, }], "the deferred entry must survive the barrier untouched" ); let prepared = runtime .begin_toolbar_mutation_with_rollback( ToolbarRuntimeUiPersistenceTarget::ToolbarVisibility, - pins_rollback_values(true, true), + pins_rollback_values(true), ) .expect("visibility permit after the barrier resolves"); assert!(matches!( @@ -370,10 +262,6 @@ fn a_barrier_defers_queued_visibility_persistence_instead_of_dropping_it() { stored_bool(&runtime, InteractionSeedTarget::TopPinned), Some(false) ); - assert_eq!( - stored_bool(&runtime, InteractionSeedTarget::SidePinned), - Some(false) - ); } /// The teardown race: a reset is in flight, F9 changes the screen, and the @@ -391,7 +279,7 @@ fn an_exit_during_an_active_reset_barrier_still_lands_the_deferred_toggle() { let temp = crate::test_temp::tempdir().unwrap(); let runtime_path = temp.path().join("runtime-ui.toml"); let config = Config::default(); - assert!(config.ui.toolbar.top_pinned && config.ui.toolbar.side_pinned); + assert!(config.ui.toolbar.top_pinned); let mut input = input_from_config(&config); let mut positions = config_positions(&config); let mut runtime = test_runtime(&config, &runtime_path); @@ -421,14 +309,13 @@ fn an_exit_during_an_active_reset_barrier_still_lands_the_deferred_toggle() { input.take_pending_toolbar_persistence(), vec![PendingToolbarPersistence::Visibility { previous_top_pinned: true, - previous_side_pinned: true, }], "the deferred entry must survive the settled barrier" ); let prepared = runtime .begin_toolbar_mutation_with_rollback( ToolbarRuntimeUiPersistenceTarget::ToolbarVisibility, - pins_rollback_values(true, true), + pins_rollback_values(true), ) .expect("visibility permit after teardown settled the barrier"); assert!(matches!( @@ -442,7 +329,6 @@ fn an_exit_during_an_active_reset_barrier_still_lands_the_deferred_toggle() { let mut restarted = test_runtime(&config, &runtime_path); restarted.apply_startup_state(&mut restarted_input); assert!(!restarted_input.toolbar_top_pinned); - assert!(!restarted_input.toolbar_side_pinned); assert!( !restarted_input.toolbar_visible, "the toggle pressed during the reset barrier must survive the exit" diff --git a/src/backend/wayland/runtime_ui_state/tests/visibility_recovery.rs b/src/backend/wayland/runtime_ui_state/tests/visibility_recovery.rs index f059e9ca..b951405e 100644 --- a/src/backend/wayland/runtime_ui_state/tests/visibility_recovery.rs +++ b/src/backend/wayland/runtime_ui_state/tests/visibility_recovery.rs @@ -19,7 +19,7 @@ fn an_exit_during_retry_pending_recovery_still_lands_the_deferred_toggle() { fs::create_dir(&store_dir).unwrap(); let runtime_path = store_dir.join("runtime-ui.toml"); let config = Config::default(); - assert!(config.ui.toolbar.top_pinned && config.ui.toolbar.side_pinned); + assert!(config.ui.toolbar.top_pinned); let mut input = input_from_config(&config); let mut runtime = test_runtime(&config, &runtime_path); @@ -53,14 +53,13 @@ fn an_exit_during_retry_pending_recovery_still_lands_the_deferred_toggle() { input.take_pending_toolbar_persistence(), vec![PendingToolbarPersistence::Visibility { previous_top_pinned: true, - previous_side_pinned: true, }], "the deferred entry must survive the settled recovery" ); let prepared = runtime .begin_toolbar_mutation_with_rollback( ToolbarRuntimeUiPersistenceTarget::ToolbarVisibility, - pins_rollback_values(true, true), + pins_rollback_values(true), ) .expect("visibility permit after recovery settled"); assert!(matches!( @@ -79,7 +78,6 @@ fn an_exit_during_retry_pending_recovery_still_lands_the_deferred_toggle() { "the recovery must have landed the write that originally failed" ); assert!(!restarted_input.toolbar_top_pinned); - assert!(!restarted_input.toolbar_side_pinned); assert!( !restarted_input.toolbar_visible, "the toggle pressed during the recovery barrier must survive the exit" @@ -101,7 +99,7 @@ fn a_deferred_hide_rollback_lands_in_the_presenter_restore_snapshot() { let mut input = input_from_config(&config); let mut positions = config_positions(&config); - input.handle_action(Action::ToggleToolbar); // hide, pins → false/false + input.handle_action(Action::ToggleToolbar); // hide, pin → false assert!(!input.toolbar_visible()); input.take_pending_toolbar_persistence(); // the write whose rollback arrives below @@ -109,18 +107,18 @@ fn a_deferred_hide_rollback_lands_in_the_presenter_restore_snapshot() { input.toggle_presenter_mode(); assert!(input.presenter_mode); - apply_toolbar_runtime_rollback(&mut input, &mut positions, &pins_rollback(true, true)); + apply_toolbar_runtime_rollback(&mut input, &mut positions, &pins_rollback(true)); - assert!(input.toolbar_top_pinned && input.toolbar_side_pinned); + assert!(input.toolbar_top_pinned); assert!( - !input.toolbar_visible && !input.toolbar_top_visible && !input.toolbar_side_visible, + !input.toolbar_visible && !input.toolbar_top_visible, "the live presenter-hidden flags must not move under the owner" ); input.toggle_presenter_mode(); assert!(!input.presenter_mode); assert!( - input.toolbar_visible && input.toolbar_top_visible && input.toolbar_side_visible, + input.toolbar_visible && input.toolbar_top_visible, "presenter exit must restore visibility agreeing with the rolled-back pins" ); } @@ -146,25 +144,25 @@ fn a_deferred_hide_rollback_lands_in_the_focus_mode_snapshot() { 720, ); - input.handle_action(Action::ToggleToolbar); // hide, pins → false/false + input.handle_action(Action::ToggleToolbar); // hide, pin → false assert!(!input.toolbar_visible()); input.take_pending_toolbar_persistence(); // the write whose rollback arrives below input.handle_action(Action::ToggleFocusMode); assert!(input.focus_mode_active()); - apply_toolbar_runtime_rollback(&mut input, &mut positions, &pins_rollback(true, true)); + apply_toolbar_runtime_rollback(&mut input, &mut positions, &pins_rollback(true)); - assert!(input.toolbar_top_pinned && input.toolbar_side_pinned); + assert!(input.toolbar_top_pinned); assert!( - !input.toolbar_visible && !input.toolbar_top_visible && !input.toolbar_side_visible, + !input.toolbar_visible && !input.toolbar_top_visible, "the live focus-hidden flags must not move under the owner" ); input.handle_action(Action::ToggleFocusMode); // restore assert!(!input.focus_mode_active()); assert!( - input.toolbar_visible && input.toolbar_top_visible && input.toolbar_side_visible, + input.toolbar_visible && input.toolbar_top_visible, "focus exit must restore visibility agreeing with the rolled-back pins" ); } @@ -181,25 +179,25 @@ fn a_deferred_hide_rollback_lands_in_the_light_mode_snapshot() { // Light mode refuses to start without layer-shell passthrough support. input.compositor_capabilities.layer_shell = true; - input.handle_action(Action::ToggleToolbar); // hide, pins → false/false + input.handle_action(Action::ToggleToolbar); // hide, pin → false assert!(!input.toolbar_visible()); input.take_pending_toolbar_persistence(); // the write whose rollback arrives below input.handle_action(Action::ToggleLightMode); assert!(input.light_mode); - apply_toolbar_runtime_rollback(&mut input, &mut positions, &pins_rollback(true, true)); + apply_toolbar_runtime_rollback(&mut input, &mut positions, &pins_rollback(true)); - assert!(input.toolbar_top_pinned && input.toolbar_side_pinned); + assert!(input.toolbar_top_pinned); assert!( - !input.toolbar_visible && !input.toolbar_top_visible && !input.toolbar_side_visible, + !input.toolbar_visible && !input.toolbar_top_visible, "the live light-mode-hidden flags must not move under the owner" ); input.handle_action(Action::ToggleLightMode); // exit restores the snapshot assert!(!input.light_mode); assert!( - input.toolbar_visible && input.toolbar_top_visible && input.toolbar_side_visible, + input.toolbar_visible && input.toolbar_top_visible, "light-mode exit must restore visibility agreeing with the rolled-back pins" ); } diff --git a/src/backend/wayland/runtime_ui_state/wayland.rs b/src/backend/wayland/runtime_ui_state/wayland.rs index adb932c9..9c51a743 100644 --- a/src/backend/wayland/runtime_ui_state/wayland.rs +++ b/src/backend/wayland/runtime_ui_state/wayland.rs @@ -2,19 +2,9 @@ use super::super::state::{MoveDragKind, WaylandState}; use super::*; impl WaylandState { - fn reset_runtime_side_focus_if_pane_changed( - &mut self, - previous_pane: crate::ui::toolbar::SidePane, - ) { - if self.input_state.toolbar_side_pane != previous_pane { - self.reset_side_toolbar_focus(); - } - } - pub(in crate::backend::wayland) fn toolbar_position_snapshot(&self) -> ToolbarPositionSnapshot { ToolbarPositionSnapshot { top: (self.toolbar_top_offset(), self.toolbar_top_offset_y()), - side: (self.toolbar_side_offset_x(), self.toolbar_side_offset()), } } @@ -26,10 +16,8 @@ impl WaylandState { return; }; let mut positions = self.toolbar_position_snapshot(); - let previous_pane = self.input_state.toolbar_side_pane; apply_toolbar_runtime_rollback(&mut self.input_state, &mut positions, &rollback); - self.reset_runtime_side_focus_if_pane_changed(previous_pane); - self.restore_toolbar_offsets(positions.top, positions.side); + self.restore_toolbar_offsets(positions.top); self.toolbar.mark_dirty(); self.input_state.dirty_tracker.mark_full(); self.input_state.needs_redraw = true; @@ -145,19 +133,12 @@ impl WaylandState { pub(in crate::backend::wayland) fn persist_toolbar_visibility( &mut self, previous_top_pinned: bool, - previous_side_pinned: bool, ) { let target = ToolbarRuntimeUiPersistenceTarget::ToolbarVisibility; - let rollback = match RuntimeUiMutationValues::batch([ - ( - InteractionSeedTarget::TopPinned, - InteractionSeedValue::Bool(previous_top_pinned), - ), - ( - InteractionSeedTarget::SidePinned, - InteractionSeedValue::Bool(previous_side_pinned), - ), - ]) { + let rollback = match RuntimeUiMutationValues::one( + InteractionSeedTarget::TopPinned, + InteractionSeedValue::Bool(previous_top_pinned), + ) { Ok(values) => values, Err(error) => { log::error!("Toolbar visibility toggle has invalid rollback values: {error:?}"); @@ -267,8 +248,7 @@ impl WaylandState { } PendingToolbarPersistence::Visibility { previous_top_pinned, - previous_side_pinned, - } => self.persist_toolbar_visibility(previous_top_pinned, previous_side_pinned), + } => self.persist_toolbar_visibility(previous_top_pinned), PendingToolbarPersistence::StatusBar { previous } => self .persist_keyboard_chrome_toggle( ToolbarRuntimeUiPersistenceTarget::StatusBar, @@ -334,7 +314,6 @@ impl WaylandState { .boards .sync_pin_seeds_from_config(&configured_boards); let mut positions = self.toolbar_position_snapshot(); - let previous_pane = self.input_state.toolbar_side_pane; let Some(runtime) = self.runtime_ui.as_mut() else { return; }; @@ -351,8 +330,7 @@ impl WaylandState { self.cancel_toolbar_move_drag(); self.cancel_gtk_toolbar_drag_lifecycle(); } - self.reset_runtime_side_focus_if_pane_changed(previous_pane); - self.restore_toolbar_offsets(positions.top, positions.side); + self.restore_toolbar_offsets(positions.top); self.toolbar.mark_dirty(); self.input_state.dirty_tracker.mark_full(); self.input_state.needs_redraw = true; @@ -445,12 +423,10 @@ impl WaylandState { self.cancel_toolbar_move_drag(); self.cancel_gtk_toolbar_drag_lifecycle(); let mut positions = self.toolbar_position_snapshot(); - let previous_pane = self.input_state.toolbar_side_pane; if let Some(runtime) = self.runtime_ui.as_ref() { runtime.apply_live_state(&mut self.input_state, &mut positions); } - self.reset_runtime_side_focus_if_pane_changed(previous_pane); - self.restore_toolbar_offsets(positions.top, positions.side); + self.restore_toolbar_offsets(positions.top); self.toolbar.mark_dirty(); self.input_state.dirty_tracker.mark_full(); self.input_state.needs_redraw = true; diff --git a/src/backend/wayland/state.rs b/src/backend/wayland/state.rs index 79a60fee..6c779b8a 100644 --- a/src/backend/wayland/state.rs +++ b/src/backend/wayland/state.rs @@ -85,8 +85,8 @@ use super::{ toolbar::{ ToolbarSurfaceManager, hit::{drag_intent_for_hit, intent_for_hit, quick_color_slot_for_hit}, - layout::{side_size, top_size}, - render::{render_side_palette, render_top_strip}, + layout::top_size, + render::render_top_strip, }, toolbar_intent::intent_to_event, zoom::ZoomState, @@ -130,10 +130,9 @@ pub(in crate::backend::wayland) use self::perf::{ PerfRenderProfileKind, PerfRenderSkipReason, damage_covers_logical_surface, }; pub(super) use helpers::{ - color_log, damage_summary, debug_damage_logging_enabled, debug_toolbar_color_logging_enabled, - debug_toolbar_drag_logging_enabled, drag_log, force_inline_toolbars_requested, - scale_damage_regions, surface_id, toolbar_drag_preview_enabled, toolbar_drag_throttle_interval, - toolbar_pointer_lock_enabled, + damage_summary, debug_damage_logging_enabled, debug_toolbar_drag_logging_enabled, drag_log, + force_inline_toolbars_requested, scale_damage_regions, surface_id, + toolbar_drag_preview_enabled, toolbar_drag_throttle_interval, toolbar_pointer_lock_enabled, }; pub(in crate::backend::wayland) struct WaylandGlobals { @@ -484,14 +483,9 @@ impl WaylandState { const TOP_MARGIN_RIGHT: f64 = 12.0; const TOP_BASE_MARGIN_TOP: f64 = 12.0; const TOP_MARGIN_BOTTOM: f64 = 0.0; - const SIDE_BASE_MARGIN_TOP: f64 = 24.0; - const SIDE_MARGIN_BOTTOM: f64 = 24.0; - const SIDE_BASE_MARGIN_LEFT: f64 = 24.0; - const SIDE_MARGIN_RIGHT: f64 = 0.0; const INLINE_TOP_Y: f64 = Self::TOP_BASE_MARGIN_TOP; - const INLINE_SIDE_X: f64 = 24.0; + const INLINE_TOP_X: f64 = 24.0; const TOOLBAR_CONFIGURE_FAIL_THRESHOLD: u32 = 180; - const INLINE_TOP_PUSH: f64 = 16.0; const ZOOM_STEP_KEY: f64 = 1.2; const ZOOM_STEP_SCROLL: f64 = 1.1; pub(super) const ZOOM_PAN_STEP: f64 = 32.0; diff --git a/src/backend/wayland/state/boards.rs b/src/backend/wayland/state/boards.rs index 47fd9814..581525c8 100644 --- a/src/backend/wayland/state/boards.rs +++ b/src/backend/wayland/state/boards.rs @@ -135,7 +135,7 @@ impl WaylandState { && !self.input_state.is_properties_panel_open() && !self.input_state.is_radial_menu_open() && !self.pointer_over_toolbar() - && self.toolbar_focus_target().is_none() + && !self.toolbar_focus_active() && matches!(self.input_state.state, DrawingState::Idle) } } diff --git a/src/backend/wayland/state/core/init.rs b/src/backend/wayland/state/core/init.rs index 7dd07fab..05cc8cc0 100644 --- a/src/backend/wayland/state/core/init.rs +++ b/src/backend/wayland/state/core/init.rs @@ -84,25 +84,16 @@ impl WaylandState { // now-disconnected monitor degrades instead of being lost. let mut positions = crate::backend::wayland::runtime_ui_state::ToolbarPositionSnapshot { top: (config.ui.toolbar.top_offset, config.ui.toolbar.top_offset_y), - side: ( - config.ui.toolbar.side_offset_x, - config.ui.toolbar.side_offset, - ), }; if let Some(runtime_ui) = runtime_ui.as_ref() { runtime_ui.apply_startup_positions(&mut positions); } data.toolbar_top_offset = positions.top.0; data.toolbar_top_offset_y = positions.top.1; - data.toolbar_side_offset = positions.side.1; - data.toolbar_side_offset_x = positions.side.0; drag_log(|| { format!( - "load offsets from config seeds and runtime overrides: top_offset=({}, {}), side_offset=({}, {})", - data.toolbar_top_offset, - data.toolbar_top_offset_y, - data.toolbar_side_offset, - data.toolbar_side_offset_x + "load offsets from config seeds and runtime overrides: top_offset=({}, {})", + data.toolbar_top_offset, data.toolbar_top_offset_y ) }); let zoom_manager = screencopy_manager.clone(); diff --git a/src/backend/wayland/state/data.rs b/src/backend/wayland/state/data.rs index 261bbba1..f37c62ea 100644 --- a/src/backend/wayland/state/data.rs +++ b/src/backend/wayland/state/data.rs @@ -1,13 +1,12 @@ use std::time::Instant; -use crate::backend::wayland::toolbar::{ToolbarFocusTarget, hit::HitRegion}; +use crate::backend::wayland::toolbar::hit::HitRegion; use super::capture::OverlayCaptureBarrier; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MoveDragKind { Top, - Side, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -97,29 +96,20 @@ pub struct StateData { pub(super) toolbar_layer_shell_missing_logged: bool, pub(super) inline_toolbars: bool, pub(super) inline_top_hits: Vec, - pub(super) inline_side_hits: Vec, pub(super) inline_top_rect: Option<(f64, f64, f64, f64)>, - pub(super) inline_side_rect: Option<(f64, f64, f64, f64)>, pub(super) inline_top_hover: Option<(f64, f64)>, - pub(super) inline_side_hover: Option<(f64, f64)>, pub(super) inline_top_hover_start: Option, - pub(super) inline_side_hover_start: Option, pub(super) inline_top_tooltip_pending: bool, - pub(super) inline_side_tooltip_pending: bool, pub(super) inline_top_focus_index: Option, - pub(super) inline_side_focus_index: Option, pub(super) inline_top_focus_id: Option, - pub(super) inline_side_focus_id: Option, - pub(super) toolbar_focus_target: Option, + /// True while keyboard focus is routed into the top toolbar. + pub(super) toolbar_focus_active: bool, pub(super) toolbar_top_offset: f64, pub(super) toolbar_top_offset_y: f64, - pub(super) toolbar_side_offset: f64, - pub(super) toolbar_side_offset_x: f64, pub(super) toolbar_configure_miss_count: u32, /// Highest GTK drag sequence numbers drained per bar; echoed in /// updates so the GTK side can discard stale offset mirrors. pub(super) gtk_top_offset_seq: u64, - pub(super) gtk_side_offset_seq: u64, /// GTK surface currently parked at its drag origin while the main overlay /// renders the moving toolbar preview. pub(super) gtk_drag_preview: Option, @@ -127,18 +117,14 @@ pub struct StateData { /// start-relative GTK drag. They discard fenced motion if the exact same /// preview later resumes. pub(super) gtk_top_drag_rebase: Option<(f64, f64)>, - pub(super) gtk_side_drag_rebase: Option<(f64, f64)>, /// A GTK drag that emitted feedback while a modal was engaged stays /// blocked until its matching drag-end feedback arrives. pub(super) gtk_top_drag_blocked: bool, - pub(super) gtk_side_drag_blocked: bool, /// Pointer is over the GTK top toolbar window (reported via feedback; /// GTK runs on its own connection). Restores the top-strip idle fade. pub(super) gtk_top_hover: bool, pub(super) last_applied_top_margin: Option, - pub(super) last_applied_side_margin: Option, pub(super) last_applied_top_margin_top: Option, - pub(super) last_applied_side_margin_left: Option, pub(super) toolbar_move_drag: Option, pub(super) active_drag_kind: Option, pub(super) drag_top_base_x: Option, @@ -203,9 +189,6 @@ pub struct StateData { /// per-session cap). Session-only; the across-session cap and learned /// suppression live in the persisted onboarding state. pub(super) shortcut_coach: super::onboarding::ShortcutCoachSession, - /// Once-per-session guard for the legacy side-panel deprecation notice - /// (M9). - pub(super) panel_deprecation_notice_shown: bool, } impl StateData { @@ -228,37 +211,23 @@ impl StateData { toolbar_layer_shell_missing_logged: false, inline_toolbars: false, inline_top_hits: Vec::new(), - inline_side_hits: Vec::new(), inline_top_rect: None, - inline_side_rect: None, inline_top_hover: None, - inline_side_hover: None, inline_top_hover_start: None, - inline_side_hover_start: None, inline_top_tooltip_pending: false, - inline_side_tooltip_pending: false, inline_top_focus_index: None, - inline_side_focus_index: None, inline_top_focus_id: None, - inline_side_focus_id: None, - toolbar_focus_target: None, + toolbar_focus_active: false, toolbar_top_offset: 0.0, toolbar_top_offset_y: 0.0, - toolbar_side_offset: 0.0, - toolbar_side_offset_x: 0.0, toolbar_configure_miss_count: 0, gtk_top_offset_seq: 0, - gtk_side_offset_seq: 0, gtk_drag_preview: None, gtk_top_drag_rebase: None, - gtk_side_drag_rebase: None, gtk_top_drag_blocked: false, gtk_top_hover: false, - gtk_side_drag_blocked: false, last_applied_top_margin: None, - last_applied_side_margin: None, last_applied_top_margin_top: None, - last_applied_side_margin_left: None, toolbar_move_drag: None, active_drag_kind: None, drag_top_base_x: None, @@ -302,7 +271,6 @@ impl StateData { prev_tool_preview_damage: None, top_strip_fade: crate::ui::toolbar::snapshot::fade::TopStripFade::new(), shortcut_coach: super::onboarding::ShortcutCoachSession::default(), - panel_deprecation_notice_shown: false, } } } diff --git a/src/backend/wayland/state/gtk_toolbar.rs b/src/backend/wayland/state/gtk_toolbar.rs index aef67b33..87a3256c 100644 --- a/src/backend/wayland/state/gtk_toolbar.rs +++ b/src/backend/wayland/state/gtk_toolbar.rs @@ -14,18 +14,11 @@ fn gtk_toolbar_feedback_blocked(input_state: &crate::input::InputState) -> bool input_state.command_palette_is_engaged() } -fn acknowledge_blocked_gtk_drag_feedback( - top_seq: &mut u64, - side_seq: &mut u64, - feedback: &GtkToolbarFeedback, -) { +fn acknowledge_blocked_gtk_drag_feedback(top_seq: &mut u64, feedback: &GtkToolbarFeedback) { match feedback { GtkToolbarFeedback::SetTopOffset { seq, .. } => { *top_seq = (*top_seq).max(*seq); } - GtkToolbarFeedback::SetSideOffset { seq, .. } => { - *side_seq = (*side_seq).max(*seq); - } GtkToolbarFeedback::Event { .. } | GtkToolbarFeedback::TopHover { .. } | GtkToolbarFeedback::CaptureSuppressionReady { .. } @@ -36,7 +29,6 @@ fn acknowledge_blocked_gtk_drag_feedback( fn gtk_toolbar_feedback_is_blocked( modal_engaged: bool, top_drag_blocked: &mut bool, - side_drag_blocked: &mut bool, feedback: &GtkToolbarFeedback, ) -> bool { match feedback { @@ -52,13 +44,6 @@ fn gtk_toolbar_feedback_is_blocked( } blocked } - GtkToolbarFeedback::SetSideOffset { phase, .. } => { - let blocked = modal_engaged || *side_drag_blocked; - if blocked { - *side_drag_blocked = !phase.is_end(); - } - blocked - } } } @@ -124,14 +109,9 @@ impl WaylandState { if gtk_toolbar_feedback_is_blocked( gtk_toolbar_feedback_blocked(&self.input_state), &mut self.data.gtk_top_drag_blocked, - &mut self.data.gtk_side_drag_blocked, &feedback, ) { - acknowledge_blocked_gtk_drag_feedback( - &mut self.data.gtk_top_offset_seq, - &mut self.data.gtk_side_offset_seq, - &feedback, - ); + acknowledge_blocked_gtk_drag_feedback(&mut self.data.gtk_top_offset_seq, &feedback); // If a modal opened after an accepted drag start, the blocked // end still has to close the preview lifecycle. Keep the last // accepted position rather than applying motion produced while @@ -153,22 +133,6 @@ impl WaylandState { phase, ); } - GtkToolbarFeedback::SetSideOffset { - surface_size, - phase, - .. - } if phase.is_end() - && self.data.gtk_drag_preview - == Some(crate::toolbar_gtk::GtkToolbarKind::Side) => - { - self.data.gtk_side_drag_rebase = None; - self.apply_gtk_side_offset( - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset, - surface_size, - phase, - ); - } _ => {} } continue; @@ -210,22 +174,6 @@ impl WaylandState { self.data.gtk_top_offset_seq = seq; self.apply_gtk_top_offset(x, y, surface_size, phase); } - GtkToolbarFeedback::SetSideOffset { - x, - y, - surface_size, - seq, - phase, - } => { - super::drag_log(|| { - format!( - "gtk side receive seq={seq} phase={phase:?} offset=({x:.3},{y:.3}) surface={}x{}", - surface_size.width, surface_size.height, - ) - }); - self.data.gtk_side_offset_seq = seq; - self.apply_gtk_side_offset(x, y, surface_size, phase); - } } } // Feedback committed before the terminal transition remains accepted @@ -253,15 +201,9 @@ impl WaylandState { let unmap_suppressed = self.overlay_passthrough_requested() && !capture_suppressed; let update = GtkToolbarUpdate { top_visible: self.input_state.toolbar_top_visible() && !unmap_suppressed, - side_visible: self.input_state.toolbar_side_visible() && !unmap_suppressed, top_offset: (self.data.toolbar_top_offset, self.data.toolbar_top_offset_y), - side_offset: ( - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset, - ), top_offset_seq: self.data.gtk_top_offset_seq, - side_offset_seq: self.data.gtk_side_offset_seq, - top_base_x: self.gtk_top_base_x(&snapshot), + top_base_x: self.gtk_top_base_x(), output_name: self .surface .current_output() @@ -284,10 +226,9 @@ impl WaylandState { }; if let Some(generation) = update.capture_suppression_generation { log::info!( - "capture.preflight id={generation} component=backend phase=gtk-update-queued reason={:?} top_visible={} side_visible={} output={:?}", + "capture.preflight id={generation} component=backend phase=gtk-update-queued reason={:?} top_visible={} output={:?}", self.data.overlay_suppression, update.top_visible, - update.side_visible, update.output_name ); } @@ -324,13 +265,11 @@ mod modal_tests { } #[test] - fn blocked_drag_feedback_advances_only_the_originating_sequence() { + fn blocked_drag_feedback_advances_the_sequence_and_never_regresses_it() { let mut top_seq = 4; - let mut side_seq = 7; acknowledge_blocked_gtk_drag_feedback( &mut top_seq, - &mut side_seq, &GtkToolbarFeedback::SetTopOffset { x: 100.0, y: 50.0, @@ -339,30 +278,10 @@ mod modal_tests { phase: GtkToolbarDragPhase::End, }, ); - assert_eq!((top_seq, side_seq), (9, 7)); + assert_eq!(top_seq, 9); acknowledge_blocked_gtk_drag_feedback( &mut top_seq, - &mut side_seq, - &GtkToolbarFeedback::SetSideOffset { - x: 25.0, - y: 75.0, - surface_size: TEST_SURFACE_SIZE, - seq: 11, - phase: GtkToolbarDragPhase::Move, - }, - ); - assert_eq!((top_seq, side_seq), (9, 11)); - } - - #[test] - fn blocked_drag_feedback_never_regresses_a_sequence() { - let mut top_seq = 9; - let mut side_seq = 11; - - acknowledge_blocked_gtk_drag_feedback( - &mut top_seq, - &mut side_seq, &GtkToolbarFeedback::SetTopOffset { x: 0.0, y: 0.0, @@ -371,13 +290,12 @@ mod modal_tests { phase: GtkToolbarDragPhase::Move, }, ); - assert_eq!((top_seq, side_seq), (9, 11)); + assert_eq!(top_seq, 9); } #[test] fn drag_started_under_modal_stays_blocked_until_done() { let mut top_blocked = false; - let mut side_blocked = false; let top_update = |phase| GtkToolbarFeedback::SetTopOffset { x: 10.0, y: 20.0, @@ -389,7 +307,6 @@ mod modal_tests { assert!(gtk_toolbar_feedback_is_blocked( true, &mut top_blocked, - &mut side_blocked, &top_update(GtkToolbarDragPhase::Start), )); assert!(top_blocked); @@ -397,7 +314,6 @@ mod modal_tests { assert!(gtk_toolbar_feedback_is_blocked( false, &mut top_blocked, - &mut side_blocked, &top_update(GtkToolbarDragPhase::Move), )); assert!(top_blocked); @@ -405,7 +321,6 @@ mod modal_tests { assert!(gtk_toolbar_feedback_is_blocked( false, &mut top_blocked, - &mut side_blocked, &top_update(GtkToolbarDragPhase::End), )); assert!(!top_blocked); @@ -413,56 +328,19 @@ mod modal_tests { assert!(!gtk_toolbar_feedback_is_blocked( false, &mut top_blocked, - &mut side_blocked, &top_update(GtkToolbarDragPhase::Start), )); } - #[test] - fn blocked_drag_latches_are_independent_per_bar() { - let mut top_blocked = false; - let mut side_blocked = false; - let top = GtkToolbarFeedback::SetTopOffset { - x: 0.0, - y: 0.0, - surface_size: TEST_SURFACE_SIZE, - seq: 1, - phase: GtkToolbarDragPhase::Start, - }; - let side = GtkToolbarFeedback::SetSideOffset { - x: 0.0, - y: 0.0, - surface_size: TEST_SURFACE_SIZE, - seq: 1, - phase: GtkToolbarDragPhase::Start, - }; - - assert!(gtk_toolbar_feedback_is_blocked( - true, - &mut top_blocked, - &mut side_blocked, - &top, - )); - assert!(!gtk_toolbar_feedback_is_blocked( - false, - &mut top_blocked, - &mut side_blocked, - &side, - )); - assert_eq!((top_blocked, side_blocked), (true, false)); - } - #[test] fn capture_suppression_ack_bypasses_modal_feedback_blocking() { let mut top_blocked = true; - let mut side_blocked = false; assert!(!gtk_toolbar_feedback_is_blocked( true, &mut top_blocked, - &mut side_blocked, &GtkToolbarFeedback::CaptureSuppressionReady { generation: 7 }, )); - assert_eq!((top_blocked, side_blocked), (true, false)); + assert!(top_blocked); } } diff --git a/src/backend/wayland/state/helpers.rs b/src/backend/wayland/state/helpers.rs index 9cbf6f2d..09492018 100644 --- a/src/backend/wayland/state/helpers.rs +++ b/src/backend/wayland/state/helpers.rs @@ -3,7 +3,7 @@ use std::{sync::OnceLock, time::Duration}; use wayland_client::{Proxy, protocol::wl_surface}; use crate::env_vars::{ - DEBUG_DAMAGE_ENV, DEBUG_TOOLBAR_COLOR_ENV, DEBUG_TOOLBAR_DRAG_ENV, FORCE_INLINE_TOOLBARS_ENV, + DEBUG_DAMAGE_ENV, DEBUG_TOOLBAR_DRAG_ENV, FORCE_INLINE_TOOLBARS_ENV, TOOLBAR_DRAG_HANDOFF_MS_ENV, TOOLBAR_DRAG_PREVIEW_ENV, TOOLBAR_DRAG_THROTTLE_MS_ENV, TOOLBAR_POINTER_LOCK_ENV, }; @@ -72,13 +72,6 @@ pub(in crate::backend::wayland) fn debug_toolbar_drag_logging_enabled() -> bool }) } -pub(in crate::backend::wayland) fn debug_toolbar_color_logging_enabled() -> bool { - static ENABLED: OnceLock = OnceLock::new(); - *ENABLED.get_or_init(|| { - parse_boolish_env(&std::env::var(DEBUG_TOOLBAR_COLOR_ENV).unwrap_or_default()) - }) -} - pub(in crate::backend::wayland) fn toolbar_pointer_lock_enabled() -> bool { static ENABLED: OnceLock = OnceLock::new(); *ENABLED.get_or_init(|| { @@ -133,13 +126,6 @@ pub(in crate::backend::wayland) fn drag_log>(message: impl FnOnce( } } -/// See [`drag_log`]; same lazy contract for color diagnostics. -pub(in crate::backend::wayland) fn color_log>(message: impl FnOnce() -> M) { - if debug_toolbar_color_logging_enabled() { - log::info!("{}", message().as_ref()); - } -} - fn force_inline_env_enabled() -> bool { static ENABLED: OnceLock = OnceLock::new(); *ENABLED.get_or_init(|| { diff --git a/src/backend/wayland/state/onboarding.rs b/src/backend/wayland/state/onboarding.rs index 76934ff8..7e245c38 100644 --- a/src/backend/wayland/state/onboarding.rs +++ b/src/backend/wayland/state/onboarding.rs @@ -15,10 +15,6 @@ const SHORTCUT_COACH_COOLDOWN: Duration = Duration::from_secs(90); /// Maximum coach hints shown per session. const SHORTCUT_COACH_SESSION_CAP: u32 = 2; -const PANEL_DEPRECATION_MESSAGE: &str = "Side panel deprecated \u{2014} use the new homes:\n\ - Drawing \u{2192} style pill \u{b7} Canvas \u{2192} \u{201c}Canvas\u{2026}\u{201d} / zoom chip / status bar\n\ - Presets \u{2192} top strip \u{b7} Session/Settings \u{2192} overflow"; - /// The visible status-HUD entry that can open the board picker. The hint must /// not advertise the pill when both configurable Board/Page segments are /// absent: the remaining color/tool/help segments perform different actions. @@ -114,7 +110,6 @@ impl WaylandState { // Show capability warning toast first if applicable. self.apply_capability_toast(); // Honest legacy notice: nudge panel-mode users toward the new homes. - self.apply_panel_deprecation_notice(); // Capture the coach's slow-path signal before apply_first_run_progress // drains pending_onboarding_usage. let coach_slow_path = self @@ -432,52 +427,6 @@ impl WaylandState { ); } } - - /// Left-panel deprecation, made honest: when the user is running the - /// deprecated legacy side palette (`side_layout = "panel"`), show a - /// once-per-session notice pointing at the concrete new homes for each - /// retired pane. Reuses the capability-toast once-per-session pattern: the - /// session-only `*_shown` flag prevents repeats within one launch. No config - /// key gates it: the notice only appears at all under the deprecated layout - /// the user opted into. - fn apply_panel_deprecation_notice(&mut self) { - // Only the deprecated legacy side palette; the pill default already - // re-homed everything, so there is nothing to explain there. - if self.input_state.toolbar_side_layout != crate::config::ToolbarSideLayout::Panel { - return; - } - if self.data.panel_deprecation_notice_shown { - return; // once per session - } - if !self.surface.is_configured() || self.overlay_suppressed() { - return; - } - if self.input_state.presenter_mode - || self.input_state.show_help - || self.input_state.command_palette_open - || self.input_state.tour_active - { - return; - } - // Don't nag during first-run onboarding; wait until it is finished or - // skipped. - if self.onboarding.state().first_run_active() { - return; - } - // Never clobber real feedback: only when the toast queue is idle. - if !self.input_state.toasts_idle() { - return; - } - - let outcome = self.input_state.push_toast( - ToastPriority::Info, - "onboarding.panel_deprecation", - Toast::info(PANEL_DEPRECATION_MESSAGE).once_per_content(), - ); - if outcome.accepted() { - self.data.panel_deprecation_notice_shown = true; - } - } } /// The capability warning to raise, if any: only when this exact capability diff --git a/src/backend/wayland/state/onboarding/tests.rs b/src/backend/wayland/state/onboarding/tests.rs index 47e4da4c..c14e4390 100644 --- a/src/backend/wayland/state/onboarding/tests.rs +++ b/src/backend/wayland/state/onboarding/tests.rs @@ -4,8 +4,8 @@ use super::first_run::{ first_run_step_eyebrow, quick_access_completed, radial_flick_completed, shortcut_rebind_footer, }; use super::{ - PANEL_DEPRECATION_MESSAGE, canvas_popover_hint_relevant, capability_toast_message, - shortcut_coach_should_fire, status_bar_board_picker_entry, + canvas_popover_hint_relevant, capability_toast_message, shortcut_coach_should_fire, + status_bar_board_picker_entry, }; use crate::config::{RadialMenuMouseBinding, ToolbarRebindModifier}; use crate::input::state::CompositorCapabilities; @@ -227,31 +227,6 @@ fn status_bar_hint_requires_a_visible_board_picker_segment() { assert_eq!(status_bar_board_picker_entry(&input), None); } -#[test] -fn panel_deprecation_notice_is_multiline_and_names_every_new_home() { - assert!(PANEL_DEPRECATION_MESSAGE.lines().count() >= 3); - for expected in [ - "style pill", - "Canvas\u{2026}", - "zoom chip", - "status bar", - "top strip", - "Session/Settings", - "overflow", - ] { - assert!( - PANEL_DEPRECATION_MESSAGE.contains(expected), - "missing {expected:?}: {PANEL_DEPRECATION_MESSAGE:?}" - ); - } - assert!( - PANEL_DEPRECATION_MESSAGE - .lines() - .all(|line| line.chars().count() <= 80), - "keep each unwrapped toast line within ordinary screen widths" - ); -} - #[test] fn shortcut_rebind_footer_uses_configured_modifier() { for (modifier, expected_chord) in [ diff --git a/src/backend/wayland/state/render/ui.rs b/src/backend/wayland/state/render/ui.rs index 277ff386..4448ab2b 100644 --- a/src/backend/wayland/state/render/ui.rs +++ b/src/backend/wayland/state/render/ui.rs @@ -214,7 +214,7 @@ impl WaylandState { let snapshot = self.toolbar_snapshot(); let (_, top_h) = crate::backend::wayland::toolbar::top_size(&snapshot); let anchor = ( - self.inline_top_base_x(&snapshot) + self.data.toolbar_top_offset, + self.inline_top_base_x() + self.data.toolbar_top_offset, self.inline_top_base_y() + self.data.toolbar_top_offset_y + top_h as f64 + 8.0, ); crate::ui::render_precision_entry_popup( diff --git a/src/backend/wayland/state/toolbar.rs b/src/backend/wayland/state/toolbar.rs index a83a76b7..ee355e36 100644 --- a/src/backend/wayland/state/toolbar.rs +++ b/src/backend/wayland/state/toolbar.rs @@ -11,5 +11,5 @@ mod geometry; pub(crate) use geometry::clamp_floating_axis_offset; mod gtk_feedback; mod inline; -mod side_pane; +mod scroll; mod visibility; diff --git a/src/backend/wayland/state/toolbar/drag/base.rs b/src/backend/wayland/state/toolbar/drag/base.rs index 23b19508..c91f9ecf 100644 --- a/src/backend/wayland/state/toolbar/drag/base.rs +++ b/src/backend/wayland/state/toolbar/drag/base.rs @@ -12,11 +12,9 @@ fn active_drag_top_base_x( impl WaylandState { /// Base X position for the top toolbar when laid out inline. - /// When a drag is in progress we freeze this base to avoid shifting the top bar while moving the side bar. - pub(in crate::backend::wayland::state) fn inline_top_base_x( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { + /// A drag freezes this base so the resting layout cannot shift underneath + /// the surface being moved. + pub(in crate::backend::wayland::state) fn inline_top_base_x(&self) -> f64 { if let Some(x) = active_drag_top_base_x( self.is_move_dragging(), self.data.gtk_drag_preview.is_some(), @@ -24,53 +22,11 @@ impl WaylandState { ) { return x; } - let allow_push = self.active_move_drag_kind() != Some(MoveDragKind::Side); - self.computed_inline_top_base_x(snapshot, allow_push) - } - - fn computed_inline_top_base_x(&self, snapshot: &ToolbarSnapshot, allow_push: bool) -> f64 { - // The GTK frontend keeps the built-in side surface unmapped, but its - // side palette occupies the same space and must push the top strip - // identically. - let side_visible = self.toolbar.is_side_visible() - || (self.gtk_toolbars_active() && self.input_state.toolbar_side_visible()); - let side_size = side_size(snapshot); - let top_size = top_size(snapshot); - let side_start_y = Self::SIDE_BASE_MARGIN_TOP + self.data.toolbar_side_offset; - let top_bottom_y = - self.inline_top_base_y() + self.data.toolbar_top_offset_y + top_size.1 as f64; - let base = Self::INLINE_SIDE_X; - let result = geometry::compute_inline_top_base_x( - base, - side_visible, - side_size.0 as f64, - side_start_y, - top_bottom_y, - Self::INLINE_TOP_PUSH, - allow_push, - ); - if self.is_move_dragging() - || self.toolbar_dragging() - || self.data.gtk_drag_preview.is_some() - { - drag_log(|| { - format!( - "inline_top_base_x: base={:.3}, side_visible={}, side_width={:.3}, side_start_y={:.3}, top_bottom_y={:.3}, allow_push={}, result={:.3}", - base, - side_visible, - side_size.0 as f64, - side_start_y, - top_bottom_y, - allow_push, - result - ) - }); - } - result + Self::INLINE_TOP_X } /// Preserve the top strip's screen X while switching from the base frozen - /// for a drag back to the resting overlap-derived base. + /// for a drag back to the resting base. /// /// Only the two explicit drag-commit paths call this /// (`finish_toolbar_move_drag` with `commit`, and `finish_gtk_offset_change`), @@ -82,8 +38,7 @@ impl WaylandState { let Some(old_base_x) = self.data.drag_top_base_x else { return; }; - let snapshot = self.toolbar_snapshot(); - let new_base_x = self.computed_inline_top_base_x(&snapshot, true); + let new_base_x = Self::INLINE_TOP_X; let delta = old_base_x - new_base_x; if delta.abs() <= 0.01 { return; @@ -115,15 +70,9 @@ impl WaylandState { ) -> (f64, f64) { match kind { MoveDragKind::Top => ( - self.inline_top_base_x(&self.toolbar_snapshot()) - + self.data.toolbar_top_offset - + local_coord.0, + self.inline_top_base_x() + self.data.toolbar_top_offset + local_coord.0, self.inline_top_base_y() + self.data.toolbar_top_offset_y + local_coord.1, ), - MoveDragKind::Side => ( - Self::SIDE_BASE_MARGIN_LEFT + self.data.toolbar_side_offset_x + local_coord.0, - Self::SIDE_BASE_MARGIN_TOP + self.data.toolbar_side_offset + local_coord.1, - ), } } } diff --git a/src/backend/wayland/state/toolbar/drag/clamp.rs b/src/backend/wayland/state/toolbar/drag/clamp.rs index 193b1c8a..394541d1 100644 --- a/src/backend/wayland/state/toolbar/drag/clamp.rs +++ b/src/backend/wayland/state/toolbar/drag/clamp.rs @@ -54,55 +54,35 @@ impl WaylandState { return false; } let (top_w, top_h) = top_size(snapshot); - let (side_w, side_h) = side_size(snapshot); - let top_base_x = self.inline_top_base_x(snapshot); + let top_base_x = self.inline_top_base_x(); let top_base_y = self.inline_top_base_y(); let before_top = (self.data.toolbar_top_offset, self.data.toolbar_top_offset_y); - let before_side = ( - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset, - ); let input = geometry::ToolbarClampInput { width, height, top_size: (top_w, top_h), - side_size: (side_w, side_h), top_base_x, top_base_y, top_margin_right: Self::TOP_MARGIN_RIGHT, top_margin_bottom: Self::TOP_MARGIN_BOTTOM, - side_base_margin_left: Self::SIDE_BASE_MARGIN_LEFT, - side_base_margin_top: Self::SIDE_BASE_MARGIN_TOP, - side_margin_right: Self::SIDE_MARGIN_RIGHT, - side_margin_bottom: Self::SIDE_MARGIN_BOTTOM, }; let offsets = geometry::ToolbarOffsets { top_x: self.data.toolbar_top_offset, top_y: self.data.toolbar_top_offset_y, - side_x: self.data.toolbar_side_offset_x, - side_y: self.data.toolbar_side_offset, }; let (clamped, bounds) = geometry::clamp_toolbar_offsets(offsets, input); self.data.toolbar_top_offset = clamped.top_x; self.data.toolbar_top_offset_y = clamped.top_y; - self.data.toolbar_side_offset_x = clamped.side_x; - self.data.toolbar_side_offset = clamped.side_y; drag_log(|| { format!( - "clamp offsets: before=({:.3}, {:.3})/({:.3}, {:.3}), after=({:.3}, {:.3})/({:.3}, {:.3}), max=({:.3}, {:.3})/({:.3}, {:.3}), size=({}, {}), top_base_x={:.3}, top_base_y={:.3}", + "clamp offsets: before=({:.3}, {:.3}), after=({:.3}, {:.3}), max=({:.3}, {:.3}), size=({}, {}), top_base_x={:.3}, top_base_y={:.3}", before_top.0, before_top.1, - before_side.0, - before_side.1, self.data.toolbar_top_offset, self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset, bounds.max_top_x, bounds.max_top_y, - bounds.max_side_x, - bounds.max_side_y, width, height, top_base_x, @@ -115,7 +95,7 @@ impl WaylandState { pub(in crate::backend::wayland::state::toolbar) fn apply_toolbar_offsets( &mut self, snapshot: &ToolbarSnapshot, - ) -> (bool, bool) { + ) -> bool { if self.surface.width() == 0 || self.surface.height() == 0 { drag_log(|| { format!( @@ -124,7 +104,7 @@ impl WaylandState { self.surface.height() ) }); - return (false, false); + return false; } let _ = self.clamp_toolbar_offsets(snapshot); // During local-coordinate preview drags, keep the layer-shell surface parked @@ -133,83 +113,55 @@ impl WaylandState { // the preview and avoid a visible catch-up animation on release. if self.toolbar_drag_preview_active() && !self.pointer_lock_active() { drag_log(|| "skip apply_toolbar_offsets: drag preview active without pointer lock"); - return (false, false); + return false; } if self.layer_shell.is_none() { - return (false, false); + return false; } - let top_base_x = self.inline_top_base_x(snapshot); - let (top_margin_left, top_margin_top, side_margin_top, side_margin_left) = - geometry::compute_layer_margins( - top_base_x, - Self::TOP_BASE_MARGIN_TOP, - Self::SIDE_BASE_MARGIN_LEFT, - Self::SIDE_BASE_MARGIN_TOP, - geometry::ToolbarOffsets { - top_x: self.data.toolbar_top_offset, - top_y: self.data.toolbar_top_offset_y, - side_x: self.data.toolbar_side_offset_x, - side_y: self.data.toolbar_side_offset, - }, - ); + let top_base_x = self.inline_top_base_x(); + let (top_margin_left, top_margin_top) = geometry::compute_layer_margins( + top_base_x, + Self::TOP_BASE_MARGIN_TOP, + geometry::ToolbarOffsets { + top_x: self.data.toolbar_top_offset, + top_y: self.data.toolbar_top_offset_y, + }, + ); drag_log(|| { format!( - "apply_toolbar_offsets: top_margin_left={}, top_margin_top={}, side_margin_top={}, side_margin_left={}, offsets=({}, {})/({}, {}), scale={}, top_base_x={}", + "apply_toolbar_offsets: top_margin_left={}, top_margin_top={}, offsets=({}, {}), scale={}, top_base_x={}", top_margin_left, top_margin_top, - side_margin_top, - side_margin_left, self.data.toolbar_top_offset, self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset, self.surface.scale(), top_base_x ) }); if debug_toolbar_drag_logging_enabled() { debug!( - "apply_toolbar_offsets: top_margin_left={} (last={:?}), top_margin_top={} (last={:?}), side_margin_top={} (last={:?}), side_margin_left={} (last={:?}), offsets=({}, {})/({}, {}), top_base_x={}", + "apply_toolbar_offsets: top_margin_left={} (last={:?}), top_margin_top={} (last={:?}), offsets=({}, {}), top_base_x={}", top_margin_left, self.data.last_applied_top_margin, top_margin_top, self.data.last_applied_top_margin_top, - side_margin_top, - self.data.last_applied_side_margin, - side_margin_left, - self.data.last_applied_side_margin_left, self.data.toolbar_top_offset, self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset, top_base_x ); } let top_changed = self.data.last_applied_top_margin != Some(top_margin_left) || self.data.last_applied_top_margin_top != Some(top_margin_top); - let side_changed = self.data.last_applied_side_margin != Some(side_margin_top) - || self.data.last_applied_side_margin_left != Some(side_margin_left); - if !top_changed && !side_changed { - return (false, false); + if !top_changed { + return false; } self.data.last_applied_top_margin = Some(top_margin_left); - self.data.last_applied_side_margin = Some(side_margin_top); self.data.last_applied_top_margin_top = Some(top_margin_top); - self.data.last_applied_side_margin_left = Some(side_margin_left); - if top_changed { - self.toolbar - .set_top_margins(top_margin_top, top_margin_left); - } - if side_changed { - self.toolbar - .set_side_margins(side_margin_top, side_margin_left); - } - if (top_changed || side_changed) - && self.toolbar_drag_preview_active() - && self.pointer_lock_active() - { + self.toolbar + .set_top_margins(top_margin_top, top_margin_left); + if self.toolbar_drag_preview_active() && self.pointer_lock_active() { self.request_toolbar_drag_flush(); } - (top_changed, side_changed) + top_changed } } diff --git a/src/backend/wayland/state/toolbar/drag/handoff.rs b/src/backend/wayland/state/toolbar/drag/handoff.rs index 364cdeca..33093209 100644 --- a/src/backend/wayland/state/toolbar/drag/handoff.rs +++ b/src/backend/wayland/state/toolbar/drag/handoff.rs @@ -6,24 +6,18 @@ fn reset_gtk_drag_lifecycle( handoff_at: &mut Option, frozen_top_base_x: &mut Option, top_rebase: &mut Option<(f64, f64)>, - side_rebase: &mut Option<(f64, f64)>, top_blocked: &mut bool, - side_blocked: &mut bool, ) -> bool { let had_state = preview.is_some() || handoff_at.is_some() || frozen_top_base_x.is_some() || top_rebase.is_some() - || side_rebase.is_some() - || *top_blocked - || *side_blocked; + || *top_blocked; *preview = None; *handoff_at = None; *frozen_top_base_x = None; *top_rebase = None; - *side_rebase = None; *top_blocked = false; - *side_blocked = false; had_state } @@ -81,8 +75,7 @@ impl WaylandState { &mut self, kind: crate::toolbar_gtk::GtkToolbarKind, ) { - let snapshot = self.toolbar_snapshot(); - let frozen_top_base_x = self.inline_top_base_x(&snapshot); + let frozen_top_base_x = self.inline_top_base_x(); drag_log(|| { format!( "begin GTK {:?} drag preview (park transparent input surface, freeze top base at {frozen_top_base_x:.3})", @@ -115,9 +108,7 @@ impl WaylandState { &mut self.data.toolbar_drag_handoff_at, &mut self.data.drag_top_base_x, &mut self.data.gtk_top_drag_rebase, - &mut self.data.gtk_side_drag_rebase, &mut self.data.gtk_top_drag_blocked, - &mut self.data.gtk_side_drag_blocked, ); if had_state { drag_log(|| "cancel GTK drag lifecycle (restore built-in toolbar rendering)"); @@ -169,25 +160,19 @@ mod tests { let mut handoff_at = Some(Instant::now()); let mut frozen_top_base_x = Some(42.0); let mut top_rebase = Some((1.0, 2.0)); - let mut side_rebase = Some((3.0, 4.0)); let mut top_blocked = true; - let mut side_blocked = true; assert!(reset_gtk_drag_lifecycle( &mut preview, &mut handoff_at, &mut frozen_top_base_x, &mut top_rebase, - &mut side_rebase, &mut top_blocked, - &mut side_blocked, )); assert_eq!(preview, None); assert_eq!(handoff_at, None); assert_eq!(frozen_top_base_x, None); assert_eq!(top_rebase, None); - assert_eq!(side_rebase, None); assert!(!top_blocked); - assert!(!side_blocked); } } diff --git a/src/backend/wayland/state/toolbar/drag/move_drag.rs b/src/backend/wayland/state/toolbar/drag/move_drag.rs index 86346f72..39f06d66 100644 --- a/src/backend/wayland/state/toolbar/drag/move_drag.rs +++ b/src/backend/wayland/state/toolbar/drag/move_drag.rs @@ -48,24 +48,18 @@ impl WaylandState { }); self.data.toolbar_drag_pending_apply = false; self.data.last_toolbar_drag_apply = None; - // Freeze base positions so the other toolbar doesn't push while dragging. - let snapshot = self.toolbar_snapshot(); - let top_base_x = self.inline_top_base_x(&snapshot); + // Freeze the base position so a relayout cannot shift the surface + // under the pointer mid-drag. + let top_base_x = self.inline_top_base_x(); let top_base_y = self.inline_top_base_y(); - let side_base_x = Self::SIDE_BASE_MARGIN_LEFT + self.data.toolbar_side_offset_x; - let side_base_y = Self::SIDE_BASE_MARGIN_TOP + self.data.toolbar_side_offset; drag_log(|| { format!( - "begin move drag snapshot: kind={:?}, top_base=({:.3}, {:.3}), side_base=({:.3}, {:.3}), offsets=({}, {})/({}, {}), size=({}, {}), scale={}", + "begin move drag snapshot: kind={:?}, top_base=({:.3}, {:.3}), offsets=({}, {}), size=({}, {}), scale={}", kind, top_base_x, top_base_y, - side_base_x, - side_base_y, self.data.toolbar_top_offset, self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset, self.surface.width(), self.surface.height(), self.surface.scale() @@ -113,14 +107,12 @@ impl WaylandState { } drag_log(|| { format!( - "handle_toolbar_move_local: kind={:?}, local_coord=({:.3}, {:.3}), offsets=({}, {})/({}, {})", + "handle_toolbar_move_local: kind={:?}, local_coord=({:.3}, {:.3}), offsets=({}, {})", kind, local_coord.0, local_coord.1, self.data.toolbar_top_offset, - self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset + self.data.toolbar_top_offset_y ) }); // For layer-shell surfaces, use local coordinates directly since they're @@ -162,10 +154,6 @@ impl WaylandState { self.data.toolbar_top_offset += delta.0; self.data.toolbar_top_offset_y += delta.1; } - MoveDragKind::Side => { - self.data.toolbar_side_offset_x += delta.0; - self.data.toolbar_side_offset += delta.1; - } } self.data.toolbar_move_drag = Some(MoveDrag { @@ -212,7 +200,7 @@ impl WaylandState { ); drag_log(|| { format!( - "move_local delta: kind={:?}, local=({:.3}, {:.3}), effective=({:.3}, {:.3}), last_screen=({:.3}, {:.3}), delta=({:.3}, {:.3}), offsets_before=({}, {})/({}, {})", + "move_local delta: kind={:?}, local=({:.3}, {:.3}), effective=({:.3}, {:.3}), last_screen=({:.3}, {:.3}), delta=({:.3}, {:.3}), offsets_before=({}, {})", kind, local_coord.0, local_coord.1, @@ -223,13 +211,11 @@ impl WaylandState { delta.0, delta.1, self.data.toolbar_top_offset, - self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset + self.data.toolbar_top_offset_y ) }); log::debug!( - "handle_toolbar_move_local: kind={:?}, local_coord=({:.3}, {:.3}), effective_coord=({:.3}, {:.3}), last_coord=({:.3}, {:.3}), delta=({:.3}, {:.3}), offsets=({}, {})/({}, {})", + "handle_toolbar_move_local: kind={:?}, local_coord=({:.3}, {:.3}), effective_coord=({:.3}, {:.3}), last_coord=({:.3}, {:.3}), delta=({:.3}, {:.3}), offsets=({}, {})", kind, local_coord.0, local_coord.1, @@ -240,9 +226,7 @@ impl WaylandState { delta.0, delta.1, self.data.toolbar_top_offset, - self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset + self.data.toolbar_top_offset_y ); if delta.0 == 0.0 && delta.1 == 0.0 { self.data.toolbar_move_drag = Some(MoveDrag { @@ -258,27 +242,17 @@ impl WaylandState { self.data.toolbar_top_offset += delta.0; self.data.toolbar_top_offset_y += delta.1; } - MoveDragKind::Side => { - self.data.toolbar_side_offset_x += delta.0; - self.data.toolbar_side_offset += delta.1; - } } drag_log(|| { format!( - "move_local applied: kind={:?}, offsets_after=({}, {})/({}, {})", - kind, - self.data.toolbar_top_offset, - self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset + "move_local applied: kind={:?}, offsets_after=({}, {})", + kind, self.data.toolbar_top_offset, self.data.toolbar_top_offset_y ) }); log::debug!( - "After update offsets: top=({}, {}), side=({}, {})", + "After update offsets: top=({}, {})", self.data.toolbar_top_offset, - self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset + self.data.toolbar_top_offset_y ); self.data.toolbar_move_drag = Some(MoveDrag { @@ -328,14 +302,12 @@ impl WaylandState { } drag_log(|| { format!( - "handle_toolbar_move_screen: kind={:?}, screen_coord=({:.3}, {:.3}), offsets=({}, {})/({}, {})", + "handle_toolbar_move_screen: kind={:?}, screen_coord=({:.3}, {:.3}), offsets=({}, {})", kind, screen_coord.0, screen_coord.1, self.data.toolbar_top_offset, - self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset + self.data.toolbar_top_offset_y ) }); let snapshot = self @@ -364,7 +336,7 @@ impl WaylandState { ); drag_log(|| { format!( - "move_screen delta: kind={:?}, screen=({:.3}, {:.3}), last_screen=({:.3}, {:.3}), delta=({:.3}, {:.3}), offsets_before=({}, {})/({}, {})", + "move_screen delta: kind={:?}, screen=({:.3}, {:.3}), last_screen=({:.3}, {:.3}), delta=({:.3}, {:.3}), offsets_before=({}, {})", kind, screen_coord.0, screen_coord.1, @@ -373,13 +345,11 @@ impl WaylandState { delta.0, delta.1, self.data.toolbar_top_offset, - self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset + self.data.toolbar_top_offset_y ) }); log::debug!( - "handle_toolbar_move_screen: kind={:?}, screen_coord=({:.3}, {:.3}), last_screen_coord=({:.3}, {:.3}), delta=({:.3}, {:.3}), offsets=({}, {})/({}, {})", + "handle_toolbar_move_screen: kind={:?}, screen_coord=({:.3}, {:.3}), last_screen_coord=({:.3}, {:.3}), delta=({:.3}, {:.3}), offsets=({}, {})", kind, screen_coord.0, screen_coord.1, @@ -388,9 +358,7 @@ impl WaylandState { delta.0, delta.1, self.data.toolbar_top_offset, - self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset + self.data.toolbar_top_offset_y ); if delta.0 == 0.0 && delta.1 == 0.0 { self.data.toolbar_move_drag = Some(MoveDrag { @@ -405,19 +373,11 @@ impl WaylandState { self.data.toolbar_top_offset += delta.0; self.data.toolbar_top_offset_y += delta.1; } - MoveDragKind::Side => { - self.data.toolbar_side_offset_x += delta.0; - self.data.toolbar_side_offset += delta.1; - } } drag_log(|| { format!( - "move_screen applied: kind={:?}, offsets_after=({}, {})/({}, {})", - kind, - self.data.toolbar_top_offset, - self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset + "move_screen applied: kind={:?}, offsets_after=({}, {})", + kind, self.data.toolbar_top_offset, self.data.toolbar_top_offset_y ) }); diff --git a/src/backend/wayland/state/toolbar/drag/relative.rs b/src/backend/wayland/state/toolbar/drag/relative.rs index 6c30d05f..e4b25389 100644 --- a/src/backend/wayland/state/toolbar/drag/relative.rs +++ b/src/backend/wayland/state/toolbar/drag/relative.rs @@ -12,14 +12,12 @@ impl WaylandState { } drag_log(|| { format!( - "relative delta begin: kind={:?}, delta=({:.3}, {:.3}), offsets_before=({}, {})/({}, {})", + "relative delta begin: kind={:?}, delta=({:.3}, {:.3}), offsets_before=({}, {})", kind, delta.0, delta.1, self.data.toolbar_top_offset, - self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset + self.data.toolbar_top_offset_y ) }); let snapshot = self @@ -33,24 +31,18 @@ impl WaylandState { self.data.toolbar_top_offset += delta.0; self.data.toolbar_top_offset_y += delta.1; } - MoveDragKind::Side => { - self.data.toolbar_side_offset_x += delta.0; - self.data.toolbar_side_offset += delta.1; - } } self.apply_toolbar_offsets_throttled(&snapshot); drag_log(|| { format!( - "relative delta applied: kind={:?}, delta=({:.3}, {:.3}), offsets=({}, {})/({}, {})", + "relative delta applied: kind={:?}, delta=({:.3}, {:.3}), offsets=({}, {})", kind, delta.0, delta.1, self.data.toolbar_top_offset, - self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset + self.data.toolbar_top_offset_y ) }); @@ -75,11 +67,9 @@ impl WaylandState { } drag_log(|| { format!( - "end move drag: offsets=({}, {})/({}, {}), active_kind={:?}, pointer_locked={}", + "end move drag: offsets=({}, {}), active_kind={:?}, pointer_locked={}", self.data.toolbar_top_offset, self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset, self.data.active_drag_kind, self.pointer_lock_active() ) diff --git a/src/backend/wayland/state/toolbar/events.rs b/src/backend/wayland/state/toolbar/events.rs index 2293ef4a..48e8a25a 100644 --- a/src/backend/wayland/state/toolbar/events.rs +++ b/src/backend/wayland/state/toolbar/events.rs @@ -2,10 +2,9 @@ use super::*; use crate::{ backend::wayland::runtime_ui_state::ToolbarRuntimeFinish, input::InputState, - onboarding::OnboardingState, ui::toolbar::model::{ ToolbarBackendRoute, ToolbarEventPolicy, ToolbarPersistence, ToolbarPopover, - ToolbarPreApplyEffect, ToolbarRuntimeUiPersistenceTarget, popovers_for_event, + ToolbarRuntimeUiPersistenceTarget, popovers_for_event, }, }; use wayland_client::{Connection, QueueHandle}; @@ -21,16 +20,6 @@ pub(in crate::backend::wayland::state) use session::SessionFileDialogController; use feedback::{ToolbarPinChange, pin_durability}; use session::populate_session_snapshot; -fn record_drawer_hint_shown(state: &mut OnboardingState) -> bool { - if state.drawer_hint_count >= crate::onboarding::DRAWER_HINT_MAX { - return false; - } - - state.drawer_hint_count = state.drawer_hint_count.saturating_add(1); - state.drawer_hint_shown = state.drawer_hint_count >= crate::onboarding::DRAWER_HINT_MAX; - true -} - fn toolbar_event_blocked_by_modal(input_state: &InputState) -> bool { input_state.command_palette_is_engaged() } @@ -54,18 +43,13 @@ impl WaylandState { /// Returns a snapshot of the current input state for toolbar UI consumption. pub(in crate::backend::wayland) fn toolbar_snapshot(&self) -> ToolbarSnapshot { let hints = ToolbarBindingHints::from_input_state(&self.input_state); - let hint_max = crate::onboarding::DRAWER_HINT_MAX; - let show_drawer_hint = self.onboarding.state().drawer_hint_count < hint_max - && self.input_state.toolbar_side_pane == crate::ui::toolbar::SidePane::Draw; - let mut snapshot = - ToolbarSnapshot::from_input_with_options(&self.input_state, hints, show_drawer_hint); + let mut snapshot = ToolbarSnapshot::from_input_with_bindings(&self.input_state, hints); populate_session_snapshot(&mut snapshot, self.session.options()); snapshot.runtime_ui_persistence = self .runtime_ui .as_ref() .map(|runtime| runtime.persistence_snapshot()) .or_else(|| self.runtime_ui_unavailable.clone()); - snapshot.side_viewport_max = self.side_pane_viewport_max(&snapshot); snapshot.top_viewport_max = self.top_strip_viewport_max(&snapshot); snapshot.top_available_height = self.top_popover_available_height(&snapshot); snapshot.top_fade = self.data.top_strip_fade.value(); @@ -83,7 +67,7 @@ impl WaylandState { } else { 1.0 }; - let base_x = self.inline_top_base_x(snapshot); + let base_x = self.inline_top_base_x(); super::geometry::remaining_top_width(screen_width, base_x, Self::TOP_MARGIN_RIGHT, scale) } @@ -104,25 +88,6 @@ impl WaylandState { super::geometry::remaining_top_height(screen_height, surface_y, scale) } - /// Height available to the side palette in pre-scale spec units: the - /// screen space below the palette's top edge, floored so a pathological - /// drag offset cannot collapse the pane entirely. - fn side_pane_viewport_max(&self, snapshot: &ToolbarSnapshot) -> Option { - const MIN_SIDE_VIEWPORT: f64 = 180.0; - let screen_height = self.surface.height() as f64; - if screen_height <= 0.0 { - return None; - } - let scale = if snapshot.toolbar_scale.is_finite() { - snapshot.toolbar_scale.clamp(0.5, 3.0) - } else { - 1.0 - }; - let side_top = Self::SIDE_BASE_MARGIN_TOP + self.data.toolbar_side_offset; - let available = screen_height - side_top - Self::SIDE_MARGIN_BOTTOM; - Some((available / scale).max(MIN_SIDE_VIEWPORT)) - } - /// Applies an incoming toolbar event and schedules redraws as needed. pub(in crate::backend::wayland) fn handle_toolbar_event( &mut self, @@ -238,15 +203,6 @@ impl WaylandState { } let policy = ToolbarEventPolicy::for_event(&event); - for effect in &policy.pre_apply_effects { - match effect { - ToolbarPreApplyEffect::RecordDrawerHintShown => { - if record_drawer_hint_shown(self.onboarding.state_mut()) { - self.onboarding.save(); - } - } - } - } match (&policy.backend_route, &event) { (ToolbarBackendRoute::MoveTopToolbar, ToolbarEvent::MoveTopToolbar { x, y }) => { @@ -268,28 +224,7 @@ impl WaylandState { } return; } - (ToolbarBackendRoute::MoveSideToolbar, ToolbarEvent::MoveSideToolbar { x, y }) => { - let inline_active = self.inline_toolbars_active(); - let coord_is_screen = inline_active; - drag_log(|| { - format!( - "toolbar move event: kind=Side, coord=({:.3}, {:.3}), coord_is_screen={}, inline_active={}", - *x, *y, coord_is_screen, inline_active - ) - }); - if !self.begin_toolbar_move_drag(MoveDragKind::Side, (*x, *y), coord_is_screen) { - return; - } - if coord_is_screen { - self.handle_toolbar_move_screen(MoveDragKind::Side, (*x, *y)); - } else { - self.handle_toolbar_move(MoveDragKind::Side, (*x, *y)); - } - return; - } - (ToolbarBackendRoute::ApplyToInput, _) - | (ToolbarBackendRoute::MoveTopToolbar, _) - | (ToolbarBackendRoute::MoveSideToolbar, _) => {} + (ToolbarBackendRoute::ApplyToInput, _) | (ToolbarBackendRoute::MoveTopToolbar, _) => {} } #[cfg(feature = "tablet-input")] @@ -297,7 +232,6 @@ impl WaylandState { #[cfg(feature = "tablet-input")] let thickness_event = policy.tablet_thickness_sensitive; - let pane_switch = matches!(event, ToolbarEvent::SetSidePane(_)); let starts_item_drag = matches!(event, ToolbarEvent::StartToolbarItemDrag { .. }); if matches!(event, ToolbarEvent::DragToolbarItemOver { .. }) && !self.toolbar_item_drag_update_allowed() @@ -349,9 +283,6 @@ impl WaylandState { if applied { self.toolbar.mark_dirty(); self.input_state.needs_redraw = true; - if pane_switch { - self.reset_side_toolbar_focus(); - } #[cfg(feature = "tablet-input")] if thickness_event && self.sync_stylus_thickness_cache(prev_thickness) { diff --git a/src/backend/wayland/state/toolbar/events/feedback.rs b/src/backend/wayland/state/toolbar/events/feedback.rs index 3091df5c..2a2ff79a 100644 --- a/src/backend/wayland/state/toolbar/events/feedback.rs +++ b/src/backend/wayland/state/toolbar/events/feedback.rs @@ -6,14 +6,12 @@ use crate::ui::toolbar::ToolbarEvent; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum ToolbarPinChange { Top(bool), - Side(bool), } impl ToolbarPinChange { pub(super) fn from_event(event: &ToolbarEvent) -> Option { match event { ToolbarEvent::PinTopToolbar(pinned) => Some(Self::Top(*pinned)), - ToolbarEvent::PinSideToolbar(pinned) => Some(Self::Side(*pinned)), _ => None, } } @@ -26,24 +24,12 @@ impl ToolbarPinChange { (Self::Top(false), ToolbarPinDurability::StartupPersistent) => { "Top toolbar will be hidden at startup" } - (Self::Side(true), ToolbarPinDurability::StartupPersistent) => { - "Side toolbar will open at startup" - } - (Self::Side(false), ToolbarPinDurability::StartupPersistent) => { - "Side toolbar will be hidden at startup" - } (Self::Top(true), ToolbarPinDurability::LiveOnly) => { "Top toolbar pinned for this run only" } (Self::Top(false), ToolbarPinDurability::LiveOnly) => { "Top toolbar unpinned for this run only" } - (Self::Side(true), ToolbarPinDurability::LiveOnly) => { - "Side toolbar pinned for this run only" - } - (Self::Side(false), ToolbarPinDurability::LiveOnly) => { - "Side toolbar unpinned for this run only" - } } } diff --git a/src/backend/wayland/state/toolbar/events/session.rs b/src/backend/wayland/state/toolbar/events/session.rs index 020d0c2d..0abcc7c9 100644 --- a/src/backend/wayland/state/toolbar/events/session.rs +++ b/src/backend/wayland/state/toolbar/events/session.rs @@ -12,12 +12,9 @@ pub(super) fn populate_session_snapshot( let active_path = options.map(|options| options.session_file_path()); snapshot.active_session_name = active_path.as_deref().map(session_display_name); snapshot.active_session_path = active_path.clone(); - // Recents are only read (from the catalog on disk) while a surface that - // shows them is up: the side palette's Session pane or the top strip's - // Session popover. - let session_surface_open = snapshot.active_side_pane == crate::ui::toolbar::SidePane::Session - || snapshot.session_popover_open; - snapshot.recent_sessions = if session_surface_open { + // Recents are only read (from the catalog on disk) while the top strip's + // Session popover is up. + snapshot.recent_sessions = if snapshot.session_popover_open { recent_session_snapshots(active_path.as_deref()) } else { Vec::new() diff --git a/src/backend/wayland/state/toolbar/events/tests.rs b/src/backend/wayland/state/toolbar/events/tests.rs index fb380d0e..c069b8f7 100644 --- a/src/backend/wayland/state/toolbar/events/tests.rs +++ b/src/backend/wayland/state/toolbar/events/tests.rs @@ -33,7 +33,6 @@ use crate::draw::{Color, FontDescriptor}; use crate::env_vars::XDG_DATA_HOME_ENV; use crate::input::state::test_support::make_test_input_state; use crate::input::{EraserMode, Tool}; -use crate::ui::toolbar::ToolbarSideSection; use anyhow::anyhow; use std::fs; use std::path::{Path, PathBuf}; @@ -86,16 +85,6 @@ fn pin_confirmations_distinguish_persistent_and_live_only_changes() { ToolbarPinDurability::StartupPersistent, "Top toolbar will be hidden at startup", ), - ( - ToolbarPinChange::Side(true), - ToolbarPinDurability::StartupPersistent, - "Side toolbar will open at startup", - ), - ( - ToolbarPinChange::Side(false), - ToolbarPinDurability::StartupPersistent, - "Side toolbar will be hidden at startup", - ), ( ToolbarPinChange::Top(true), ToolbarPinDurability::LiveOnly, @@ -106,16 +95,6 @@ fn pin_confirmations_distinguish_persistent_and_live_only_changes() { ToolbarPinDurability::LiveOnly, "Top toolbar unpinned for this run only", ), - ( - ToolbarPinChange::Side(true), - ToolbarPinDurability::LiveOnly, - "Side toolbar pinned for this run only", - ), - ( - ToolbarPinChange::Side(false), - ToolbarPinDurability::LiveOnly, - "Side toolbar unpinned for this run only", - ), ]; for (change, durability, expected) in cases { @@ -171,7 +150,6 @@ fn runtime_toolbar_events_do_not_directly_save_config() { ToolbarEvent::SaveSessionAsCancel, ToolbarEvent::SessionInfo, ToolbarEvent::ClearSession, - ToolbarEvent::ScrollSidePane(24.0), // The overflow-anchored Session/Settings popovers are runtime-only // flyout state, like the overflow toggle itself. ToolbarEvent::ToggleSessionPopover(true), @@ -197,19 +175,8 @@ fn toolbar_runtime_preferences_have_exact_runtime_state_targets() { let events = vec![ (ToolbarEvent::PinTopToolbar(true), Runtime::TopPinned), - (ToolbarEvent::PinSideToolbar(true), Runtime::SidePinned), - ( - ToolbarEvent::SetSidePane(crate::ui::toolbar::SidePane::Canvas), - Runtime::SidePane, - ), - ( - ToolbarEvent::ToggleSideSectionCollapsed(ToolbarSideSection::Session, true), - Runtime::CollapsedSection(ToolbarSideSection::Session), - ), (ToolbarEvent::SetTopMinimized(true), Runtime::TopMinimized), - (ToolbarEvent::SetSideMinimized(true), Runtime::SideMinimized), (ToolbarEvent::CloseTopToolbar, Runtime::TopMinimized), - (ToolbarEvent::CloseSideToolbar, Runtime::SideMinimized), ( ToolbarEvent::SetToolbarItemHidden(ids::TOP_TOOL_PEN, true), Runtime::ItemVisibility { @@ -505,32 +472,6 @@ fn command_palette_and_shortcut_capture_block_shared_toolbar_events() { assert!(toolbar_event_blocked_by_modal(&input_state)); } -#[test] -fn drawer_hint_pre_apply_effect_is_conditionally_recorded_below_max() { - let mut state = OnboardingState { - drawer_hint_count: crate::onboarding::DRAWER_HINT_MAX - 1, - drawer_hint_shown: false, - ..OnboardingState::default() - }; - - assert!(record_drawer_hint_shown(&mut state)); - assert_eq!(state.drawer_hint_count, crate::onboarding::DRAWER_HINT_MAX); - assert!(state.drawer_hint_shown); -} - -#[test] -fn drawer_hint_pre_apply_effect_is_ignored_at_max() { - let mut state = OnboardingState { - drawer_hint_count: crate::onboarding::DRAWER_HINT_MAX, - drawer_hint_shown: true, - ..OnboardingState::default() - }; - - assert!(!record_drawer_hint_shown(&mut state)); - assert_eq!(state.drawer_hint_count, crate::onboarding::DRAWER_HINT_MAX); - assert!(state.drawer_hint_shown); -} - fn failing_session_file_chooser( _mode: SessionFileDialogMode, _current_path: Option<&Path>, diff --git a/src/backend/wayland/state/toolbar/geometry.rs b/src/backend/wayland/state/toolbar/geometry.rs index d2f365f7..6d38fd3c 100644 --- a/src/backend/wayland/state/toolbar/geometry.rs +++ b/src/backend/wayland/state/toolbar/geometry.rs @@ -2,8 +2,6 @@ pub(super) struct ToolbarOffsets { pub top_x: f64, pub top_y: f64, - pub side_x: f64, - pub side_y: f64, } #[derive(Debug, Clone, Copy, PartialEq)] @@ -12,10 +10,6 @@ pub(super) struct ToolbarClampBounds { pub max_top_x: f64, pub min_top_y: f64, pub max_top_y: f64, - pub min_side_x: f64, - pub max_side_x: f64, - pub min_side_y: f64, - pub max_side_y: f64, } #[derive(Debug, Clone, Copy)] @@ -23,15 +17,10 @@ pub(super) struct ToolbarClampInput { pub width: f64, pub height: f64, pub top_size: (u32, u32), - pub side_size: (u32, u32), pub top_base_x: f64, pub top_base_y: f64, pub top_margin_right: f64, pub top_margin_bottom: f64, - pub side_base_margin_left: f64, - pub side_base_margin_top: f64, - pub side_margin_right: f64, - pub side_margin_bottom: f64, } /// Clamp one floating surface axis using the same start/end margin policy @@ -48,23 +37,6 @@ pub(crate) fn clamp_floating_axis_offset( (offset.clamp(min, max), min, max) } -pub(super) fn compute_inline_top_base_x( - base: f64, - side_visible: bool, - side_width: f64, - side_start_y: f64, - top_bottom_y: f64, - inline_top_push: f64, - allow_push: bool, -) -> f64 { - let side_overlaps_top = side_visible && side_start_y < top_bottom_y; - if side_overlaps_top && allow_push { - base + side_width + inline_top_push - } else { - base - } -} - pub(super) fn remaining_top_width( screen_width: f64, base_x: f64, @@ -100,8 +72,6 @@ pub(super) fn clamp_toolbar_offsets( ) -> (ToolbarOffsets, ToolbarClampBounds) { let top_w = input.top_size.0 as f64; let top_h = input.top_size.1 as f64; - let side_w = input.side_size.0 as f64; - let side_h = input.side_size.1 as f64; let (top_x, min_top_x, max_top_x) = clamp_floating_axis_offset( offsets.top_x, @@ -117,37 +87,14 @@ pub(super) fn clamp_toolbar_offsets( input.top_base_y, input.top_margin_bottom, ); - let (side_x, min_side_x, max_side_x) = clamp_floating_axis_offset( - offsets.side_x, - input.width, - side_w, - input.side_base_margin_left, - input.side_margin_right, - ); - let (side_y, min_side_y, max_side_y) = clamp_floating_axis_offset( - offsets.side_y, - input.height, - side_h, - input.side_base_margin_top, - input.side_margin_bottom, - ); - let clamped = ToolbarOffsets { - top_x, - top_y, - side_x, - side_y, - }; + let clamped = ToolbarOffsets { top_x, top_y }; let bounds = ToolbarClampBounds { min_top_x, max_top_x, min_top_y, max_top_y, - min_side_x, - max_side_x, - min_side_y, - max_side_y, }; (clamped, bounds) @@ -156,20 +103,11 @@ pub(super) fn clamp_toolbar_offsets( pub(super) fn compute_layer_margins( top_base_x: f64, top_base_margin_top: f64, - side_base_margin_left: f64, - side_base_margin_top: f64, offsets: ToolbarOffsets, -) -> (i32, i32, i32, i32) { +) -> (i32, i32) { let top_margin_left = (top_base_x + offsets.top_x).round() as i32; let top_margin_top = (top_base_margin_top + offsets.top_y).round() as i32; - let side_margin_top = (side_base_margin_top + offsets.side_y).round() as i32; - let side_margin_left = (side_base_margin_left + offsets.side_x).round() as i32; - ( - top_margin_left, - top_margin_top, - side_margin_top, - side_margin_left, - ) + (top_margin_left, top_margin_top) } pub(super) fn point_in_rect(px: f64, py: f64, x: f64, y: f64, w: f64, h: f64) -> bool { @@ -181,37 +119,11 @@ mod tests { use super::*; #[test] - fn inline_top_base_x_pushes_when_overlapping_and_allowed() { + fn inline_budget_leaves_room_for_the_top_strip_base() { let base = 24.0; - let side_width = 80.0; - let top_bottom_y = 40.0; - let side_start_y = 12.0; - let push = 16.0; - let result = compute_inline_top_base_x( - base, - true, - side_width, - side_start_y, - top_bottom_y, - push, - true, - ); - assert_eq!(result, 120.0); - } - - #[test] - fn inline_top_base_x_skips_push_when_not_allowed() { - let result = compute_inline_top_base_x(24.0, true, 80.0, 10.0, 40.0, 16.0, false); - assert_eq!(result, 24.0); - } - - #[test] - fn inline_budget_subtracts_visible_side_palette_base() { - let base = compute_inline_top_base_x(24.0, true, 220.0, 12.0, 58.0, 16.0, true); let budget = remaining_top_width(800.0, base, 12.0, 1.0).unwrap(); - assert_eq!(base, 260.0); - assert_eq!(budget, 528.0); + assert_eq!(budget, 764.0); assert_eq!(base + budget + 12.0, 800.0); let state = crate::input::state::test_support::make_test_input_state(); @@ -224,16 +136,6 @@ mod tests { assert!(planned <= budget, "planned={planned}, budget={budget}"); } - #[test] - fn layer_shell_budget_leaves_room_for_its_positioned_base() { - let base = compute_inline_top_base_x(24.0, true, 260.0, 24.0, 58.0, 16.0, true); - let budget = remaining_top_width(1366.0, base, 12.0, 1.0).unwrap(); - - assert_eq!(base, 300.0); - assert_eq!(budget, 1054.0); - assert_eq!(base + budget + 12.0, 1366.0); - } - #[test] fn top_budget_does_not_claim_a_480_pixel_floor() { assert_eq!(remaining_top_width(400.0, 12.0, 12.0, 1.0), Some(376.0)); @@ -256,22 +158,15 @@ mod tests { let offsets = ToolbarOffsets { top_x: 500.0, top_y: -100.0, - side_x: -100.0, - side_y: 500.0, }; let input = ToolbarClampInput { width: 300.0, height: 200.0, top_size: (100, 40), - side_size: (60, 120), top_base_x: 40.0, top_base_y: 16.0, top_margin_right: 12.0, top_margin_bottom: 0.0, - side_base_margin_left: 24.0, - side_base_margin_top: 24.0, - side_margin_right: 0.0, - side_margin_bottom: 24.0, }; let (clamped, bounds) = clamp_toolbar_offsets(offsets, input); @@ -282,10 +177,6 @@ mod tests { max_top_x: 148.0, min_top_y: -16.0, max_top_y: 144.0, - min_side_x: -24.0, - max_side_x: 216.0, - min_side_y: -24.0, - max_side_y: 32.0, } ); assert_eq!( @@ -293,44 +184,31 @@ mod tests { ToolbarOffsets { top_x: 148.0, top_y: -16.0, - side_x: -24.0, - side_y: 32.0, } ); } #[test] fn pipeline_clamp_then_margins_matches_expected() { - let base = 24.0; - let top_base_x = compute_inline_top_base_x(base, true, 80.0, 12.0, 48.0, 16.0, true); + let top_base_x = 24.0; let input = ToolbarClampInput { width: 500.0, height: 220.0, top_size: (200, 40), - side_size: (80, 120), top_base_x, top_base_y: 16.0, top_margin_right: 12.0, top_margin_bottom: 0.0, - side_base_margin_left: 24.0, - side_base_margin_top: 24.0, - side_margin_right: 0.0, - side_margin_bottom: 24.0, }; let offsets = ToolbarOffsets { top_x: 400.0, top_y: 10.0, - side_x: 0.0, - side_y: 0.0, }; let (clamped, _) = clamp_toolbar_offsets(offsets, input); - let (top_left, top_top, side_top, side_left) = - compute_layer_margins(top_base_x, 12.0, 24.0, 24.0, clamped); + let (top_left, top_top) = compute_layer_margins(top_base_x, 12.0, clamped); assert_eq!(top_left, 288); assert_eq!(top_top, 22); - assert_eq!(side_top, 24); - assert_eq!(side_left, 24); } #[test] @@ -339,24 +217,17 @@ mod tests { width: 300.0, height: 200.0, top_size: (120, 40), - side_size: (60, 120), top_base_x: 40.0, top_base_y: 16.0, top_margin_right: 12.0, top_margin_bottom: 0.0, - side_base_margin_left: 24.0, - side_base_margin_top: 24.0, - side_margin_right: 0.0, - side_margin_bottom: 24.0, }; let offsets = ToolbarOffsets { top_x: -400.0, top_y: 0.0, - side_x: 0.0, - side_y: 0.0, }; let (clamped, _) = clamp_toolbar_offsets(offsets, input); - let (top_left, _, _, _) = compute_layer_margins(40.0, 12.0, 24.0, 24.0, clamped); + let (top_left, _) = compute_layer_margins(40.0, 12.0, clamped); assert_eq!(top_left, 0); } } diff --git a/src/backend/wayland/state/toolbar/gtk_feedback.rs b/src/backend/wayland/state/toolbar/gtk_feedback.rs index be5e470f..9271f0a7 100644 --- a/src/backend/wayland/state/toolbar/gtk_feedback.rs +++ b/src/backend/wayland/state/toolbar/gtk_feedback.rs @@ -51,13 +51,10 @@ fn clamp_gtk_surface_offset( } impl WaylandState { - /// Base X the GTK top strip must use, matching the backend clamp and - /// overlap-push math (`inline_top_base_x` is toolbar-module private). - pub(in crate::backend::wayland) fn gtk_top_base_x( - &self, - snapshot: &crate::ui::toolbar::ToolbarSnapshot, - ) -> f64 { - self.inline_top_base_x(snapshot) + /// Base X the GTK top strip must use, matching the backend clamp math + /// (`inline_top_base_x` is toolbar-module private). + pub(in crate::backend::wayland) fn gtk_top_base_x(&self) -> f64 { + self.inline_top_base_x() } pub(in crate::backend::wayland) fn apply_gtk_top_offset( @@ -98,47 +95,6 @@ impl WaylandState { } } - pub(in crate::backend::wayland) fn apply_gtk_side_offset( - &mut self, - x: f64, - y: f64, - surface_size: GtkToolbarSurfaceSize, - phase: GtkToolbarDragPhase, - ) { - if phase == GtkToolbarDragPhase::Start { - self.data.gtk_side_drag_rebase = None; - if !self.begin_toolbar_position_preview(MoveDragKind::Side) { - self.data.gtk_side_drag_blocked = true; - return; - } - self.begin_gtk_toolbar_drag_preview(GtkToolbarKind::Side); - } else if !self.toolbar_position_drag_update_allowed(MoveDragKind::Side) { - if phase.is_end() { - self.data.gtk_side_drag_rebase = None; - self.clamp_gtk_side_offset(surface_size); - self.finish_gtk_offset_change(); - } else { - self.data.gtk_side_drag_rebase = Some(gtk_drag_rebase( - ( - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset, - ), - (x, y), - )); - } - return; - } - let (x, y) = apply_gtk_drag_rebase((x, y), self.data.gtk_side_drag_rebase); - self.data.toolbar_side_offset_x = x; - self.data.toolbar_side_offset = y; - self.mark_gtk_drag_preview_dirty(); - if phase.is_end() { - self.data.gtk_side_drag_rebase = None; - self.clamp_gtk_side_offset(surface_size); - self.finish_gtk_offset_change(); - } - } - fn mark_gtk_drag_preview_dirty(&mut self) { if self.data.gtk_drag_preview.is_none() { return; @@ -149,8 +105,7 @@ impl WaylandState { } fn clamp_gtk_top_offset(&mut self, surface_size: GtkToolbarSurfaceSize) { - let snapshot = self.toolbar_snapshot(); - let base_x = self.inline_top_base_x(&snapshot); + let base_x = self.inline_top_base_x(); let before = (self.data.toolbar_top_offset, self.data.toolbar_top_offset_y); let Some((x, y)) = clamp_gtk_surface_offset( (self.data.toolbar_top_offset, self.data.toolbar_top_offset_y), @@ -159,6 +114,7 @@ impl WaylandState { (base_x, Self::TOP_BASE_MARGIN_TOP), (Self::TOP_MARGIN_RIGHT, Self::TOP_MARGIN_BOTTOM), ) else { + let snapshot = self.toolbar_snapshot(); self.clamp_toolbar_offsets(&snapshot); return; }; @@ -180,44 +136,6 @@ impl WaylandState { }); } - fn clamp_gtk_side_offset(&mut self, surface_size: GtkToolbarSurfaceSize) { - let before = ( - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset, - ); - let Some((x, y)) = clamp_gtk_surface_offset( - ( - self.data.toolbar_side_offset_x, - self.data.toolbar_side_offset, - ), - (self.surface.width(), self.surface.height()), - surface_size, - (Self::SIDE_BASE_MARGIN_LEFT, Self::SIDE_BASE_MARGIN_TOP), - (Self::SIDE_MARGIN_RIGHT, Self::SIDE_MARGIN_BOTTOM), - ) else { - let snapshot = self.toolbar_snapshot(); - self.clamp_toolbar_offsets(&snapshot); - return; - }; - self.data.toolbar_side_offset_x = x; - self.data.toolbar_side_offset = y; - drag_log(|| { - format!( - "gtk side final clamp before=({:.3},{:.3}) after=({x:.3},{y:.3}) viewport={}x{} surface={}x{} base=({:.3},{:.3}) end=({:.3},{:.3})", - before.0, - before.1, - self.surface.width(), - self.surface.height(), - surface_size.width, - surface_size.height, - Self::SIDE_BASE_MARGIN_LEFT, - Self::SIDE_BASE_MARGIN_TOP, - Self::SIDE_MARGIN_RIGHT, - Self::SIDE_MARGIN_BOTTOM, - ) - }); - } - /// On drag end, persist the offset accepted against GTK's measured /// surface. Intermediate positions are mirrored without disk writes. fn finish_gtk_offset_change(&mut self) { diff --git a/src/backend/wayland/state/toolbar/inline/drag.rs b/src/backend/wayland/state/toolbar/inline/drag.rs index 74f438a4..15001c7e 100644 --- a/src/backend/wayland/state/toolbar/inline/drag.rs +++ b/src/backend/wayland/state/toolbar/inline/drag.rs @@ -12,17 +12,12 @@ impl WaylandState { use crate::backend::wayland::toolbar_intent::ToolbarIntent; use crate::ui::toolbar::ToolbarEvent; - match self.data.toolbar_move_drag { - Some(MoveDrag { - kind: MoveDragKind::Top, - .. - }) => Some(ToolbarIntent(ToolbarEvent::MoveTopToolbar { x, y })), - Some(MoveDrag { - kind: MoveDragKind::Side, - .. - }) => Some(ToolbarIntent(ToolbarEvent::MoveSideToolbar { x, y })), - None => None, - } + self.data.toolbar_move_drag.as_ref().map( + |MoveDrag { + kind: MoveDragKind::Top, + .. + }| ToolbarIntent(ToolbarEvent::MoveTopToolbar { x, y }), + ) } /// Returns true if we're currently in a toolbar move drag operation. diff --git a/src/backend/wayland/state/toolbar/inline/focus.rs b/src/backend/wayland/state/toolbar/inline/focus.rs index 8bcb3081..efebe4e4 100644 --- a/src/backend/wayland/state/toolbar/inline/focus.rs +++ b/src/backend/wayland/state/toolbar/inline/focus.rs @@ -1,115 +1,74 @@ use super::*; -use crate::backend::wayland::toolbar::ToolbarFocusTarget; use crate::backend::wayland::toolbar::hit::{ focus_hover_point, focused_event, next_focus_index, resolve_focus_index, }; use crate::input::Key; impl WaylandState { - pub(in crate::backend::wayland) fn inline_toolbar_focus_hover( - &self, - target: ToolbarFocusTarget, - ) -> Option<(f64, f64)> { - let hits = match target { - ToolbarFocusTarget::Top => &self.data.inline_top_hits, - ToolbarFocusTarget::Side => &self.data.inline_side_hits, - }; + fn inline_focus_index(&self) -> Option { + self.data.inline_top_focus_index + } + + fn inline_focus_id(&self) -> Option<&str> { + self.data.inline_top_focus_id.as_deref() + } + + pub(in crate::backend::wayland) fn inline_toolbar_focus_hover(&self) -> Option<(f64, f64)> { + let hits = &self.data.inline_top_hits; focus_hover_point( hits, - resolve_focus_index( - hits, - self.inline_focus_index(target), - self.inline_focus_id(target), - ), + resolve_focus_index(hits, self.inline_focus_index(), self.inline_focus_id()), ) } - pub(in crate::backend::wayland) fn inline_toolbar_focus_next( - &mut self, - target: ToolbarFocusTarget, - reverse: bool, - ) -> bool { - let hits = match target { - ToolbarFocusTarget::Top => &self.data.inline_top_hits, - ToolbarFocusTarget::Side => &self.data.inline_side_hits, - }; - let current = resolve_focus_index( - hits, - self.inline_focus_index(target), - self.inline_focus_id(target), - ); + pub(in crate::backend::wayland) fn inline_toolbar_focus_next(&mut self, reverse: bool) -> bool { + let hits = &self.data.inline_top_hits; + let current = resolve_focus_index(hits, self.inline_focus_index(), self.inline_focus_id()); let next = next_focus_index(hits, current, reverse); if next != current { let id = next.and_then(|index| hits[index].focus_id.clone()); - *self.inline_focus_index_mut(target) = next; - self.set_inline_focus_id(target, id); + self.data.inline_top_focus_index = next; + self.data.inline_top_focus_id = id; self.mark_inline_toolbar_full_damage(); return true; } false } - pub(in crate::backend::wayland) fn inline_toolbar_focused_event( - &self, - target: ToolbarFocusTarget, - ) -> Option { - let hits = match target { - ToolbarFocusTarget::Top => &self.data.inline_top_hits, - ToolbarFocusTarget::Side => &self.data.inline_side_hits, - }; + pub(in crate::backend::wayland) fn inline_toolbar_focused_event(&self) -> Option { + let hits = &self.data.inline_top_hits; focused_event( hits, - resolve_focus_index( - hits, - self.inline_focus_index(target), - self.inline_focus_id(target), - ), + resolve_focus_index(hits, self.inline_focus_index(), self.inline_focus_id()), ) } - pub(in crate::backend::wayland) fn inline_toolbar_focus_target_from_hover( - &self, - ) -> Option { - if self.data.inline_top_hover.is_some() { - Some(ToolbarFocusTarget::Top) - } else if self.data.inline_side_hover.is_some() { - Some(ToolbarFocusTarget::Side) - } else { - None - } - } - - pub(in crate::backend::wayland) fn toolbar_focus_target(&self) -> Option { - self.data.toolbar_focus_target + pub(in crate::backend::wayland) fn toolbar_focus_active(&self) -> bool { + self.data.toolbar_focus_active } - pub(in crate::backend::wayland) fn set_toolbar_focus_target( - &mut self, - target: Option, - ) { - self.data.toolbar_focus_target = target; + pub(in crate::backend::wayland) fn set_toolbar_focus_active(&mut self, active: bool) { + self.data.toolbar_focus_active = active; } pub(in crate::backend::wayland) fn clear_toolbar_focus(&mut self) { - self.data.toolbar_focus_target = None; + self.data.toolbar_focus_active = false; self.toolbar.clear_focus(); - let had_inline_focus = self.data.inline_top_focus_index.is_some() - || self.data.inline_side_focus_index.is_some() - || self.data.inline_top_focus_id.is_some() - || self.data.inline_side_focus_id.is_some(); + let had_inline_focus = + self.data.inline_top_focus_index.is_some() || self.data.inline_top_focus_id.is_some(); self.clear_inline_toolbar_focus(); if self.inline_toolbars_active() && had_inline_focus { self.mark_inline_toolbar_full_damage(); } } - pub(in crate::backend::wayland) fn toolbar_focus_target_from_hover( - &self, - ) -> Option { + /// Whether the pointer currently hovers the toolbar in the active + /// placement, which is what seeds keyboard focus on the first Tab. + pub(in crate::backend::wayland) fn toolbar_hovered(&self) -> bool { if self.inline_toolbars_active() { - self.inline_toolbar_focus_target_from_hover() + self.data.inline_top_hover.is_some() } else { - self.toolbar.hovered_target() + self.toolbar.is_hovered() } } @@ -145,24 +104,14 @@ impl WaylandState { let is_tab = matches!(key, Key::Tab); let is_activate = matches!(key, Key::Return | Key::Space); - let mut target = self.toolbar_focus_target(); - if target.is_none() { - target = self.toolbar_focus_target_from_hover(); - if target.is_none() { + if !self.toolbar_focus_active() { + if !self.toolbar_hovered() { return false; } - self.data.toolbar_focus_target = target; + self.data.toolbar_focus_active = true; } - let target = match target { - Some(target) => target, - None => return false, - }; - if matches!(target, ToolbarFocusTarget::Top) && !self.toolbar.is_top_visible() { - self.clear_toolbar_focus(); - return false; - } - if matches!(target, ToolbarFocusTarget::Side) && !self.toolbar.is_side_visible() { + if !self.toolbar.is_top_visible() { self.clear_toolbar_focus(); return false; } @@ -170,18 +119,18 @@ impl WaylandState { if is_tab { let reverse = self.input_state.modifiers.shift; if self.inline_toolbars_active() { - self.inline_toolbar_focus_next(target, reverse); + self.inline_toolbar_focus_next(reverse); } else { - self.toolbar.focus_next(target, reverse); + self.toolbar.focus_next(reverse); } return true; } if is_activate { let event = if self.inline_toolbars_active() { - self.inline_toolbar_focused_event(target) + self.inline_toolbar_focused_event() } else { - self.toolbar.focused_event(target) + self.toolbar.focused_event() }; if let Some(event) = event { self.handle_toolbar_event(event, conn, qh); diff --git a/src/backend/wayland/state/toolbar/inline/input.rs b/src/backend/wayland/state/toolbar/inline/input.rs index d17711d3..4a7e1462 100644 --- a/src/backend/wayland/state/toolbar/inline/input.rs +++ b/src/backend/wayland/state/toolbar/inline/input.rs @@ -10,25 +10,14 @@ impl WaylandState { if !self.inline_toolbars_active() || !self.toolbar.is_visible() { return None; } - if self.toolbar.is_top_visible() - && point_in_surface(self.data.inline_top_rect, position) - && let Some(intent) = self - .data - .inline_top_hits - .iter() - .find_map(|hit| intent_for_hit(hit, position.0, position.1)) - { - return Some(intent); - } - if self.toolbar.is_side_visible() && point_in_surface(self.data.inline_side_rect, position) + if !self.toolbar.is_top_visible() || !point_in_surface(self.data.inline_top_rect, position) { - return self - .data - .inline_side_hits - .iter() - .find_map(|hit| intent_for_hit(hit, position.0, position.1)); + return None; } - None + self.data + .inline_top_hits + .iter() + .find_map(|hit| intent_for_hit(hit, position.0, position.1)) } /// The quick-color slot an inline-toolbar secondary press targets, read @@ -37,25 +26,14 @@ impl WaylandState { if !self.inline_toolbars_active() || !self.toolbar.is_visible() { return None; } - if self.toolbar.is_top_visible() - && point_in_surface(self.data.inline_top_rect, position) - && let Some(index) = self - .data - .inline_top_hits - .iter() - .find_map(|hit| quick_color_slot_for_hit(hit, position.0, position.1)) - { - return Some(index); - } - if self.toolbar.is_side_visible() && point_in_surface(self.data.inline_side_rect, position) + if !self.toolbar.is_top_visible() || !point_in_surface(self.data.inline_top_rect, position) { - return self - .data - .inline_side_hits - .iter() - .find_map(|hit| quick_color_slot_for_hit(hit, position.0, position.1)); + return None; } - None + self.data + .inline_top_hits + .iter() + .find_map(|hit| quick_color_slot_for_hit(hit, position.0, position.1)) } /// Secondary press on an inline-toolbar swatch: opens the picker bound to @@ -86,25 +64,14 @@ impl WaylandState { if let Some(intent) = self.move_drag_intent(position.0, position.1) { return Some(intent); } - if self.toolbar.is_top_visible() - && point_in_surface(self.data.inline_top_rect, position) - && let Some(intent) = self - .data - .inline_top_hits - .iter() - .find_map(|hit| drag_intent_for_hit(hit, position.0, position.1)) - { - return Some(intent); - } - if self.toolbar.is_side_visible() && point_in_surface(self.data.inline_side_rect, position) + if !self.toolbar.is_top_visible() || !point_in_surface(self.data.inline_top_rect, position) { - return self - .data - .inline_side_hits - .iter() - .find_map(|hit| drag_intent_for_hit(hit, position.0, position.1)); + return None; } - None + self.data + .inline_top_hits + .iter() + .find_map(|hit| drag_intent_for_hit(hit, position.0, position.1)) } pub(in crate::backend::wayland) fn inline_toolbar_motion( @@ -120,25 +87,16 @@ impl WaylandState { self.input_state.update_pointer_position(mx, my); let was_top_hover = self.data.inline_top_hover; - let was_side_hover = self.data.inline_side_hover; let was_top_hit = was_top_hover.and_then(|(x, y)| { self.data .inline_top_hits .iter() .position(|hit| hit.contains(x, y)) }); - let was_side_hit = was_side_hover.and_then(|(x, y)| { - self.data - .inline_side_hits - .iter() - .position(|hit| hit.contains(x, y)) - }); self.data.inline_top_hover = None; - self.data.inline_side_hover = None; let top_visible = self.toolbar.is_top_visible(); - let side_visible = self.toolbar.is_side_visible(); let mut over_toolbar = false; if top_visible @@ -154,19 +112,6 @@ impl WaylandState { self.data.inline_top_hover_start = None; } - if side_visible - && let Some((x, y, w, h)) = self.data.inline_side_rect - && geometry::point_in_rect(position.0, position.1, x, y, w, h) - { - over_toolbar = true; - if was_side_hover.is_none() { - self.data.inline_side_hover_start = Some(Instant::now()); - } - self.data.inline_side_hover = Some(position); - } else { - self.data.inline_side_hover_start = None; - } - if self.toolbar_dragging() && let Some(intent) = self.inline_toolbar_drag_at(position) { @@ -186,45 +131,25 @@ impl WaylandState { .iter() .position(|hit| hit.contains(x, y)) }); - let side_hit = self.data.inline_side_hover.and_then(|(x, y)| { - self.data - .inline_side_hits - .iter() - .position(|hit| hit.contains(x, y)) - }); let top_target_changed = inline_hover_target_changed( was_top_hover, was_top_hit, self.data.inline_top_hover, top_hit, ); - let side_target_changed = inline_hover_target_changed( - was_side_hover, - was_side_hit, - self.data.inline_side_hover, - side_hit, - ); if top_target_changed { self.data.inline_top_tooltip_pending = inline_tooltip_pending( self.data.inline_top_hover_start, hit_has_tooltip(&self.data.inline_top_hits, top_hit), ); } - if side_target_changed { - self.data.inline_side_tooltip_pending = inline_tooltip_pending( - self.data.inline_side_hover_start, - hit_has_tooltip(&self.data.inline_side_hits, side_hit), - ); - } - if top_target_changed || side_target_changed { + if top_target_changed { // The inline toolbar and annotations share the main surface's SHM // swapchain. Refresh every slot when hover visuals change so a // compositor cannot resurface a buffer containing older toolbar or // annotation pixels during rapid pointer motion. self.mark_inline_toolbar_full_damage(); - } else if was_top_hover != self.data.inline_top_hover - || was_side_hover != self.data.inline_side_hover - { + } else if was_top_hover != self.data.inline_top_hover { // Preserve motion-driven tooltip timing without paying for a full // swapchain refresh while the pointer stays on the same control. self.toolbar.mark_dirty(); @@ -235,8 +160,8 @@ impl WaylandState { self.set_pointer_over_toolbar(true); } else if !self.toolbar_dragging() { self.set_pointer_over_toolbar(false); - if self.data.toolbar_focus_target.is_some() { - self.data.toolbar_focus_target = None; + if self.data.toolbar_focus_active { + self.data.toolbar_focus_active = false; self.clear_inline_toolbar_focus(); self.mark_inline_toolbar_full_damage(); } @@ -276,19 +201,13 @@ impl WaylandState { if !self.inline_toolbars_active() { return; } - let had_hover = - self.data.inline_top_hover.is_some() || self.data.inline_side_hover.is_some(); - let had_focus = self.data.inline_top_focus_index.is_some() - || self.data.inline_side_focus_index.is_some() - || self.data.inline_top_focus_id.is_some() - || self.data.inline_side_focus_id.is_some(); + let had_hover = self.data.inline_top_hover.is_some(); + let had_focus = + self.data.inline_top_focus_index.is_some() || self.data.inline_top_focus_id.is_some(); self.data.inline_top_hover = None; - self.data.inline_side_hover = None; self.data.inline_top_hover_start = None; - self.data.inline_side_hover_start = None; self.data.inline_top_tooltip_pending = false; - self.data.inline_side_tooltip_pending = false; - self.data.toolbar_focus_target = None; + self.data.toolbar_focus_active = false; self.clear_inline_toolbar_focus(); self.set_pointer_over_toolbar(false); // Don't clear drag state if we're in a move drag - the drag continues outside diff --git a/src/backend/wayland/state/toolbar/inline/mod.rs b/src/backend/wayland/state/toolbar/inline/mod.rs index 56b95a1e..0a764caa 100644 --- a/src/backend/wayland/state/toolbar/inline/mod.rs +++ b/src/backend/wayland/state/toolbar/inline/mod.rs @@ -1,5 +1,5 @@ use super::*; -use crate::backend::wayland::toolbar::{ToolbarCursorHint, ToolbarFocusTarget}; +use crate::backend::wayland::toolbar::ToolbarCursorHint; mod drag; mod focus; @@ -19,114 +19,51 @@ impl WaylandState { &self, now: Instant, ) -> Option { - min_inline_tooltip_timeout( - inline_tooltip_timeout( - self.data.inline_top_tooltip_pending, - self.data.inline_top_hover_start, - now, - ), - inline_tooltip_timeout( - self.data.inline_side_tooltip_pending, - self.data.inline_side_hover_start, - now, - ), + inline_tooltip_timeout( + self.data.inline_top_tooltip_pending, + self.data.inline_top_hover_start, + now, ) } pub(in crate::backend::wayland) fn update_inline_toolbar_tooltip(&mut self, now: Instant) { - let top_due = inline_tooltip_due( + let due = inline_tooltip_due( self.data.inline_top_tooltip_pending, self.data.inline_top_hover_start, now, ); - let side_due = inline_tooltip_due( - self.data.inline_side_tooltip_pending, - self.data.inline_side_hover_start, - now, - ); - if top_due { + if due { self.data.inline_top_tooltip_pending = false; - } - if side_due { - self.data.inline_side_tooltip_pending = false; - } - if top_due || side_due { self.mark_inline_toolbar_full_damage(); } } pub(super) fn clear_inline_toolbar_hits(&mut self) { self.data.inline_top_hits.clear(); - self.data.inline_side_hits.clear(); self.data.inline_top_rect = None; - self.data.inline_side_rect = None; } pub(super) fn clear_inline_toolbar_hover(&mut self) { self.data.inline_top_hover = None; - self.data.inline_side_hover = None; self.data.inline_top_tooltip_pending = false; - self.data.inline_side_tooltip_pending = false; } pub(super) fn clear_inline_toolbar_focus(&mut self) { self.data.inline_top_focus_index = None; - self.data.inline_side_focus_index = None; self.data.inline_top_focus_id = None; - self.data.inline_side_focus_id = None; - } - - fn inline_focus_index(&self, target: ToolbarFocusTarget) -> Option { - match target { - ToolbarFocusTarget::Top => self.data.inline_top_focus_index, - ToolbarFocusTarget::Side => self.data.inline_side_focus_index, - } - } - - fn inline_focus_index_mut(&mut self, target: ToolbarFocusTarget) -> &mut Option { - match target { - ToolbarFocusTarget::Top => &mut self.data.inline_top_focus_index, - ToolbarFocusTarget::Side => &mut self.data.inline_side_focus_index, - } - } - - fn inline_focus_id(&self, target: ToolbarFocusTarget) -> Option<&str> { - match target { - ToolbarFocusTarget::Top => self.data.inline_top_focus_id.as_deref(), - ToolbarFocusTarget::Side => self.data.inline_side_focus_id.as_deref(), - } - } - - fn set_inline_focus_id(&mut self, target: ToolbarFocusTarget, id: Option) { - match target { - ToolbarFocusTarget::Top => self.data.inline_top_focus_id = id, - ToolbarFocusTarget::Side => self.data.inline_side_focus_id = id, - } } /// Get cursor hint for inline toolbar hover position. pub(in crate::backend::wayland) fn inline_toolbar_cursor_hint( &self, ) -> Option { - // Check top toolbar hover - if let Some((hx, hy)) = self.data.inline_top_hover { - for hit in &self.data.inline_top_hits { - if hit.contains(hx, hy) { - return Some(hit.kind.cursor_hint()); - } + let (hx, hy) = self.data.inline_top_hover?; + for hit in &self.data.inline_top_hits { + if hit.contains(hx, hy) { + return Some(hit.kind.cursor_hint()); } - return Some(ToolbarCursorHint::Default); } - // Check side toolbar hover - if let Some((hx, hy)) = self.data.inline_side_hover { - for hit in &self.data.inline_side_hits { - if hit.contains(hx, hy) { - return Some(hit.kind.cursor_hint()); - } - } - return Some(ToolbarCursorHint::Default); - } - None + Some(ToolbarCursorHint::Default) } } @@ -150,18 +87,10 @@ fn inline_tooltip_due(pending: bool, hover_start: Option, now: Instant) inline_tooltip_timeout(pending, hover_start, now) == Some(Duration::ZERO) } -fn min_inline_tooltip_timeout(top: Option, side: Option) -> Option { - match (top, side) { - (Some(top), Some(side)) => Some(top.min(side)), - (Some(timeout), None) | (None, Some(timeout)) => Some(timeout), - (None, None) => None, - } -} - #[cfg(test)] mod tests { - use super::{inline_tooltip_due, inline_tooltip_timeout, min_inline_tooltip_timeout}; - use std::time::{Duration, Instant}; + use super::{inline_tooltip_due, inline_tooltip_timeout}; + use std::time::Instant; #[test] fn inline_tooltip_timeout_reaches_zero_at_the_deadline() { @@ -175,15 +104,4 @@ mod tests { assert_eq!(inline_tooltip_timeout(false, Some(start), due), None); assert_eq!(inline_tooltip_timeout(true, None, due), None); } - - #[test] - fn inline_tooltip_timeout_uses_the_earliest_pending_toolbar() { - assert_eq!( - min_inline_tooltip_timeout( - Some(Duration::from_millis(200)), - Some(Duration::from_millis(50)), - ), - Some(Duration::from_millis(50)) - ); - } } diff --git a/src/backend/wayland/state/toolbar/inline/render.rs b/src/backend/wayland/state/toolbar/inline/render.rs index 9211e215..40157329 100644 --- a/src/backend/wayland/state/toolbar/inline/render.rs +++ b/src/backend/wayland/state/toolbar/inline/render.rs @@ -6,44 +6,13 @@ impl WaylandState { ctx: &cairo::Context, snapshot: &ToolbarSnapshot, ) { - if !self.inline_toolbars_render_active() || !self.toolbar.is_visible() { + if !self.inline_toolbars_render_active() || !self.toolbar.is_top_visible() { self.clear_inline_toolbar_hits(); self.clear_inline_toolbar_hover(); return; } - let top_visible = self.toolbar.is_top_visible(); - let side_visible = self.toolbar.is_side_visible(); - - if !top_visible { - self.data.inline_top_hover = None; - self.data.inline_top_tooltip_pending = false; - self.data.inline_top_focus_index = None; - self.data.inline_top_focus_id = None; - } - if !side_visible { - self.data.inline_side_hover = None; - self.data.inline_side_tooltip_pending = false; - self.data.inline_side_focus_index = None; - self.data.inline_side_focus_id = None; - } - - if !top_visible && !side_visible { - self.clear_inline_toolbar_hits(); - self.clear_inline_toolbar_hover(); - return; - } - - let top_focus_hover = if top_visible { - self.inline_toolbar_focus_hover(ToolbarFocusTarget::Top) - } else { - None - }; - let side_focus_hover = if side_visible { - self.inline_toolbar_focus_hover(ToolbarFocusTarget::Side) - } else { - None - }; + let focus_hover = self.inline_toolbar_focus_hover(); self.clear_inline_toolbar_hits(); self.clamp_toolbar_offsets(snapshot); let ui_scale = if snapshot.toolbar_scale.is_finite() { @@ -52,114 +21,53 @@ impl WaylandState { 1.0 }; - // Position inline toolbars with padding and keep top bar to the right of the side bar. - let side_offset = ( - Self::INLINE_SIDE_X + self.data.toolbar_side_offset_x, - Self::SIDE_BASE_MARGIN_TOP + self.data.toolbar_side_offset, - ); - - let top_base_x = self.inline_top_base_x(snapshot); let top_offset = ( - top_base_x + self.data.toolbar_top_offset, + self.inline_top_base_x() + self.data.toolbar_top_offset, self.inline_top_base_y() + self.data.toolbar_top_offset_y, ); - // Top toolbar - if top_visible { - let top_size = top_size(snapshot); - let top_base_w = top_size.0 as f64 / ui_scale; - let top_base_h = top_size.1 as f64 / ui_scale; - let top_hover_local = self - .data - .inline_top_hover - .or(top_focus_hover) - .map(|(x, y)| (x - top_offset.0, y - top_offset.1)) - .map(|(x, y)| (x / ui_scale, y / ui_scale)); - let _ = ctx.save(); - ctx.translate(top_offset.0, top_offset.1); - if (ui_scale - 1.0).abs() > f64::EPSILON { - ctx.scale(ui_scale, ui_scale); - } - if let Err(err) = render_top_strip( - ctx, - top_base_w, - top_base_h, - snapshot, - &mut self.data.inline_top_hits, - top_hover_local, - self.data.inline_top_hover_start, - ) { - log::warn!("Failed to render inline top toolbar: {}", err); - } - let _ = ctx.restore(); - for hit in &mut self.data.inline_top_hits { - hit.rect.0 = hit.rect.0 * ui_scale + top_offset.0; - hit.rect.1 = hit.rect.1 * ui_scale + top_offset.1; - hit.rect.2 *= ui_scale; - hit.rect.3 *= ui_scale; - } - self.data.inline_top_rect = Some(( - top_offset.0, - top_offset.1, - top_size.0 as f64, - top_size.1 as f64, - )); - crate::backend::wayland::toolbar::hit::clip_hit_regions_to_bounds( - &mut self.data.inline_top_hits, - 0, - self.data.inline_top_rect.expect("top rect was just set"), - ); - } else { - self.data.inline_top_rect = None; + let top_size = top_size(snapshot); + let top_base_w = top_size.0 as f64 / ui_scale; + let top_base_h = top_size.1 as f64 / ui_scale; + let top_hover_local = self + .data + .inline_top_hover + .or(focus_hover) + .map(|(x, y)| (x - top_offset.0, y - top_offset.1)) + .map(|(x, y)| (x / ui_scale, y / ui_scale)); + let _ = ctx.save(); + ctx.translate(top_offset.0, top_offset.1); + if (ui_scale - 1.0).abs() > f64::EPSILON { + ctx.scale(ui_scale, ui_scale); } - - // Side toolbar - if side_visible { - let side_size = side_size(snapshot); - let side_base_w = side_size.0 as f64 / ui_scale; - let side_base_h = side_size.1 as f64 / ui_scale; - let side_hover_local = self - .data - .inline_side_hover - .or(side_focus_hover) - .map(|(x, y)| (x - side_offset.0, y - side_offset.1)) - .map(|(x, y)| (x / ui_scale, y / ui_scale)); - let _ = ctx.save(); - ctx.translate(side_offset.0, side_offset.1); - if (ui_scale - 1.0).abs() > f64::EPSILON { - ctx.scale(ui_scale, ui_scale); - } - if let Err(err) = render_side_palette( - ctx, - side_base_w, - side_base_h, - snapshot, - &mut self.data.inline_side_hits, - side_hover_local, - self.data.inline_side_hover_start, - ) { - log::warn!("Failed to render inline side toolbar: {}", err); - } - let _ = ctx.restore(); - for hit in &mut self.data.inline_side_hits { - hit.rect.0 = hit.rect.0 * ui_scale + side_offset.0; - hit.rect.1 = hit.rect.1 * ui_scale + side_offset.1; - hit.rect.2 *= ui_scale; - hit.rect.3 *= ui_scale; - } - self.data.inline_side_rect = Some(( - side_offset.0, - side_offset.1, - side_size.0 as f64, - side_size.1 as f64, - )); - crate::backend::wayland::toolbar::hit::clip_hit_regions_to_bounds( - &mut self.data.inline_side_hits, - 0, - self.data.inline_side_rect.expect("side rect was just set"), - ); - } else { - self.data.inline_side_rect = None; + if let Err(err) = render_top_strip( + ctx, + top_base_w, + top_base_h, + snapshot, + &mut self.data.inline_top_hits, + top_hover_local, + self.data.inline_top_hover_start, + ) { + log::warn!("Failed to render inline top toolbar: {}", err); } + let _ = ctx.restore(); + for hit in &mut self.data.inline_top_hits { + hit.rect.0 = hit.rect.0 * ui_scale + top_offset.0; + hit.rect.1 = hit.rect.1 * ui_scale + top_offset.1; + hit.rect.2 *= ui_scale; + hit.rect.3 *= ui_scale; + } + self.data.inline_top_rect = Some(( + top_offset.0, + top_offset.1, + top_size.0 as f64, + top_size.1 as f64, + )); + crate::backend::wayland::toolbar::hit::clip_hit_regions_to_bounds( + &mut self.data.inline_top_hits, + 0, + self.data.inline_top_rect.expect("top rect was just set"), + ); } } diff --git a/src/backend/wayland/state/toolbar/scroll.rs b/src/backend/wayland/state/toolbar/scroll.rs new file mode 100644 index 00000000..8a154afc --- /dev/null +++ b/src/backend/wayland/state/toolbar/scroll.rs @@ -0,0 +1,56 @@ +use wayland_client::protocol::wl_surface; + +use super::*; +use crate::backend::wayland::toolbar::layout::top_popover_scroll_bounds; + +/// Wheel step for the top-strip Canvas/Session/Settings popovers, in pre-scale +/// spec units. +const WHEEL_SCROLL_STEP: f64 = 48.0; + +impl WaylandState { + /// True when a wheel event landed on the top strip: the pointer is over + /// the top toolbar surface, or over the inline top strip. With a + /// Canvas/Session/Settings popover open the wheel scrolls its content (the + /// GTK popovers scroll via their `ScrolledWindow`). + pub(in crate::backend::wayland) fn wheel_over_top_toolbar( + &self, + surface: &wl_surface::WlSurface, + position: (f64, f64), + ) -> bool { + if self.toolbar.is_focusable_surface(surface) { + return true; + } + self.inline_toolbars_active() + && self.data.inline_top_rect.is_some_and(|(x, y, w, h)| { + geometry::point_in_rect(position.0, position.1, x, y, w, h) + }) + } + + /// Scrolls the open Canvas/Session/Settings popover by wheel notches when its + /// content overflows the capped viewport. Returns true when the scroll + /// offset changed. `ScrollTopPopover` is on the popovers' spared-event + /// list, so routing it never dismisses them. + pub(in crate::backend::wayland) fn scroll_top_popover_by_wheel( + &mut self, + scroll_direction: i32, + ) -> bool { + if scroll_direction == 0 { + return false; + } + let snapshot = self.toolbar_snapshot(); + let Some((natural, viewport)) = top_popover_scroll_bounds(&snapshot) else { + return false; + }; + let max_scroll = (natural - viewport).max(0.0); + if max_scroll <= 0.0 { + return false; + } + let next = (snapshot.top_popover_scroll + scroll_direction as f64 * WHEEL_SCROLL_STEP) + .clamp(0.0, max_scroll); + if (next - snapshot.top_popover_scroll).abs() < 0.5 { + return false; + } + self.handle_toolbar_event(ToolbarEvent::ScrollTopPopover(next), None, None); + true + } +} diff --git a/src/backend/wayland/state/toolbar/side_pane.rs b/src/backend/wayland/state/toolbar/side_pane.rs deleted file mode 100644 index 563fa075..00000000 --- a/src/backend/wayland/state/toolbar/side_pane.rs +++ /dev/null @@ -1,113 +0,0 @@ -use wayland_client::protocol::wl_surface; - -use super::*; -use crate::backend::wayland::toolbar::ToolbarFocusTarget; -use crate::backend::wayland::toolbar::layout::{side_scroll_bounds, top_popover_scroll_bounds}; - -/// Wheel step for the side palette and the top-strip Canvas/Session/Settings -/// popovers, in pre-scale spec units. -const WHEEL_SCROLL_STEP: f64 = 48.0; - -impl WaylandState { - /// True when a wheel event should scroll the side palette: the pointer - /// is over the side toolbar surface, or over the inline side palette. - pub(in crate::backend::wayland) fn wheel_over_side_toolbar( - &self, - surface: &wl_surface::WlSurface, - position: (f64, f64), - ) -> bool { - if self.toolbar.focus_target_for_surface(surface) == Some(ToolbarFocusTarget::Side) { - return true; - } - self.inline_toolbars_active() - && self.data.inline_side_rect.is_some_and(|(x, y, w, h)| { - geometry::point_in_rect(position.0, position.1, x, y, w, h) - }) - } - - /// True when a wheel event landed on the top strip: the pointer is over - /// the top toolbar surface, or over the inline top strip. With a - /// Canvas/Session/Settings popover open the wheel scrolls its content (the - /// re-hosted side panes were wheel-scrollable, and the GTK popovers scroll - /// via their `ScrolledWindow`). - pub(in crate::backend::wayland) fn wheel_over_top_toolbar( - &self, - surface: &wl_surface::WlSurface, - position: (f64, f64), - ) -> bool { - if self.toolbar.focus_target_for_surface(surface) == Some(ToolbarFocusTarget::Top) { - return true; - } - self.inline_toolbars_active() - && self.data.inline_top_rect.is_some_and(|(x, y, w, h)| { - geometry::point_in_rect(position.0, position.1, x, y, w, h) - }) - } - - /// Scrolls the active side pane by wheel notches when it overflows its - /// viewport. Returns true when the scroll offset changed. - pub(in crate::backend::wayland) fn scroll_side_pane_by_wheel( - &mut self, - scroll_direction: i32, - ) -> bool { - if scroll_direction == 0 { - return false; - } - let snapshot = self.toolbar_snapshot(); - if snapshot.side_minimized { - return false; - } - let (natural, viewport) = side_scroll_bounds(&snapshot); - let max_scroll = (natural - viewport).max(0.0); - if max_scroll <= 0.0 { - return false; - } - let next = (snapshot.side_scroll + scroll_direction as f64 * WHEEL_SCROLL_STEP) - .clamp(0.0, max_scroll); - if (next - snapshot.side_scroll).abs() < 0.5 { - return false; - } - self.handle_toolbar_event(ToolbarEvent::ScrollSidePane(next), None, None); - true - } - - /// Scrolls the open Canvas/Session/Settings popover by wheel notches when its - /// content overflows the capped viewport. Returns true when the scroll - /// offset changed. `ScrollTopPopover` is on the popovers' spared-event - /// list, so routing it never dismisses them. - pub(in crate::backend::wayland) fn scroll_top_popover_by_wheel( - &mut self, - scroll_direction: i32, - ) -> bool { - if scroll_direction == 0 { - return false; - } - let snapshot = self.toolbar_snapshot(); - let Some((natural, viewport)) = top_popover_scroll_bounds(&snapshot) else { - return false; - }; - let max_scroll = (natural - viewport).max(0.0); - if max_scroll <= 0.0 { - return false; - } - let next = (snapshot.top_popover_scroll + scroll_direction as f64 * WHEEL_SCROLL_STEP) - .clamp(0.0, max_scroll); - if (next - snapshot.top_popover_scroll).abs() < 0.5 { - return false; - } - self.handle_toolbar_event(ToolbarEvent::ScrollTopPopover(next), None, None); - true - } - - /// Drops keyboard focus on the side toolbar. Pane switches rebuild the - /// hit list, so a stale focus index would land on an unrelated widget; - /// the next Tab re-seeds focus at the first focusable region. - pub(in crate::backend::wayland) fn reset_side_toolbar_focus(&mut self) { - self.toolbar.clear_side_focus(); - let had_focus = self.data.inline_side_focus_index.take().is_some() - || self.data.inline_side_focus_id.take().is_some(); - if had_focus && self.inline_toolbars_active() { - self.input_state.needs_redraw = true; - } - } -} diff --git a/src/backend/wayland/state/toolbar/visibility/access.rs b/src/backend/wayland/state/toolbar/visibility/access.rs index 6fd2b9c3..1ac87185 100644 --- a/src/backend/wayland/state/toolbar/visibility/access.rs +++ b/src/backend/wayland/state/toolbar/visibility/access.rs @@ -1,5 +1,4 @@ use super::*; -use crate::backend::wayland::toolbar::ToolbarFocusTarget; use wayland_client::protocol::wl_surface; impl WaylandState { @@ -53,23 +52,9 @@ impl WaylandState { self.data.toolbar_top_offset_y } - pub(in crate::backend::wayland) fn toolbar_side_offset(&self) -> f64 { - self.data.toolbar_side_offset - } - - pub(in crate::backend::wayland) fn toolbar_side_offset_x(&self) -> f64 { - self.data.toolbar_side_offset_x - } - - pub(in crate::backend::wayland) fn restore_toolbar_offsets( - &mut self, - top: (f64, f64), - side: (f64, f64), - ) { + pub(in crate::backend::wayland) fn restore_toolbar_offsets(&mut self, top: (f64, f64)) { self.data.toolbar_top_offset = top.0; self.data.toolbar_top_offset_y = top.1; - self.data.toolbar_side_offset_x = side.0; - self.data.toolbar_side_offset = side.1; } pub(in crate::backend::wayland) fn inline_toolbars_active(&self) -> bool { @@ -87,12 +72,10 @@ impl WaylandState { surface: &wl_surface::WlSurface, position: (f64, f64), ) -> Option<(f64, f64)> { - let target = self.toolbar.focus_target_for_surface(surface)?; - let kind = match target { - ToolbarFocusTarget::Top => MoveDragKind::Top, - ToolbarFocusTarget::Side => MoveDragKind::Side, - }; - Some(self.local_to_screen_coords(kind, position)) + if !self.toolbar.is_focusable_surface(surface) { + return None; + } + Some(self.local_to_screen_coords(MoveDragKind::Top, position)) } pub(in crate::backend::wayland) fn set_suppress_next_release(&mut self, value: bool) { diff --git a/src/backend/wayland/state/toolbar/visibility/sync.rs b/src/backend/wayland/state/toolbar/visibility/sync.rs index 65fd6602..59f3070d 100644 --- a/src/backend/wayland/state/toolbar/visibility/sync.rs +++ b/src/backend/wayland/state/toolbar/visibility/sync.rs @@ -2,17 +2,14 @@ use super::*; use crate::env_vars::{XDG_CURRENT_DESKTOP_ENV, XDG_SESSION_DESKTOP_ENV}; fn toolbar_visibility_for_frontend( - requested: (bool, bool), + requested: bool, gtk_active: bool, gtk_drag_preview: Option, -) -> (bool, bool) { +) -> bool { if !gtk_active { return requested; } - ( - requested.0 && gtk_drag_preview == Some(crate::toolbar_gtk::GtkToolbarKind::Top), - requested.1 && gtk_drag_preview == Some(crate::toolbar_gtk::GtkToolbarKind::Side), - ) + requested && gtk_drag_preview == Some(crate::toolbar_gtk::GtkToolbarKind::Top) } impl WaylandState { @@ -27,9 +24,7 @@ impl WaylandState { // that honor exclusivity (Hyprland) lock all input to the canvas // and the bars become click-through. let toolbar_visible = self.toolbar.is_visible() - || (self.gtk_toolbars_active() - && (self.input_state.toolbar_top_visible() - || self.input_state.toolbar_side_visible())); + || (self.gtk_toolbars_active() && self.input_state.toolbar_top_visible()); desired_keyboard_interactivity_for( self.layer_shell.is_some(), toolbar_visible, @@ -82,11 +77,8 @@ impl WaylandState { // Sync individual toolbar visibility. While the GTK frontend owns // the toolbars, the built-in surfaces stay unmapped. let gtk_active = self.gtk_toolbars_active(); - let (top_visible, side_visible) = toolbar_visibility_for_frontend( - ( - self.input_state.toolbar_top_visible(), - self.input_state.toolbar_side_visible(), - ), + let top_visible = toolbar_visibility_for_frontend( + self.input_state.toolbar_top_visible(), gtk_active, self.data.gtk_drag_preview, ); @@ -99,12 +91,6 @@ impl WaylandState { self.input_state.needs_redraw = true; } - if side_visible != self.toolbar.is_side_visible() { - self.toolbar.set_side_visible(side_visible); - self.input_state.needs_redraw = true; - drag_log(|| format!("toolbar visibility change: side -> {}", side_visible)); - } - let any_visible = self.toolbar.is_visible(); if !any_visible { self.set_pointer_over_toolbar(false); @@ -115,23 +101,19 @@ impl WaylandState { if any_visible { log::debug!( - "Toolbar visibility sync: top_visible={}, side_visible={}, layer_shell_available={}, inline_active={}, top_created={}, side_created={}, needs_recreate={}, scale={}", + "Toolbar visibility sync: top_visible={}, layer_shell_available={}, inline_active={}, top_created={}, needs_recreate={}, scale={}", top_visible, - side_visible, self.layer_shell.is_some(), inline_active, self.toolbar.top_created(), - self.toolbar.side_created(), self.toolbar_needs_recreate(), self.surface.scale() ); drag_log(|| { format!( - "toolbar sync: top_offset=({}, {}), side_offset=({}, {}), inline_active={}, layer_shell={}, needs_recreate={}", + "toolbar sync: top_offset=({}, {}), inline_active={}, layer_shell={}, needs_recreate={}", self.data.toolbar_top_offset, self.data.toolbar_top_offset_y, - self.data.toolbar_side_offset, - self.data.toolbar_side_offset_x, inline_active, self.layer_shell.is_some(), self.toolbar_needs_recreate() @@ -147,7 +129,7 @@ impl WaylandState { if any_visible && inline_active { // If we forced inline while layer surfaces already existed, tear them down to avoid // focus/input conflicts on compositors that support layer-shell. - if self.toolbar.top_created() || self.toolbar.side_created() { + if self.toolbar.top_created() { self.toolbar.destroy_all(); self.set_toolbar_needs_recreate(true); self.reset_toolbar_margin_cache(); @@ -158,22 +140,17 @@ impl WaylandState { if any_visible && self.layer_shell.is_some() && !inline_active && !drag_preview { // Detect compositors ignoring or failing to configure toolbar layer surfaces; if they // never configure after repeated attempts, fall back to inline toolbars automatically. - let (top_configured, side_configured) = self.toolbar.configured_states(); + let top_configured = self.toolbar.top_configured(); let expected_top = self.toolbar.is_top_visible(); - let expected_side = self.toolbar.is_side_visible(); - if (expected_top && !top_configured) || (expected_side && !side_configured) { + if expected_top && !top_configured { self.data.toolbar_configure_miss_count = self.data.toolbar_configure_miss_count.saturating_add(1); if debug_toolbar_drag_logging_enabled() && self.data.toolbar_configure_miss_count.is_multiple_of(60) { debug!( - "Toolbar configure pending: count={}, expected_top={}, configured_top={}, expected_side={}, configured_side={}", - self.data.toolbar_configure_miss_count, - expected_top, - top_configured, - expected_side, - side_configured + "Toolbar configure pending: count={}, expected_top={}, configured_top={}", + self.data.toolbar_configure_miss_count, expected_top, top_configured ); } } else { @@ -182,7 +159,7 @@ impl WaylandState { if self.data.toolbar_configure_miss_count > Self::TOOLBAR_CONFIGURE_FAIL_THRESHOLD { warn!( - "Toolbar layer surfaces did not configure after {} frames; falling back to inline toolbars", + "Toolbar layer surface did not configure after {} frames; falling back to the inline toolbar", self.data.toolbar_configure_miss_count ); self.toolbar.destroy_all(); @@ -260,8 +237,6 @@ impl WaylandState { fn reset_toolbar_margin_cache(&mut self) { self.data.last_applied_top_margin = None; self.data.last_applied_top_margin_top = None; - self.data.last_applied_side_margin = None; - self.data.last_applied_side_margin_left = None; } } @@ -272,25 +247,16 @@ mod tests { #[test] fn gtk_frontend_maps_only_the_inline_preview_target() { - assert_eq!( - toolbar_visibility_for_frontend((true, true), true, None), - (false, false) - ); - assert_eq!( - toolbar_visibility_for_frontend((true, true), true, Some(GtkToolbarKind::Top)), - (true, false) - ); - assert_eq!( - toolbar_visibility_for_frontend((true, true), true, Some(GtkToolbarKind::Side)), - (false, true) - ); + assert!(!toolbar_visibility_for_frontend(true, true, None)); + assert!(toolbar_visibility_for_frontend( + true, + true, + Some(GtkToolbarKind::Top) + )); } #[test] fn builtin_frontend_ignores_gtk_preview_state() { - assert_eq!( - toolbar_visibility_for_frontend((true, false), false, Some(GtkToolbarKind::Side)), - (true, false) - ); + assert!(toolbar_visibility_for_frontend(true, false, None)); } } diff --git a/src/backend/wayland/toolbar/AGENTS.md b/src/backend/wayland/toolbar/AGENTS.md index 3fae143c..c580495f 100644 --- a/src/backend/wayland/toolbar/AGENTS.md +++ b/src/backend/wayland/toolbar/AGENTS.md @@ -5,24 +5,25 @@ ## Architecture - `view/` is the declarative engine: `view/top.rs` builds the top strip as a typed `WidgetTree` of `WidgetNode`s (pure function of the `ToolbarSnapshot`); `view/popover.rs` places anchored popovers (shapes/options, overflow); `view/node.rs`/`view/tree.rs` define the node model and the legacy `HitRegion` adapter. -- `render/paint.rs` is the single painter over the tree; `render/side_palette/` still hand-renders the side palette pane-by-pane (Draw/Canvas/Session/Settings) and pushes hit regions while drawing. +- `render/paint.rs` is the single painter over the tree. There is no side-palette renderer; Canvas/Session/Settings live in top popovers built from the same models. - `layout/spec/` holds size constants and the surface-size math. The top strip's width and extra height come from the same tree walk the builder performs (`view::top::top_natural_width`/`top_extra_height`), so size and content cannot drift. -- `surfaces/` owns Wayland surface lifecycle (resize via `set_size`, no destroy/recreate), buffers, input regions (`sync_top_input_region` keeps popover-grown surfaces click-through outside bar + panels), hit testing, and surface rendering. +- `surfaces/` owns Wayland surface lifecycle (resize via `set_size`, no destroy/recreate), buffers, input regions (`sync_top_input_region` keeps popover-grown surfaces click-through outside bar + panels), hit testing, and surface rendering. Layer-shell owns one auxiliary top surface; inline owns none. - `main/` owns runtime toolbar state and lifecycle. - `events.rs`, `hit.rs`, and `rows.rs` connect runtime events, hit targets, and row helpers. `hit.rs` owns the 24px minimum-target inflation both paths share. ## Invariants -- Every rect exists once: the tree (top) or the render pass (side) is the only geometry source; no payload rects, no post-render coordinate fixups beyond the one uniform scale. +- Every rect exists once: the tree is the only geometry source; no payload rects, no post-render coordinate fixups beyond the one uniform scale. - Blue is reserved for the active tool/selected value; destructive actions are red and isolated; disabled buttons are dimmed and non-interactive. -- Section visibility resolves through `config::resolve_section_visibility` (explicit `items.shown`/`items.hidden` overrides over the layout-mode baseline); the nine `show_*` booleans are derived mirrors — never write them directly. +- Section visibility resolves through `config::resolve_section_visibility` (explicit `items.shown`/`items.hidden` overrides over the layout-mode baseline); the `show_*` booleans are derived mirrors — never write them directly. - Minimize leaves a restore tab; the restore controls are not customization item ids and must never become hideable. - Colors live only in the contextual style pill; the top strip's islands read tools | presets | history | chrome. Width pressure drops the non-essential presets island first, then narrows the pill's swatches, then moves droppable tools/utilities into the overflow menu; Pen, Eraser, Undo/Redo, Clear, and chrome are never dropped. -- Config keys and item ids are additive-only; unknown ids round-trip through saves. +- Config keys and item ids are additive-only; unknown ids round-trip through saves. Historically named `side.*` ids that still customize top controls must not be renamed casually. +- **Exception (top-only toolbar retirement):** panel-era keys removed from the typed toolbar model are listed as `RetiredSetting` paths in `src/config/document.rs` and stay in authored `config.toml` on unrelated saves. For `runtime-ui.toml`, recognized maps (`item_order`, hidden/shown-style ID maps) still prune unknown and retired entries on rewrite — including retired order groups such as `side_sections` and the former panel lists — rather than passthrough. That is intentional and sacrifices downgrade round-trip of those runtime keys. ## Coupled Changes - Runtime toolbar changes may affect `src/backend/wayland/state/toolbar.rs`, `src/backend/wayland/state/toolbar/`, `src/ui/toolbar/`, `src/input/`, toolbar icons, config toolbar settings, and action metadata. - New config keys require coupled updates to `config.example.toml`, `docs/CONFIG.md`, and the configurator. ## Validation -- Add or update tests under `layout/tests/` (render-oracle hits for the side palette) and `view/top.rs` (tree assertions) for geometry or content changes. +- Add or update tests under `view/top.rs` (tree assertions) for geometry or content changes. - Run full local CI (`./tools/lint-and-test.sh`) for broad toolbar behavior changes. diff --git a/src/backend/wayland/toolbar/events.rs b/src/backend/wayland/toolbar/events.rs index a742d9dc..4ab0d3a6 100644 --- a/src/backend/wayland/toolbar/events.rs +++ b/src/backend/wayland/toolbar/events.rs @@ -13,26 +13,11 @@ pub enum HitKind { max: f64, }, DragSetFontSize, - /// 2-D saturation/value area; the fixed hue rides along so the drag - /// handler can rebuild the full color from the pointer position and the - /// hit rect alone (payload rects are banned — the rect IS the region). - PickSatVal { - hue: f64, - }, - /// Hue bar; the fixed saturation/value ride along like PickSatVal's hue. - PickHue { - sat: f64, - val: f64, - }, DragUndoDelay, DragRedoDelay, DragCustomUndoDelay, DragCustomRedoDelay, DragMoveTop, - DragMoveSide, - DragScrollSide { - max_scroll: f64, - }, /// Internal scrollbar of the top strip's Canvas/Session/Settings popover. DragScrollTopPopover { max_scroll: f64, @@ -53,8 +38,6 @@ pub enum ToolbarCursorHint { Pointer, /// Grab cursor for sliders and drag handles. Grab, - /// Crosshair for color pickers. - Crosshair, } impl HitKind { @@ -70,11 +53,8 @@ impl HitKind { | HitKind::DragCustomUndoDelay | HitKind::DragCustomRedoDelay | HitKind::DragMoveTop - | HitKind::DragMoveSide - | HitKind::DragScrollSide { .. } | HitKind::DragScrollTopPopover { .. } | HitKind::DragToolbarItem { .. } => ToolbarCursorHint::Grab, - HitKind::PickSatVal { .. } | HitKind::PickHue { .. } => ToolbarCursorHint::Crosshair, } } } diff --git a/src/backend/wayland/toolbar/hit.rs b/src/backend/wayland/toolbar/hit.rs index e8eb53ca..c1b6d9cc 100644 --- a/src/backend/wayland/toolbar/hit.rs +++ b/src/backend/wayland/toolbar/hit.rs @@ -1,4 +1,3 @@ -use crate::backend::wayland::state::{color_log, debug_toolbar_color_logging_enabled}; use crate::backend::wayland::toolbar::events::HitKind; use crate::backend::wayland::toolbar_intent::ToolbarIntent; use crate::ui::toolbar::ToolbarEvent; @@ -13,8 +12,8 @@ pub const MIN_HIT_TARGET: f64 = 24.0; #[derive(Clone, Debug)] pub struct HitRegion { - /// Stable tree identity for keyboard focus. Hand-rendered side hits do - /// not have one yet and fall back to their visual-order index. + /// Stable tree identity for keyboard focus. Non-focusable hits leave this + /// unset and fall back to their visual-order index. pub focus_id: Option, pub rect: (f64, f64, f64, f64), // x, y, w, h pub event: ToolbarEvent, @@ -154,8 +153,6 @@ fn event_for_hit(hit: &HitRegion, x: f64, y: f64, phase: HitPhase) -> Option sat_val_event_for_hit(hue, hit, x, y), - PickHue { sat, val } => hue_event_for_hit(sat, val, hit, x), DragUndoDelay => slider_event_for_hit( ToolbarSliderTarget::UndoDelay, ToolbarSliderSpec::DELAY_SECONDS, @@ -181,8 +178,6 @@ fn event_for_hit(hit: &HitRegion, x: f64, y: f64, phase: HitPhase) -> Option ToolbarEvent::MoveTopToolbar { x, y }, - DragMoveSide => ToolbarEvent::MoveSideToolbar { x, y }, - DragScrollSide { max_scroll } => scroll_event_for_hit(max_scroll, hit, y), DragScrollTopPopover { max_scroll } => popover_scroll_event_for_hit(max_scroll, hit, y), // Phase-sensitive: the press opens the gesture, motion reports where // it is now. @@ -238,44 +233,8 @@ pub fn drag_intent_for_hit(hit: &HitRegion, x: f64, y: f64) -> Option ToolbarEvent { - let s = ((x - hit.rect.0) / hit.rect.2.max(1.0)).clamp(0.0, 1.0); - let v = (1.0 - (y - hit.rect.1) / hit.rect.3.max(1.0)).clamp(0.0, 1.0); - if debug_toolbar_color_logging_enabled() { - color_log(|| { - format!( - "toolbar pick sat/val: pos=({x:.1},{y:.1}) rect={:?} h={hue:.3} s={s:.3} v={v:.3}", - hit.rect - ) - }); - } - ToolbarEvent::SetColorHsv { h: hue, s, v } -} - -/// Map a pointer x inside the hue bar to a full HSV color; hue follows x, -/// saturation and value are fixed. -fn hue_event_for_hit(sat: f64, val: f64, hit: &HitRegion, x: f64) -> ToolbarEvent { - let h = ((x - hit.rect.0) / hit.rect.2.max(1.0)).clamp(0.0, 1.0); - if debug_toolbar_color_logging_enabled() { - color_log(|| { - format!( - "toolbar pick hue: x={x:.1} rect={:?} h={h:.3} s={sat:.3} v={val:.3}", - hit.rect - ) - }); - } - ToolbarEvent::SetColorHsv { h, s: sat, v: val } -} - -/// Map a pointer y within the scrollbar track to an absolute scroll offset. -fn scroll_event_for_hit(max_scroll: f64, hit: &HitRegion, pointer_y: f64) -> ToolbarEvent { - ToolbarEvent::ScrollSidePane(scroll_offset_for_hit(max_scroll, hit, pointer_y)) -} - -/// The popover scrollbar shares the side-scrollbar pointer mapping but -/// targets the popovers' own scroll offset. +/// Map a pointer y within the popover scrollbar track to an absolute scroll +/// offset. fn popover_scroll_event_for_hit(max_scroll: f64, hit: &HitRegion, pointer_y: f64) -> ToolbarEvent { ToolbarEvent::ScrollTopPopover(scroll_offset_for_hit(max_scroll, hit, pointer_y)) } @@ -567,73 +526,6 @@ mod tests { assert!(drag_intent_for_hit(&hit, 301.0, 10.0).is_none()); } - #[test] - fn sat_val_hit_maps_pointer_to_full_hsv_color() { - let hit = HitRegion { - focus_id: None, - rect: (100.0, 50.0, 200.0, 80.0), - event: ToolbarEvent::SetColorHsv { - h: 0.5, - s: 0.5, - v: 0.5, - }, - kind: HitKind::PickSatVal { hue: 0.5 }, - tooltip: None, - }; - - let (intent, start_drag) = intent_for_hit(&hit, 200.0, 90.0).expect("press intent"); - assert!(start_drag); - match intent.0 { - ToolbarEvent::SetColorHsv { h, s, v } => { - assert!((h - 0.5).abs() < 1e-9); - assert!((s - 0.5).abs() < 1e-9); - assert!((v - 0.5).abs() < 1e-9); - } - other => panic!("unexpected event: {other:?}"), - } - - // The top-right corner is full saturation and value; the mapping - // derives from the hit rect itself, not an embedded payload rect. - let drag = drag_intent_for_hit(&hit, 300.0, 50.0).expect("drag intent"); - match drag.0 { - ToolbarEvent::SetColorHsv { h, s, v } => { - assert!((h - 0.5).abs() < 1e-9); - assert!((s - 1.0).abs() < 1e-9); - assert!((v - 1.0).abs() < 1e-9); - } - other => panic!("unexpected event: {other:?}"), - } - } - - #[test] - fn hue_hit_maps_pointer_x_and_keeps_sat_val() { - let hit = HitRegion { - focus_id: None, - rect: (100.0, 150.0, 200.0, 14.0), - event: ToolbarEvent::SetColorHsv { - h: 0.0, - s: 0.25, - v: 0.75, - }, - kind: HitKind::PickHue { - sat: 0.25, - val: 0.75, - }, - tooltip: None, - }; - - let (intent, start_drag) = intent_for_hit(&hit, 150.0, 157.0).expect("press intent"); - assert!(start_drag); - match intent.0 { - ToolbarEvent::SetColorHsv { h, s, v } => { - assert!((h - 0.25).abs() < 1e-9); - assert!((s - 0.25).abs() < 1e-9); - assert!((v - 0.75).abs() < 1e-9); - } - other => panic!("unexpected event: {other:?}"), - } - } - #[test] fn secondary_press_resolves_only_quick_color_swatches() { let swatch = click(ToolbarEvent::SetQuickColor { diff --git a/src/backend/wayland/toolbar/layout/mod.rs b/src/backend/wayland/toolbar/layout/mod.rs index aaa6104a..5f79582c 100644 --- a/src/backend/wayland/toolbar/layout/mod.rs +++ b/src/backend/wayland/toolbar/layout/mod.rs @@ -13,21 +13,6 @@ pub fn top_size(snapshot: &ToolbarSnapshot) -> (u32, u32) { scale_size(base, snapshot.toolbar_scale) } -/// Compute the target logical size for the side toolbar given snapshot state. -pub fn side_size(snapshot: &ToolbarSnapshot) -> (u32, u32) { - let base = ToolbarLayoutSpec::new(snapshot).side_size(snapshot); - scale_size(base, snapshot.toolbar_scale) -} - -/// Scroll bounds for the side palette as (natural_height, viewport_height), -/// both in pre-scale spec units; max scroll = (natural - viewport).max(0). -pub fn side_scroll_bounds(snapshot: &ToolbarSnapshot) -> (f64, f64) { - let spec = ToolbarLayoutSpec::new(snapshot); - let natural = spec.side_natural_height(snapshot); - let (_, viewport) = spec.side_size(snapshot); - (natural, viewport as f64) -} - /// Scroll bounds for the open Canvas/Session/Settings popover on the top strip as /// (natural_height, viewport_height), both in pre-scale spec units; `None` /// while no menu popover is open. diff --git a/src/backend/wayland/toolbar/layout/spec/mod.rs b/src/backend/wayland/toolbar/layout/spec/mod.rs index 6e0ea9d6..3cf4af45 100644 --- a/src/backend/wayland/toolbar/layout/spec/mod.rs +++ b/src/backend/wayland/toolbar/layout/spec/mod.rs @@ -1,4 +1,3 @@ -mod side; mod top; use crate::ui::toolbar::ToolbarSnapshot; diff --git a/src/backend/wayland/toolbar/layout/spec/side/constants.rs b/src/backend/wayland/toolbar/layout/spec/side/constants.rs deleted file mode 100644 index 8dfed7d4..00000000 --- a/src/backend/wayland/toolbar/layout/spec/side/constants.rs +++ /dev/null @@ -1,106 +0,0 @@ -use super::super::ToolbarLayoutSpec; - -impl ToolbarLayoutSpec { - pub(in crate::backend::wayland::toolbar) const SIDE_WIDTH: u32 = 260; - /// Minimized side palette: the edge restore tab. - pub(in crate::backend::wayland::toolbar) const SIDE_MINIMIZED_SIZE: (u32, u32) = (24, 64); - - pub(in crate::backend::wayland::toolbar) const SIDE_START_X: f64 = 16.0; - pub(in crate::backend::wayland::toolbar) const SIDE_TOP_PADDING: f64 = 12.0; - - // Three-row header layout - pub(in crate::backend::wayland::toolbar) const SIDE_HEADER_ROW1_HEIGHT: f64 = 30.0; // Drag + pin/close - pub(in crate::backend::wayland::toolbar) const SIDE_PANE_NAV_HEIGHT: f64 = 26.0; // Pane navigation row - pub(in crate::backend::wayland::toolbar) const SIDE_PANE_NAV_GAP: f64 = 6.0; // Gap between header row and nav - pub(in crate::backend::wayland::toolbar) const SIDE_HEADER_BOTTOM_GAP: f64 = 8.0; - - // Drag handle (compact size like before) - pub(in crate::backend::wayland::toolbar) const SIDE_HEADER_DRAG_SIZE: f64 = 18.0; - - // Utility buttons (pin, more, close) - pub(in crate::backend::wayland::toolbar) const SIDE_HEADER_BUTTON_SIZE: f64 = 22.0; - pub(in crate::backend::wayland::toolbar) const SIDE_HEADER_BUTTON_MARGIN_RIGHT: f64 = 12.0; - pub(in crate::backend::wayland::toolbar) const SIDE_HEADER_BUTTON_GAP: f64 = 6.0; - - // Segmented control height - pub(in crate::backend::wayland::toolbar) const SIDE_SEGMENT_HEIGHT: f64 = 22.0; - - // Board chip (inline style) - pub(in crate::backend::wayland::toolbar) const SIDE_BOARD_CHIP_HEIGHT: f64 = 22.0; - pub(in crate::backend::wayland::toolbar) const SIDE_BOARD_CHEVRON_SIZE: f64 = 20.0; - pub(in crate::backend::wayland::toolbar) const SIDE_CONTENT_PADDING_X: f64 = 32.0; - pub(in crate::backend::wayland::toolbar) const SIDE_CARD_INSET: f64 = 6.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_PICKER_OFFSET_Y: f64 = 24.0; - pub(in crate::backend::wayland::toolbar) const SIDE_SLIDER_ROW_OFFSET: f64 = 26.0; - pub(in crate::backend::wayland::toolbar) const SIDE_NUDGE_SIZE: f64 = 24.0; - pub(in crate::backend::wayland::toolbar) const SIDE_ACTION_BUTTON_HEIGHT_ICON: f64 = 32.0; - pub(in crate::backend::wayland::toolbar) const SIDE_ACTION_BUTTON_HEIGHT_TEXT: f64 = 24.0; - pub(in crate::backend::wayland::toolbar) const SIDE_ACTION_BUTTON_GAP: f64 = 6.0; - pub(in crate::backend::wayland::toolbar) const SIDE_ACTION_CONTENT_GAP_TEXT: f64 = 5.0; - pub(in crate::backend::wayland::toolbar) const SIDE_ACTION_GROUP_LABEL_HEIGHT: f64 = 16.0; - - pub(in crate::backend::wayland::toolbar) const SIDE_SECTION_GAP: f64 = 12.0; - pub(in crate::backend::wayland::toolbar) const SIDE_SECTION_TOGGLE_OFFSET_Y: f64 = 22.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_SECTION_LABEL_HEIGHT: f64 = 28.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_SV_HEIGHT: f64 = 72.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_HUE_HEIGHT: f64 = 14.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_HUE_GAP: f64 = 6.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_PICKER_BLOCK_HEIGHT: f64 = - Self::SIDE_COLOR_SV_HEIGHT + Self::SIDE_COLOR_HUE_GAP + Self::SIDE_COLOR_HUE_HEIGHT; - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_SECTION_BOTTOM_PADDING: f64 = 8.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_SWATCH: f64 = 24.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_SWATCH_GAP: f64 = 6.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_SWATCHES_PER_ROW: usize = 6; - // Preview row constants (between gradient picker and swatches) - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_PREVIEW_SIZE: f64 = 28.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_PREVIEW_GAP_TOP: f64 = 10.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_PREVIEW_GAP_BOTTOM: f64 = 8.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_HEX_INPUT_HEIGHT: f64 = 20.0; - /// Wide enough for the nine-character `#RRGGBBAA` a translucent color - /// prints across supported Sans font metrics, alongside the copy icon. - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_HEX_INPUT_WIDTH: f64 = 96.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLOR_EXPAND_ICON_SIZE: f64 = 8.0; - pub(in crate::backend::wayland::toolbar) const SIDE_BOARD_COLOR_DOT_SIZE: f64 = 14.0; - pub(in crate::backend::wayland::toolbar) const SIDE_SECTION_LABEL_OFFSET_Y: f64 = 12.0; - pub(in crate::backend::wayland::toolbar) const SIDE_SECTION_LABEL_OFFSET_TALL: f64 = 14.0; - pub(in crate::backend::wayland::toolbar) const SIDE_FONT_BUTTON_HEIGHT: f64 = 24.0; - pub(in crate::backend::wayland::toolbar) const SIDE_FONT_BUTTON_GAP: f64 = 8.0; - pub(in crate::backend::wayland::toolbar) const SIDE_NUDGE_ICON_SIZE: f64 = 14.0; - pub(in crate::backend::wayland::toolbar) const SIDE_SLIDER_VALUE_WIDTH: f64 = 40.0; - pub(in crate::backend::wayland::toolbar) const SIDE_TRACK_HEIGHT: f64 = 8.0; - pub(in crate::backend::wayland::toolbar) const SIDE_TRACK_KNOB_RADIUS: f64 = 7.0; - pub(in crate::backend::wayland::toolbar) const SIDE_DELAY_SLIDER_HEIGHT: f64 = 6.0; - pub(in crate::backend::wayland::toolbar) const SIDE_DELAY_SLIDER_KNOB_RADIUS: f64 = 6.0; - pub(in crate::backend::wayland::toolbar) const SIDE_DELAY_SLIDER_HIT_PADDING: f64 = 4.0; - pub(in crate::backend::wayland::toolbar) const SIDE_DELAY_SLIDER_UNDO_OFFSET_Y: f64 = 16.0; - pub(in crate::backend::wayland::toolbar) const SIDE_DELAY_SLIDER_REDO_OFFSET_Y: f64 = 32.0; - pub(in crate::backend::wayland::toolbar) const SIDE_ACTION_ICON_SIZE: f64 = 18.0; - pub(in crate::backend::wayland::toolbar) const SIDE_STEP_SLIDER_TOP_PADDING: f64 = 4.0; - pub(in crate::backend::wayland::toolbar) const SIDE_SLIDER_CARD_HEIGHT: f64 = 52.0; - pub(in crate::backend::wayland::toolbar) const SIDE_ERASER_MODE_CARD_HEIGHT: f64 = 44.0; - pub(in crate::backend::wayland::toolbar) const SIDE_TOGGLE_CARD_HEIGHT: f64 = 44.0; - pub(in crate::backend::wayland::toolbar) const SIDE_TOGGLE_CARD_HEIGHT_WITH_RESET: f64 = - Self::SIDE_TOGGLE_CARD_HEIGHT + Self::SIDE_TOGGLE_HEIGHT + Self::SIDE_TOGGLE_GAP; - pub(in crate::backend::wayland::toolbar) const SIDE_FONT_CARD_HEIGHT: f64 = 50.0; - pub(in crate::backend::wayland::toolbar) const SIDE_DELAY_SECTION_HEIGHT: f64 = 55.0; - pub(in crate::backend::wayland::toolbar) const SIDE_TOGGLE_HEIGHT: f64 = 24.0; - pub(in crate::backend::wayland::toolbar) const SIDE_TOGGLE_GAP: f64 = 6.0; - pub(in crate::backend::wayland::toolbar) const SIDE_CUSTOM_SECTION_HEIGHT: f64 = 120.0; - pub(in crate::backend::wayland::toolbar) const SIDE_STEP_HEADER_HEIGHT: f64 = 20.0; - pub(in crate::backend::wayland::toolbar) const SIDE_PRESET_CARD_HEIGHT: f64 = 74.0; - pub(in crate::backend::wayland::toolbar) const SIDE_PRESET_SLOT_SIZE: f64 = 40.0; - pub(in crate::backend::wayland::toolbar) const SIDE_PRESET_SLOT_GAP: f64 = 8.0; - pub(in crate::backend::wayland::toolbar) const SIDE_PRESET_ROW_OFFSET_Y: f64 = 24.0; - pub(in crate::backend::wayland::toolbar) const SIDE_FOOTER_PADDING: f64 = 10.0; - pub(in crate::backend::wayland::toolbar) const SIDE_SETTINGS_BUTTON_HEIGHT: f64 = 24.0; - pub(in crate::backend::wayland::toolbar) const SIDE_SETTINGS_BUTTON_GAP: f64 = 6.0; - pub(in crate::backend::wayland::toolbar) const SIDE_SESSION_META_HEIGHT: f64 = 34.0; - pub(in crate::backend::wayland::toolbar) const SIDE_SESSION_CONFIRM_LABEL_HEIGHT: f64 = 17.0; - pub(in crate::backend::wayland::toolbar) const SIDE_SESSION_BUTTON_HEIGHT: f64 = 24.0; - pub(in crate::backend::wayland::toolbar) const SIDE_SESSION_RECENT_HEIGHT: f64 = 22.0; - pub(in crate::backend::wayland::toolbar) const SIDE_SESSION_ROW_GAP: f64 = 5.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLLAPSED_SECTION_HEIGHT: f64 = 30.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLLAPSE_HEADER_HIT_HEIGHT: f64 = - Self::SIDE_SECTION_TOGGLE_OFFSET_Y - 1.0; - pub(in crate::backend::wayland::toolbar) const SIDE_COLLAPSE_CHEVRON_SIZE: f64 = 14.0; -} diff --git a/src/backend/wayland/toolbar/layout/spec/side/mod.rs b/src/backend/wayland/toolbar/layout/spec/side/mod.rs deleted file mode 100644 index 281e6c4e..00000000 --- a/src/backend/wayland/toolbar/layout/spec/side/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod constants; -mod sizes; diff --git a/src/backend/wayland/toolbar/layout/spec/side/sizes.rs b/src/backend/wayland/toolbar/layout/spec/side/sizes.rs deleted file mode 100644 index fef78752..00000000 --- a/src/backend/wayland/toolbar/layout/spec/side/sizes.rs +++ /dev/null @@ -1,311 +0,0 @@ -use crate::ui::toolbar::model::{ - ToolbarActionsModel, ToolbarSessionModel, ToolbarSettingsModel, toolbar_boards_model, - toolbar_pages_model, -}; -use crate::ui::toolbar::snapshot::ToolContext; -use crate::ui::toolbar::{ToolbarSideSection, ToolbarSnapshot}; - -use super::super::ToolbarLayoutSpec; - -mod sections; - -impl ToolbarLayoutSpec { - pub(in crate::backend::wayland::toolbar) fn side_size( - &self, - snapshot: &ToolbarSnapshot, - ) -> (u32, u32) { - if snapshot.side_minimized { - return Self::SIDE_MINIMIZED_SIZE; - } - let natural = self.side_natural_height(snapshot); - let height = match snapshot.side_viewport_max { - Some(max) if max > 0.0 && natural > max => max, - _ => natural, - }; - (Self::SIDE_WIDTH, height.ceil() as u32) - } - - /// Unclamped content height of the active pane; when it exceeds the - /// viewport the pane scrolls instead of growing the surface. - pub(in crate::backend::wayland::toolbar) fn side_natural_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - let base_height = self.side_content_start_y(); - let tool_context = ToolContext::from_snapshot(snapshot); - - let show_actions = ToolbarActionsModel::from_snapshot(snapshot).is_some(); - let show_pages = toolbar_pages_model(snapshot).is_some(); - let show_boards = toolbar_boards_model(snapshot).is_some(); - let show_presets = !snapshot.side_section_hidden(ToolbarSideSection::Presets) - && snapshot.show_presets - && snapshot.preset_slot_count.min(snapshot.presets.len()) > 0; - let show_step_section = snapshot.show_step_section - && !snapshot.side_section_hidden(ToolbarSideSection::StepUndo); - let show_settings_section = ToolbarSettingsModel::from_snapshot(snapshot).is_some(); - let show_session_section = ToolbarSessionModel::from_snapshot(snapshot).is_some(); - - let mut height: f64 = base_height; - let add_section = |section_height: f64, height: &mut f64| { - if section_height > 0.0 { - *height += section_height + Self::SIDE_SECTION_GAP; - } - }; - - let mut thickness_block_added = false; - let mut text_block_added = false; - for section in crate::ui::toolbar::model::ordered_pane_sections(snapshot) { - match section { - ToolbarSideSection::Colors - if tool_context.needs_color - && !snapshot.side_section_hidden(ToolbarSideSection::Colors) => - { - add_section(self.side_colors_height(snapshot), &mut height); - } - ToolbarSideSection::Presets if show_presets => { - add_section(self.side_presets_height(snapshot), &mut height); - } - ToolbarSideSection::Thickness - | ToolbarSideSection::EraserMode - | ToolbarSideSection::PolygonSides - if tool_context.needs_thickness && !thickness_block_added => - { - thickness_block_added = true; - if !snapshot.side_section_hidden(ToolbarSideSection::Thickness) { - add_section(self.side_thickness_height(snapshot), &mut height); - } - if tool_context.show_eraser_mode - && !snapshot.side_section_hidden(ToolbarSideSection::EraserMode) - { - add_section(self.side_eraser_mode_height(snapshot), &mut height); - } - if tool_context.show_polygon_sides_control - && !snapshot.side_section_hidden(ToolbarSideSection::PolygonSides) - { - add_section(self.side_polygon_sides_height(snapshot), &mut height); - } - } - ToolbarSideSection::ArrowLabels - if tool_context.show_arrow_labels - && !snapshot.side_section_hidden(ToolbarSideSection::ArrowLabels) => - { - add_section(self.side_arrow_labels_height(snapshot), &mut height); - } - ToolbarSideSection::StepMarkers - if tool_context.show_step_counter - && !snapshot.side_section_hidden(ToolbarSideSection::StepMarkers) => - { - add_section(self.side_step_markers_height(snapshot), &mut height); - } - ToolbarSideSection::MarkerOpacity - if tool_context.show_marker_opacity - && !snapshot.side_section_hidden(ToolbarSideSection::MarkerOpacity) => - { - add_section(self.side_marker_opacity_height(snapshot), &mut height); - } - ToolbarSideSection::TextSize | ToolbarSideSection::Font - if tool_context.show_font_controls && !text_block_added => - { - text_block_added = true; - if !snapshot.side_section_hidden(ToolbarSideSection::TextSize) { - add_section(self.side_text_size_height(snapshot), &mut height); - } - if !snapshot.side_section_hidden(ToolbarSideSection::Font) { - add_section(self.side_font_height(snapshot), &mut height); - } - } - ToolbarSideSection::Actions if show_actions => { - add_section(self.side_actions_height(snapshot), &mut height); - } - ToolbarSideSection::Boards if show_boards => { - add_section(self.side_boards_height(snapshot), &mut height); - } - ToolbarSideSection::Pages if show_pages => { - add_section(self.side_pages_height(snapshot), &mut height); - } - ToolbarSideSection::StepUndo if show_step_section => { - add_section(self.side_step_height(snapshot), &mut height); - } - ToolbarSideSection::Session if show_session_section => { - add_section(self.side_session_height(snapshot), &mut height); - } - ToolbarSideSection::Settings if show_settings_section => { - add_section(self.side_settings_height(snapshot), &mut height); - } - _ => {} - } - } - - height + Self::SIDE_FOOTER_PADDING - } - - pub(in crate::backend::wayland::toolbar) fn side_content_width(&self, width: f64) -> f64 { - width - Self::SIDE_CONTENT_PADDING_X - } - - pub(in crate::backend::wayland::toolbar) fn side_colors_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - if snapshot.side_section_hidden(ToolbarSideSection::Colors) { - return 0.0; - } - if snapshot.side_section_collapsed(ToolbarSideSection::Colors) { - return Self::SIDE_COLLAPSED_SECTION_HEIGHT; - } - let visible_rows = if snapshot.show_more_colors { - let color_count = snapshot.quick_colors.rendered_len().max(1); - color_count.div_ceil(Self::SIDE_COLOR_SWATCHES_PER_ROW) - } else { - 1 - }; - let rows = visible_rows as f64; - let preview_row_h = Self::SIDE_COLOR_PREVIEW_SIZE - + Self::SIDE_COLOR_PREVIEW_GAP_TOP - + Self::SIDE_COLOR_PREVIEW_GAP_BOTTOM; - Self::SIDE_COLOR_SECTION_LABEL_HEIGHT - + Self::SIDE_COLOR_PICKER_BLOCK_HEIGHT - + preview_row_h - + Self::SIDE_COLOR_SECTION_BOTTOM_PADDING - + (Self::SIDE_COLOR_SWATCH + Self::SIDE_COLOR_SWATCH_GAP) * rows - } - - pub(in crate::backend::wayland::toolbar) fn side_presets_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - self.collapsible_section_height( - snapshot, - ToolbarSideSection::Presets, - Self::SIDE_PRESET_CARD_HEIGHT, - ) - } - - pub(in crate::backend::wayland::toolbar) fn side_thickness_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - self.collapsible_section_height( - snapshot, - ToolbarSideSection::Thickness, - Self::SIDE_SLIDER_CARD_HEIGHT, - ) - } - - pub(in crate::backend::wayland::toolbar) fn side_eraser_mode_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - self.collapsible_section_height( - snapshot, - ToolbarSideSection::EraserMode, - Self::SIDE_ERASER_MODE_CARD_HEIGHT, - ) - } - - pub(in crate::backend::wayland::toolbar) fn side_polygon_sides_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - self.collapsible_section_height( - snapshot, - ToolbarSideSection::PolygonSides, - Self::SIDE_SLIDER_CARD_HEIGHT, - ) - } - - pub(in crate::backend::wayland::toolbar) fn side_arrow_labels_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - let expanded = if snapshot.arrow_label_enabled { - Self::SIDE_TOGGLE_CARD_HEIGHT_WITH_RESET - } else { - Self::SIDE_TOGGLE_CARD_HEIGHT - }; - self.collapsible_section_height(snapshot, ToolbarSideSection::ArrowLabels, expanded) - } - - pub(in crate::backend::wayland::toolbar) fn side_step_markers_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - self.collapsible_section_height( - snapshot, - ToolbarSideSection::StepMarkers, - Self::SIDE_TOGGLE_CARD_HEIGHT_WITH_RESET, - ) - } - - pub(in crate::backend::wayland::toolbar) fn side_marker_opacity_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - self.collapsible_section_height( - snapshot, - ToolbarSideSection::MarkerOpacity, - Self::SIDE_SLIDER_CARD_HEIGHT, - ) - } - - pub(in crate::backend::wayland::toolbar) fn side_text_size_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - self.collapsible_section_height( - snapshot, - ToolbarSideSection::TextSize, - Self::SIDE_SLIDER_CARD_HEIGHT, - ) - } - - pub(in crate::backend::wayland::toolbar) fn side_font_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - self.collapsible_section_height( - snapshot, - ToolbarSideSection::Font, - Self::SIDE_FONT_CARD_HEIGHT, - ) - } - - /// Y position of the pane navigation row. - pub(in crate::backend::wayland::toolbar) fn side_pane_nav_y(&self) -> f64 { - Self::SIDE_TOP_PADDING + Self::SIDE_HEADER_ROW1_HEIGHT + Self::SIDE_PANE_NAV_GAP - } - - /// Y position where pane content starts (after header + nav rows). - /// = 12 + 30 + 6 + 26 + 8 = 82px - pub(in crate::backend::wayland::toolbar) fn side_content_start_y(&self) -> f64 { - Self::SIDE_TOP_PADDING - + Self::SIDE_HEADER_ROW1_HEIGHT - + Self::SIDE_PANE_NAV_GAP - + Self::SIDE_PANE_NAV_HEIGHT - + Self::SIDE_HEADER_BOTTOM_GAP - } - - pub(in crate::backend::wayland::toolbar) fn side_card_x(&self) -> f64 { - Self::SIDE_START_X - Self::SIDE_CARD_INSET - } - - pub(in crate::backend::wayland::toolbar) fn side_card_width(&self, width: f64) -> f64 { - width - 2.0 * Self::SIDE_START_X + Self::SIDE_CARD_INSET * 2.0 - } - - fn collapsible_section_height( - &self, - snapshot: &ToolbarSnapshot, - section: ToolbarSideSection, - expanded_height: f64, - ) -> f64 { - if snapshot.side_section_hidden(section) { - return 0.0; - } - if snapshot.side_section_collapsed(section) { - Self::SIDE_COLLAPSED_SECTION_HEIGHT - } else { - expanded_height - } - } -} diff --git a/src/backend/wayland/toolbar/layout/spec/side/sizes/sections.rs b/src/backend/wayland/toolbar/layout/spec/side/sizes/sections.rs deleted file mode 100644 index 90cda4ad..00000000 --- a/src/backend/wayland/toolbar/layout/spec/side/sizes/sections.rs +++ /dev/null @@ -1,279 +0,0 @@ -use crate::backend::wayland::toolbar::rows::capped_grid_columns; -use crate::ui::toolbar::model::{ - ToolbarActionsModel, ToolbarCommandGroupKind, ToolbarSessionModel, ToolbarSettingsModel, - ToolbarSettingsNotice, toolbar_boards_model, toolbar_pages_model, -}; -use crate::ui::toolbar::{ToolbarSideSection, ToolbarSnapshot}; -use crate::ui_text::{UiTextStyle, measure_text}; - -use super::super::super::ToolbarLayoutSpec; - -impl ToolbarLayoutSpec { - pub(in crate::backend::wayland::toolbar) fn side_actions_content_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - let Some(model) = ToolbarActionsModel::from_snapshot(snapshot) else { - return 0.0; - }; - - if self.use_icons { - let icon_btn = Self::SIDE_ACTION_BUTTON_HEIGHT_ICON; - let icon_gap = Self::SIDE_ACTION_BUTTON_GAP; - let mut height = 0.0; - let mut has_group = false; - for group in model.groups() { - if group.buttons.is_empty() { - continue; - } - if has_group { - height += icon_gap; - } - let columns = match group.kind { - ToolbarCommandGroupKind::History => group.buttons.len(), - ToolbarCommandGroupKind::Zoom | ToolbarCommandGroupKind::AdvancedActions => 5, - ToolbarCommandGroupKind::Pages | ToolbarCommandGroupKind::Boards => { - group.buttons.len() - } - }; - let rows = group.buttons.len().div_ceil(columns); - if group.kind.sub_label().is_some() { - height += Self::SIDE_ACTION_GROUP_LABEL_HEIGHT; - } - height += icon_btn * rows as f64 + icon_gap * (rows as f64 - 1.0); - has_group = true; - } - height - } else { - let action_h = Self::SIDE_ACTION_BUTTON_HEIGHT_TEXT; - let action_gap = Self::SIDE_ACTION_CONTENT_GAP_TEXT; - let mut height = 0.0; - let mut has_group = false; - for group in model.groups() { - if group.buttons.is_empty() { - continue; - } - if has_group { - height += Self::SIDE_ACTION_BUTTON_GAP; - } - let columns = match group.kind { - ToolbarCommandGroupKind::History => 1, - ToolbarCommandGroupKind::Zoom | ToolbarCommandGroupKind::AdvancedActions => 2, - ToolbarCommandGroupKind::Pages | ToolbarCommandGroupKind::Boards => { - group.buttons.len().max(1) - } - }; - let rows = group.buttons.len().div_ceil(columns); - if group.kind.sub_label().is_some() { - height += Self::SIDE_ACTION_GROUP_LABEL_HEIGHT; - } - height += action_h * rows as f64 + action_gap * (rows as f64 - 1.0); - has_group = true; - } - height - } - } - - pub(in crate::backend::wayland::toolbar) fn side_actions_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - if snapshot.side_section_collapsed(ToolbarSideSection::Actions) { - return Self::SIDE_COLLAPSED_SECTION_HEIGHT; - } - let content = self.side_actions_content_height(snapshot); - if content <= 0.0 { - 0.0 - } else { - Self::SIDE_SECTION_TOGGLE_OFFSET_Y + content + Self::SIDE_ACTION_BUTTON_GAP - } - } - - pub(in crate::backend::wayland::toolbar) fn side_pages_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - let Some(pages) = toolbar_pages_model(snapshot) else { - return 0.0; - }; - if snapshot.side_section_collapsed(ToolbarSideSection::Pages) { - return Self::SIDE_COLLAPSED_SECTION_HEIGHT; - } - let btn_h = if self.use_icons { - Self::SIDE_ACTION_BUTTON_HEIGHT_ICON - } else { - Self::SIDE_ACTION_BUTTON_HEIGHT_TEXT - }; - let columns = pages.buttons.len().max(1); - let rows = pages.buttons.len().div_ceil(columns); - Self::SIDE_SECTION_TOGGLE_OFFSET_Y - + btn_h * rows as f64 - + Self::SIDE_ACTION_BUTTON_GAP * rows as f64 - } - - pub(in crate::backend::wayland::toolbar) fn side_boards_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - let Some(boards) = toolbar_boards_model(snapshot) else { - return 0.0; - }; - if snapshot.side_section_collapsed(ToolbarSideSection::Boards) { - return Self::SIDE_COLLAPSED_SECTION_HEIGHT; - } - let btn_h = if self.use_icons { - Self::SIDE_ACTION_BUTTON_HEIGHT_ICON - } else { - Self::SIDE_ACTION_BUTTON_HEIGHT_TEXT - }; - let columns = capped_grid_columns(boards.buttons.len(), 5); - let rows = boards.buttons.len().div_ceil(columns); - Self::SIDE_SECTION_TOGGLE_OFFSET_Y - + btn_h * rows as f64 - + Self::SIDE_ACTION_BUTTON_GAP * rows as f64 - } - - pub(in crate::backend::wayland::toolbar) fn side_step_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - if snapshot.side_section_hidden(ToolbarSideSection::StepUndo) { - return 0.0; - } - if snapshot.side_section_collapsed(ToolbarSideSection::StepUndo) { - return Self::SIDE_COLLAPSED_SECTION_HEIGHT; - } - let delay_h = if snapshot.show_delay_sliders { - Self::SIDE_DELAY_SECTION_HEIGHT - } else { - 0.0 - }; - let toggles_h = Self::SIDE_TOGGLE_HEIGHT * 2.0 + Self::SIDE_TOGGLE_GAP; - Self::SIDE_STEP_HEADER_HEIGHT - + toggles_h - + if snapshot.custom_section_enabled { - Self::SIDE_CUSTOM_SECTION_HEIGHT - } else { - 0.0 - } - + delay_h - } - - pub(in crate::backend::wayland::toolbar) fn side_settings_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - let dedicated_panel = snapshot.customize_items_open; - if !dedicated_panel && snapshot.side_section_collapsed(ToolbarSideSection::Settings) { - return Self::SIDE_COLLAPSED_SECTION_HEIGHT; - } - let toggle_h = Self::SIDE_TOGGLE_HEIGHT; - let toggle_gap = Self::SIDE_TOGGLE_GAP; - let Some(settings) = ToolbarSettingsModel::from_snapshot(snapshot) else { - return 0.0; - }; - let rows = settings.toggle_rows().len(); - let toggle_rows_h = if rows > 0 { - toggle_h * rows as f64 + toggle_gap * (rows as f64 - 1.0) - } else { - 0.0 - }; - let button_rows = settings.buttons().len().div_ceil(2); - let buttons_h = if button_rows > 0 { - Self::SIDE_SETTINGS_BUTTON_HEIGHT * button_rows as f64 - + Self::SIDE_SETTINGS_BUTTON_GAP * (button_rows as f64 - 1.0) - } else { - 0.0 - }; - let content_width = self.side_content_width(Self::SIDE_WIDTH as f64); - let notices_h = settings - .notices() - .iter() - .map(|notice| self.side_settings_notice_height(notice, content_width) + toggle_gap) - .sum::(); - let group_rows = settings.groups().len().div_ceil(2); - let group_rows_h = if group_rows > 0 { - toggle_h - + toggle_gap - + Self::SIDE_SETTINGS_BUTTON_HEIGHT * group_rows as f64 - + Self::SIDE_SETTINGS_BUTTON_GAP * (group_rows as f64 - 1.0) - } else { - 0.0 - }; - let item_rows = settings.item_overrides().len(); - let item_rows_h = if item_rows > 0 { - toggle_h - + toggle_gap - + toggle_h * item_rows as f64 - + toggle_gap * (item_rows as f64 - 1.0) - } else { - 0.0 - }; - let customize_h = group_rows_h + item_rows_h; - let customize_gap = if customize_h > 0.0 { toggle_gap } else { 0.0 }; - // Interim Simple/Full layout-mode row at the top of the pane (only - // outside the customization sub-panel). - let mode_row_h = if dedicated_panel { - 0.0 - } else { - Self::SIDE_SEGMENT_HEIGHT + toggle_gap - }; - let content_h = mode_row_h - + toggle_rows_h - + toggle_gap - + notices_h - + buttons_h - + customize_gap - + customize_h; - Self::SIDE_SECTION_TOGGLE_OFFSET_Y + content_h + Self::SIDE_SETTINGS_BUTTON_GAP - } - - pub(in crate::backend::wayland::toolbar) fn side_settings_notice_height( - &self, - notice: &ToolbarSettingsNotice, - content_width: f64, - ) -> f64 { - let style = UiTextStyle { - family: crate::ui::theme::toolbar::FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Normal, - size: crate::ui::theme::toolbar::FONT_SIZE_SECONDARY, - }; - measure_text(style, notice.text.as_ref(), Some(content_width)) - .map(|extents| extents.height() + 4.0) - .unwrap_or(Self::SIDE_TOGGLE_HEIGHT) - .max(Self::SIDE_TOGGLE_HEIGHT) - } - - pub(in crate::backend::wayland::toolbar) fn side_session_height( - &self, - snapshot: &ToolbarSnapshot, - ) -> f64 { - let Some(session) = ToolbarSessionModel::from_snapshot(snapshot) else { - return 0.0; - }; - if snapshot.side_section_collapsed(ToolbarSideSection::Session) { - return Self::SIDE_COLLAPSED_SECTION_HEIGHT; - } - let button_rows = session.button_rows(); - let buttons_h = if button_rows == 0 { - 0.0 - } else { - Self::SIDE_SESSION_BUTTON_HEIGHT * button_rows as f64 - + Self::SIDE_SESSION_ROW_GAP * (button_rows as f64 - 1.0) - }; - let recents_h = if session.has_recent_sessions() { - Self::SIDE_SESSION_ROW_GAP - + session.recents.len() as f64 - * (Self::SIDE_SESSION_RECENT_HEIGHT + Self::SIDE_SESSION_ROW_GAP) - } else { - 0.0 - }; - Self::SIDE_SECTION_TOGGLE_OFFSET_Y - + Self::SIDE_SESSION_META_HEIGHT - + Self::SIDE_SESSION_ROW_GAP - + buttons_h - + recents_h - + Self::SIDE_SESSION_ROW_GAP - } -} diff --git a/src/backend/wayland/toolbar/layout/tests/collapsible.rs b/src/backend/wayland/toolbar/layout/tests/collapsible.rs deleted file mode 100644 index b52699be..00000000 --- a/src/backend/wayland/toolbar/layout/tests/collapsible.rs +++ /dev/null @@ -1,274 +0,0 @@ -use super::{ - ToolbarLayoutSpec, create_test_input_state, rendered_side_hits, side_size, snapshot_from_state, -}; -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::input::Tool; -use crate::ui::toolbar::{ - SessionRecentSnapshot, SidePane, ToolbarEvent, ToolbarSideSection, ToolbarSnapshot, -}; - -fn assert_expand_hits(hits: &[HitRegion], sections: &[ToolbarSideSection]) { - for section in sections { - assert!( - hits.iter().any(|hit| matches!( - hit.event, - ToolbarEvent::ToggleSideSectionCollapsed(hit_section, false) - if hit_section == *section - )), - "missing expand hit for {section:?}" - ); - } -} - -fn populate_session(snapshot: &mut ToolbarSnapshot) { - snapshot.active_session_path = - Some(std::path::PathBuf::from("/tmp/current.wayscriber-session")); - snapshot.active_session_name = Some("current.wayscriber-session".to_string()); - snapshot.recent_sessions = vec![SessionRecentSnapshot { - display_name: "recent.wayscriber-session".to_string(), - path: std::path::PathBuf::from("/tmp/recent.wayscriber-session"), - }]; -} - -#[test] -fn collapsed_header_hit_excludes_body_start_boundary() { - let mut state = create_test_input_state(); - state.toolbar_side_pane = SidePane::Settings; - state.show_settings_section = true; - let snapshot = snapshot_from_state(&state); - let hits = rendered_side_hits(&snapshot); - let header = hits - .iter() - .find(|hit| { - matches!( - hit.event, - ToolbarEvent::ToggleSideSectionCollapsed(ToolbarSideSection::Settings, true) - ) - }) - .expect("settings collapse header"); - let body_start_y = header.rect.1 + ToolbarLayoutSpec::SIDE_SECTION_TOGGLE_OFFSET_Y; - - assert!( - !header.contains(header.rect.0 + header.rect.2 / 2.0, body_start_y), - "collapse header must not contain the first body row boundary" - ); -} - -#[test] -fn side_session_collapsed_hides_body_hits_and_keeps_expand_hit() { - let mut state = create_test_input_state(); - state.toolbar_side_pane = SidePane::Session; - - let mut expanded = snapshot_from_state(&state); - populate_session(&mut expanded); - - state - .toolbar_collapsed_side_sections - .insert(ToolbarSideSection::Session); - let mut collapsed = snapshot_from_state(&state); - populate_session(&mut collapsed); - - let expanded_size = side_size(&expanded); - let collapsed_size = side_size(&collapsed); - assert!( - collapsed_size.1 < expanded_size.1, - "collapsed Session section should reduce side toolbar height" - ); - - let hits = rendered_side_hits(&collapsed); - - assert!(hits.iter().any(|hit| matches!( - hit.event, - ToolbarEvent::ToggleSideSectionCollapsed(ToolbarSideSection::Session, false) - ))); - assert!( - !hits - .iter() - .any(|hit| matches!(hit.event, ToolbarEvent::OpenSession)), - "collapsed Session section should hide Open hit" - ); - assert!( - !hits - .iter() - .any(|hit| matches!(hit.event, ToolbarEvent::OpenRecentSession(_))), - "collapsed Session section should hide recent hits" - ); -} - -#[test] -fn side_settings_collapsed_hides_body_hits_and_keeps_expand_hit() { - let mut state = create_test_input_state(); - state.toolbar_side_pane = SidePane::Settings; - state.show_settings_section = true; - let expanded = snapshot_from_state(&state); - - state - .toolbar_collapsed_side_sections - .insert(ToolbarSideSection::Settings); - let collapsed = snapshot_from_state(&state); - - let expanded_size = side_size(&expanded); - let collapsed_size = side_size(&collapsed); - assert!( - collapsed_size.1 < expanded_size.1, - "collapsed Settings section should reduce side toolbar height" - ); - - let hits = rendered_side_hits(&collapsed); - - assert!(hits.iter().any(|hit| matches!( - hit.event, - ToolbarEvent::ToggleSideSectionCollapsed(ToolbarSideSection::Settings, false) - ))); - assert!( - !hits - .iter() - .any(|hit| matches!(hit.event, ToolbarEvent::ToggleContextAwareUi(_))), - "collapsed Settings section should hide setting toggles" - ); - assert!( - !hits - .iter() - .any(|hit| matches!(hit.event, ToolbarEvent::OpenConfigFile)), - "collapsed Settings section should hide action buttons" - ); -} - -#[test] -fn draw_pane_sections_collapsed_keep_only_headers() { - let mut state = create_test_input_state(); - state.toolbar_side_pane = SidePane::Draw; - - let sections = [ - ToolbarSideSection::Colors, - ToolbarSideSection::Presets, - ToolbarSideSection::Thickness, - ToolbarSideSection::TextSize, - ToolbarSideSection::Font, - ]; - state - .toolbar_collapsed_side_sections - .extend(sections.iter().copied()); - let collapsed = snapshot_from_state(&state); - - state.toolbar_collapsed_side_sections.clear(); - let expanded = snapshot_from_state(&state); - assert!(side_size(&collapsed).1 < side_size(&expanded).1); - - let hits = rendered_side_hits(&collapsed); - assert_expand_hits(&hits, §ions); - assert!(!hits.iter().any(|hit| matches!( - hit.kind, - HitKind::PickSatVal { .. } | HitKind::PickHue { .. } - ))); - assert!(!hits.iter().any(|hit| matches!( - hit.event, - ToolbarEvent::SetThickness(_) | ToolbarEvent::SetFontSize(_) | ToolbarEvent::SetFont(_) - ))); -} - -#[test] -fn canvas_pane_sections_collapsed_keep_only_headers() { - let mut state = create_test_input_state(); - state.toolbar_side_pane = SidePane::Canvas; - state.show_step_section = true; - - let sections = [ - ToolbarSideSection::Actions, - ToolbarSideSection::Boards, - ToolbarSideSection::Pages, - ToolbarSideSection::StepUndo, - ]; - state - .toolbar_collapsed_side_sections - .extend(sections.iter().copied()); - let collapsed = snapshot_from_state(&state); - - state.toolbar_collapsed_side_sections.clear(); - let expanded = snapshot_from_state(&state); - assert!(side_size(&collapsed).1 < side_size(&expanded).1); - - let hits = rendered_side_hits(&collapsed); - assert_expand_hits(&hits, §ions); - assert!(!hits.iter().any(|hit| matches!( - hit.event, - ToolbarEvent::Undo - | ToolbarEvent::PageNew - | ToolbarEvent::BoardNew - | ToolbarEvent::ToggleCustomSection(_) - ))); -} - -#[test] -fn tool_specific_side_sections_collapsed_keep_only_headers() { - let mut state = create_test_input_state(); - state - .toolbar_collapsed_side_sections - .insert(ToolbarSideSection::EraserMode); - state.set_tool_override(Some(Tool::Eraser)); - let collapsed = snapshot_from_state(&state); - let hits = rendered_side_hits(&collapsed); - assert_expand_hits(&hits, &[ToolbarSideSection::EraserMode]); - assert!( - !hits - .iter() - .any(|hit| matches!(hit.event, ToolbarEvent::SetEraserMode(_))) - ); - - let mut state = create_test_input_state(); - state - .toolbar_collapsed_side_sections - .insert(ToolbarSideSection::PolygonSides); - state.set_tool_override(Some(Tool::RegularPolygon)); - let collapsed = snapshot_from_state(&state); - let hits = rendered_side_hits(&collapsed); - assert_expand_hits(&hits, &[ToolbarSideSection::PolygonSides]); - assert!( - !hits - .iter() - .any(|hit| matches!(hit.event, ToolbarEvent::NudgePolygonSides(_))) - ); - - let mut state = create_test_input_state(); - state - .toolbar_collapsed_side_sections - .insert(ToolbarSideSection::ArrowLabels); - state.set_tool_override(Some(Tool::Arrow)); - let collapsed = snapshot_from_state(&state); - let hits = rendered_side_hits(&collapsed); - assert_expand_hits(&hits, &[ToolbarSideSection::ArrowLabels]); - assert!( - !hits - .iter() - .any(|hit| matches!(hit.event, ToolbarEvent::ToggleArrowLabels(_))) - ); - - let mut state = create_test_input_state(); - state - .toolbar_collapsed_side_sections - .insert(ToolbarSideSection::MarkerOpacity); - state.set_tool_override(Some(Tool::Marker)); - let collapsed = snapshot_from_state(&state); - let hits = rendered_side_hits(&collapsed); - assert_expand_hits(&hits, &[ToolbarSideSection::MarkerOpacity]); - assert!( - !hits - .iter() - .any(|hit| matches!(hit.event, ToolbarEvent::NudgeMarkerOpacity(_))) - ); - - let mut state = create_test_input_state(); - state - .toolbar_collapsed_side_sections - .insert(ToolbarSideSection::StepMarkers); - state.set_tool_override(Some(Tool::StepMarker)); - let collapsed = snapshot_from_state(&state); - let hits = rendered_side_hits(&collapsed); - assert_expand_hits(&hits, &[ToolbarSideSection::StepMarkers]); - assert!( - !hits - .iter() - .any(|hit| matches!(hit.event, ToolbarEvent::ResetStepMarkerCounter)) - ); -} diff --git a/src/backend/wayland/toolbar/layout/tests/mod.rs b/src/backend/wayland/toolbar/layout/tests/mod.rs index a299d1ba..f1c0591d 100644 --- a/src/backend/wayland/toolbar/layout/tests/mod.rs +++ b/src/backend/wayland/toolbar/layout/tests/mod.rs @@ -1,31 +1,8 @@ -use super::super::events::HitKind; -use super::super::hit::HitRegion; use super::*; use crate::config::{BoardsConfig, KeybindingsConfig, PresenterModeConfig}; use crate::draw::{Color, FontDescriptor}; use crate::input::{ClickHighlightSettings, EraserMode, InputState}; -use crate::ui::toolbar::model::{ - ToolbarActivation, ToolbarSessionModel, ToolbarSettingsModel, ToolbarSliderSpec, -}; -use crate::ui::toolbar::{ - SessionRecentSnapshot, SidePane, ToolbarBindingHints, ToolbarEvent, ToolbarSnapshot, -}; - -mod collapsible; - -/// Render-time hit oracle: the side palette has no static hit builder, so -/// hits come from drawing the palette at its computed size. -fn rendered_side_hits(snapshot: &ToolbarSnapshot) -> Vec { - let (w, h) = side_size(snapshot); - let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, w as i32, h as i32).unwrap(); - let ctx = cairo::Context::new(&surface).unwrap(); - let mut hits = Vec::new(); - crate::backend::wayland::toolbar::render_side_palette( - &ctx, w as f64, h as f64, snapshot, &mut hits, None, None, - ) - .unwrap(); - hits -} +use crate::ui::toolbar::{ToolbarBindingHints, ToolbarEvent, ToolbarSnapshot}; fn create_test_input_state() -> InputState { let keybindings = KeybindingsConfig::default(); @@ -76,14 +53,6 @@ fn snapshot_from_state(state: &InputState) -> ToolbarSnapshot { ToolbarSnapshot::from_input_with_bindings(state, ToolbarBindingHints::default()) } -fn event_name(event: &ToolbarEvent) -> String { - format!("{event:?}") -} - -fn activation_event_name(activation: &ToolbarActivation) -> String { - event_name(&activation.compatibility_event()) -} - #[test] fn top_size_respects_icon_mode() { let mut state = create_test_input_state(); @@ -435,458 +404,6 @@ fn highlight_ring_and_top_popovers_use_separate_lanes() { ); } -#[test] -fn scrolled_side_hits_are_clipped_at_both_viewport_edges() { - let mut state = create_test_input_state(); - state.toolbar_side_pane = SidePane::Settings; - state.toolbar_side_scroll[SidePane::Settings.index()] = 17.3; - let mut snapshot = snapshot_from_state(&state); - snapshot.side_viewport_max = Some(240.0); - snapshot.side_scroll = 17.3; - let (_, height) = side_size(&snapshot); - let content_top = ToolbarLayoutSpec::new(&snapshot).side_content_start_y(); - let hits = rendered_side_hits(&snapshot); - let content_hits = &hits[8..]; - - assert!(!content_hits.is_empty()); - assert!(content_hits.iter().all(|hit| { - hit.rect.1 >= content_top - f64::EPSILON - && hit.rect.1 + hit.rect.3 <= height as f64 + f64::EPSILON - })); - assert!(content_hits.iter().any(|hit| { - (hit.rect.1 - content_top).abs() < 0.01 - || (hit.rect.1 + hit.rect.3 - height as f64).abs() < 0.01 - })); -} - -#[test] -fn minimized_side_palette_is_a_single_restore_tab() { - let mut state = create_test_input_state(); - state.toolbar_side_minimized = true; - let snapshot = snapshot_from_state(&state); - - assert_eq!(side_size(&snapshot), (24, 64)); - - let hits = rendered_side_hits(&snapshot); - assert_eq!(hits.len(), 1, "one restore hit only: {hits:?}"); - assert!(matches!( - hits[0].event, - ToolbarEvent::SetSideMinimized(false) - )); -} - -#[test] -fn side_color_picker_offers_sat_val_area_and_hue_bar() { - let count_swatches = |hits: &[HitRegion]| { - hits.iter() - .filter(|hit| matches!(hit.event, ToolbarEvent::SetQuickColor { .. })) - .count() - }; - - let mut state = create_test_input_state(); - state.show_more_colors = false; - let snapshot = snapshot_from_state(&state); - let compact_hits = rendered_side_hits(&snapshot); - let sv = compact_hits - .iter() - .find(|hit| matches!(hit.kind, HitKind::PickSatVal { .. })) - .expect("sat/val area hit"); - let hue = compact_hits - .iter() - .find(|hit| matches!(hit.kind, HitKind::PickHue { .. })) - .expect("hue bar hit"); - assert_eq!(sv.rect.3, ToolbarLayoutSpec::SIDE_COLOR_SV_HEIGHT); - assert_eq!(hue.rect.3, ToolbarLayoutSpec::SIDE_COLOR_HUE_HEIGHT); - assert!( - hue.rect.1 > sv.rect.1 + sv.rect.3, - "hue bar sits below the sat/val area" - ); - - state.show_more_colors = true; - let snapshot = snapshot_from_state(&state); - let expanded_hits = rendered_side_hits(&snapshot); - assert!( - count_swatches(&expanded_hits) > count_swatches(&compact_hits), - "extended palette should add swatch hits" - ); -} - -#[test] -fn side_palette_swatch_hits_carry_their_own_palette_slot() { - let mut state = create_test_input_state(); - state.show_more_colors = true; - let snapshot = snapshot_from_state(&state); - - let mut slots = rendered_side_hits(&snapshot) - .into_iter() - .filter_map(|hit| match hit.event { - ToolbarEvent::SetQuickColor { index, color, .. } => Some((index, color)), - _ => None, - }) - .collect::>(); - assert!(!slots.is_empty(), "expanded palette renders swatches"); - - // Recoloring targets a slot, so every swatch must report a distinct index - // that matches the color it draws — the compact row reorders the palette. - let mut indices = slots.iter().map(|(index, _)| *index).collect::>(); - indices.sort_unstable(); - let unique = indices.len(); - indices.dedup(); - assert_eq!(indices.len(), unique, "each swatch owns one slot"); - - slots.sort_by_key(|(index, _)| *index); - for (index, color) in slots { - assert_eq!( - snapshot.quick_colors.color_for_index(index), - Some(color), - "swatch for slot {index} draws that slot's color" - ); - } -} - -#[test] -fn canvas_pane_right_aligns_destructive_buttons() { - let mut state = create_test_input_state(); - state.toolbar_side_pane = SidePane::Canvas; - state.toolbar_use_icons = true; - let snapshot = snapshot_from_state(&state); - let hits = rendered_side_hits(&snapshot); - - let right_edge = ToolbarLayoutSpec::SIDE_START_X - + (ToolbarLayoutSpec::SIDE_WIDTH as f64 - ToolbarLayoutSpec::SIDE_CONTENT_PADDING_X); - for (event_name, matcher) in [ - ( - "ClearCanvas", - &(|hit: &HitRegion| matches!(hit.event, ToolbarEvent::ClearCanvas { .. })) - as &dyn Fn(&HitRegion) -> bool, - ), - ("BoardDelete", &|hit: &HitRegion| { - matches!(hit.event, ToolbarEvent::BoardDelete) - }), - ("PageDelete", &|hit: &HitRegion| { - matches!(hit.event, ToolbarEvent::PageDelete) - }), - ] { - let hit = hits - .iter() - .find(|hit| matcher(hit)) - .unwrap_or_else(|| panic!("{event_name} hit")); - assert!( - (hit.rect.0 + hit.rect.2 - right_edge).abs() < 1.0, - "{event_name} should be right-aligned" - ); - } - - // The guard gap: the button before the delete does not abut it. - let duplicate = hits - .iter() - .find(|hit| matches!(hit.event, ToolbarEvent::PageDuplicate)) - .expect("page duplicate hit"); - let delete = hits - .iter() - .find(|hit| matches!(hit.event, ToolbarEvent::PageDelete)) - .expect("page delete hit"); - assert!( - duplicate.rect.0 + duplicate.rect.2 + ToolbarLayoutSpec::SIDE_ACTION_BUTTON_GAP - < delete.rect.0, - "destructive delete should sit apart from its neighbors" - ); -} - -#[test] -fn preset_slots_save_on_empty_click_and_clear_on_hover_badge() { - let state = create_test_input_state(); - let mut snapshot = snapshot_from_state(&state); - snapshot.presets[0] = Some(crate::ui::toolbar::PresetSlotSnapshot { - name: None, - tool: crate::input::Tool::Pen, - color: Color { - r: 1.0, - g: 0.5, - b: 0.0, - a: 1.0, - }, - size: 4.0, - eraser_kind: None, - eraser_mode: None, - marker_opacity: None, - fill_enabled: None, - font_size: None, - text_background_enabled: None, - arrow_length: None, - arrow_angle: None, - arrow_head_at_end: None, - show_status_bar: None, - }); - - // Without hover: filled slot applies, empty slots save, and the old - // per-slot micro action row is gone. - let hits = rendered_side_hits(&snapshot); - assert!( - hits.iter() - .any(|hit| matches!(hit.event, ToolbarEvent::ApplyPreset(1))) - ); - assert!( - hits.iter() - .any(|hit| matches!(hit.event, ToolbarEvent::SavePreset(2))) - ); - assert!( - !hits - .iter() - .any(|hit| matches!(hit.event, ToolbarEvent::SavePreset(1))), - "filled slot should not save on click" - ); - assert!( - !hits - .iter() - .any(|hit| matches!(hit.event, ToolbarEvent::ClearPreset(_))), - "clear affordance only appears on hover" - ); - - // Hovering the filled slot reveals the clear badge, and its hit takes - // precedence over the apply hit underneath. - let apply_hit = hits - .iter() - .find(|hit| matches!(hit.event, ToolbarEvent::ApplyPreset(1))) - .expect("apply hit"); - let hover = ( - apply_hit.rect.0 + apply_hit.rect.2 / 2.0, - apply_hit.rect.1 + apply_hit.rect.3 / 2.0, - ); - let (w, h) = side_size(&snapshot); - let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, w as i32, h as i32).unwrap(); - let ctx = cairo::Context::new(&surface).unwrap(); - let mut hovered_hits = Vec::new(); - crate::backend::wayland::toolbar::render_side_palette( - &ctx, - w as f64, - h as f64, - &snapshot, - &mut hovered_hits, - Some(hover), - None, - ) - .unwrap(); - let clear_index = hovered_hits - .iter() - .position(|hit| matches!(hit.event, ToolbarEvent::ClearPreset(1))) - .expect("clear badge hit while hovered"); - let apply_index = hovered_hits - .iter() - .position(|hit| matches!(hit.event, ToolbarEvent::ApplyPreset(1))) - .expect("apply hit while hovered"); - assert!( - clear_index < apply_index, - "clear badge must win first-match hit testing over apply" - ); -} - -#[test] -fn side_header_leads_with_drag_handle_and_offers_all_panes() { - let state = create_test_input_state(); - let snapshot = snapshot_from_state(&state); - let hits = rendered_side_hits(&snapshot); - - assert!(matches!(hits[0].kind, HitKind::DragMoveSide)); - assert!( - hits.iter() - .any(|hit| matches!(hit.event, ToolbarEvent::ToggleBoardPicker)) - ); - for pane in SidePane::ALL { - assert!( - hits.iter() - .any(|hit| hit.event == ToolbarEvent::SetSidePane(pane)), - "missing pane nav hit for {pane:?}" - ); - } -} - -#[test] -fn side_size_caps_height_at_viewport_max_and_reports_scroll_bounds() { - let state = create_test_input_state(); - let mut snapshot = snapshot_from_state(&state); - - snapshot.side_viewport_max = None; - let (_, natural_height) = side_size(&snapshot); - let (natural, viewport) = side_scroll_bounds(&snapshot); - assert_eq!(viewport, natural_height as f64); - assert!(natural <= viewport); - - let cap = (natural_height as f64 / 2.0).floor(); - snapshot.side_viewport_max = Some(cap); - let (_, capped_height) = side_size(&snapshot); - assert_eq!(capped_height as f64, cap.ceil()); - let (natural, viewport) = side_scroll_bounds(&snapshot); - assert!( - natural - viewport > 0.0, - "capped pane should report positive max scroll" - ); - - // The scrollbar hit carries the same max scroll for drag handling. - let hits = rendered_side_hits(&snapshot); - assert!(hits.iter().any(|hit| matches!( - hit.kind, - HitKind::DragScrollSide { max_scroll } if (max_scroll - (natural - viewport)).abs() < 1.0 - ))); -} - -#[test] -fn side_settings_pane_hits_include_model_controls() { - let mut state = create_test_input_state(); - state.toolbar_side_pane = SidePane::Settings; - state.show_settings_section = true; - state.toolbar_layout_mode = crate::config::ToolbarLayoutMode::Regular; - let snapshot = snapshot_from_state(&state); - let model = ToolbarSettingsModel::from_snapshot(&snapshot).expect("settings model"); - let expected: Vec<_> = model - .toggles() - .iter() - .map(|toggle| activation_event_name(&toggle.activation)) - .chain( - model - .buttons() - .iter() - .map(|button| event_name(&button.event)), - ) - .collect(); - - let hits = rendered_side_hits(&snapshot); - let hit_events: Vec<_> = hits.iter().map(|hit| event_name(&hit.event)).collect(); - - for expected_event in &expected { - assert!( - hit_events.contains(expected_event), - "missing settings hit {expected_event}" - ); - } - assert!(hit_events.contains(&"ToggleContextAwareUi(false)".to_string())); -} - -#[test] -fn wide_settings_toggles_span_the_full_content_width() { - let mut state = create_test_input_state(); - state.toolbar_side_pane = SidePane::Settings; - state.toolbar_layout_mode = crate::config::ToolbarLayoutMode::Regular; - state.refresh_section_visibility(); - let snapshot = snapshot_from_state(&state); - let hits = rendered_side_hits(&snapshot); - - let full_width = - ToolbarLayoutSpec::SIDE_WIDTH as f64 - ToolbarLayoutSpec::SIDE_CONTENT_PADDING_X; - let step = hits - .iter() - .find(|hit| matches!(hit.event, ToolbarEvent::ToggleStepSection(_))) - .expect("multi-step toggle hit"); - assert!( - (step.rect.2 - full_width).abs() < 1.0, - "long label takes a full row: {}", - step.rect.2 - ); - let advanced = hits - .iter() - .find(|hit| matches!(hit.event, ToolbarEvent::ToggleActionsAdvanced(_))) - .expect("advanced actions toggle hit"); - assert!((advanced.rect.2 - full_width).abs() < 1.0); - - // Narrow toggles still pair up in half-width cells. - let narrow = hits - .iter() - .find(|hit| matches!(hit.event, ToolbarEvent::ToggleContextAwareUi(_))) - .expect("context toggle hit"); - assert!(narrow.rect.2 < full_width / 2.0 + 1.0); -} - -#[test] -fn side_session_pane_hits_include_model_controls_and_recents() { - let mut state = create_test_input_state(); - state.toolbar_side_pane = SidePane::Session; - let mut snapshot = snapshot_from_state(&state); - snapshot.active_session_path = - Some(std::path::PathBuf::from("/tmp/current.wayscriber-session")); - snapshot.active_session_name = Some("current.wayscriber-session".to_string()); - snapshot.recent_sessions = vec![SessionRecentSnapshot { - display_name: "recent.wayscriber-session".to_string(), - path: std::path::PathBuf::from("/tmp/recent.wayscriber-session"), - }]; - let model = ToolbarSessionModel::from_snapshot(&snapshot).expect("session model"); - - let hits = rendered_side_hits(&snapshot); - let hit_events: Vec<_> = hits.iter().map(|hit| event_name(&hit.event)).collect(); - - for button in &model.buttons { - assert!( - hit_events.contains(&event_name(&button.event)), - "missing session button hit {:?}", - button.event - ); - } - assert!(hit_events.iter().any(|event| { - event.contains("OpenRecentSession") && event.contains("recent.wayscriber-session") - })); -} - -#[test] -fn side_session_overwrite_confirmation_hits_replace_action_buttons() { - let mut state = create_test_input_state(); - state.toolbar_side_pane = SidePane::Session; - let mut snapshot = snapshot_from_state(&state); - let target = std::path::PathBuf::from("/tmp/existing.wayscriber-session"); - snapshot.active_session_path = - Some(std::path::PathBuf::from("/tmp/current.wayscriber-session")); - snapshot.active_session_name = Some("current.wayscriber-session".to_string()); - snapshot.pending_save_as_overwrite_path = Some(target.clone()); - - let rendered_hits = rendered_side_hits(&snapshot); - assert_session_overwrite_confirmation_hits(&rendered_hits, &target); -} - -fn assert_session_overwrite_confirmation_hits( - hits: &[crate::backend::wayland::toolbar::hit::HitRegion], - target: &std::path::Path, -) { - assert!(hits.iter().any(|hit| matches!( - &hit.event, - ToolbarEvent::SaveSessionAsConfirm(path) if path == target - ))); - assert!( - hits.iter() - .any(|hit| matches!(hit.event, ToolbarEvent::SaveSessionAsCancel)) - ); - assert!( - !hits - .iter() - .any(|hit| matches!(hit.event, ToolbarEvent::SaveSessionAs)), - "pending overwrite prompt should replace the Save As action grid" - ); -} - -#[test] -fn font_size_nudge_hits_use_slider_spec_step() { - let mut state = create_test_input_state(); - state.show_text_controls = true; - state.current_font_size = 32.0; - let snapshot = snapshot_from_state(&state); - let (w, h) = side_size(&snapshot); - - let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, w as i32, h as i32).unwrap(); - let ctx = cairo::Context::new(&surface).unwrap(); - let mut hits = Vec::new(); - crate::backend::wayland::toolbar::render_side_palette( - &ctx, w as f64, h as f64, &snapshot, &mut hits, None, None, - ) - .unwrap(); - - let step = ToolbarSliderSpec::FONT_SIZE.step.expect("font size step"); - assert!(hits.iter().any(|hit| matches!( - hit.event, - ToolbarEvent::NudgeFontSize(value) if (value + step).abs() < f64::EPSILON - ))); - assert!(hits.iter().any(|hit| matches!( - hit.event, - ToolbarEvent::NudgeFontSize(value) if (value - step).abs() < f64::EPSILON - ))); -} - #[test] fn top_size_scales_with_toolbar_scale() { let mut state = create_test_input_state(); diff --git a/src/backend/wayland/toolbar/main/input.rs b/src/backend/wayland/toolbar/main/input.rs index 343f7fcc..a26974d4 100644 --- a/src/backend/wayland/toolbar/main/input.rs +++ b/src/backend/wayland/toolbar/main/input.rs @@ -3,7 +3,6 @@ use std::time::Instant; use wayland_client::protocol::wl_surface; use super::structs::ToolbarSurfaceManager; -use crate::backend::wayland::toolbar::ToolbarFocusTarget; use crate::backend::wayland::toolbar_intent::ToolbarIntent; use crate::ui::toolbar::ToolbarEvent; @@ -16,9 +15,6 @@ impl ToolbarSurfaceManager { if self.top.is_surface(surface) { return self.top.hit_at(position.0, position.1); } - if self.side.is_surface(surface) { - return self.side.hit_at(position.0, position.1); - } None } @@ -33,9 +29,6 @@ impl ToolbarSurfaceManager { if self.top.is_surface(surface) { return self.top.quick_color_slot_at(position.0, position.1); } - if self.side.is_surface(surface) { - return self.side.quick_color_slot_at(position.0, position.1); - } None } @@ -55,17 +48,6 @@ impl ToolbarSurfaceManager { } return self.top.drag_at(position.0, position.1); } - if self.side.is_surface(surface) { - if self.side_hover != Some(position) { - // Reset hover start time when position changes - if self.side_hover.is_none() { - self.side_hover_start = Some(Instant::now()); - } - self.side_hover = Some(position); - self.side.set_hover(Some(position)); - } - return self.side.drag_at(position.0, position.1); - } None } @@ -74,58 +56,28 @@ impl ToolbarSurfaceManager { self.top_hover = None; self.top_hover_start = None; self.top.set_hover(None); - } else if self.side.is_surface(surface) { - self.side_hover = None; - self.side_hover_start = None; - self.side.set_hover(None); } } - pub fn focus_target_for_surface( - &self, - surface: &wl_surface::WlSurface, - ) -> Option { - if self.top.is_surface(surface) { - Some(ToolbarFocusTarget::Top) - } else if self.side.is_surface(surface) { - Some(ToolbarFocusTarget::Side) - } else { - None - } + /// Whether the surface is the top toolbar surface, i.e. whether keyboard + /// focus routing applies to it. + pub fn is_focusable_surface(&self, surface: &wl_surface::WlSurface) -> bool { + self.top.is_surface(surface) } - pub fn hovered_target(&self) -> Option { - if self.top_hover.is_some() { - Some(ToolbarFocusTarget::Top) - } else if self.side_hover.is_some() { - Some(ToolbarFocusTarget::Side) - } else { - None - } + pub fn is_hovered(&self) -> bool { + self.top_hover.is_some() } pub fn clear_focus(&mut self) { self.top.clear_focus(); - self.side.clear_focus(); } - /// Drops side-toolbar focus only; used when a pane switch invalidates - /// the side hit list while top-toolbar focus stays meaningful. - pub fn clear_side_focus(&mut self) { - self.side.clear_focus(); + pub fn focus_next(&mut self, reverse: bool) -> bool { + self.top.focus_next(reverse) } - pub fn focus_next(&mut self, target: ToolbarFocusTarget, reverse: bool) -> bool { - match target { - ToolbarFocusTarget::Top => self.top.focus_next(reverse), - ToolbarFocusTarget::Side => self.side.focus_next(reverse), - } - } - - pub fn focused_event(&self, target: ToolbarFocusTarget) -> Option { - match target { - ToolbarFocusTarget::Top => self.top.focused_event(), - ToolbarFocusTarget::Side => self.side.focused_event(), - } + pub fn focused_event(&self) -> Option { + self.top.focused_event() } } diff --git a/src/backend/wayland/toolbar/main/lifecycle.rs b/src/backend/wayland/toolbar/main/lifecycle.rs index 9f3d0155..fc838ca5 100644 --- a/src/backend/wayland/toolbar/main/lifecycle.rs +++ b/src/backend/wayland/toolbar/main/lifecycle.rs @@ -20,7 +20,6 @@ impl ToolbarSurfaceManager { snapshot: &ToolbarSnapshot, ) { let top_size = crate::backend::wayland::toolbar::top_size(snapshot); - let side_size = crate::backend::wayland::toolbar::side_size(snapshot); if self.is_top_visible() { if self.top.layer_surface.is_none() { @@ -38,23 +37,8 @@ impl ToolbarSurfaceManager { .ensure_created(qh, compositor, layer_shell, scale, output); } - if self.is_side_visible() { - if self.side.layer_surface.is_none() { - info!( - "Ensuring side toolbar surface exists at logical size {:?}, scale {}", - side_size, scale - ); - self.side.set_logical_size(side_size); - } else if self.side.logical_size != side_size { - self.side.resize(side_size); - } - self.side - .ensure_created(qh, compositor, layer_shell, scale, output); - } - if self.suppressed { self.top.set_suppressed(compositor, true); - self.side.set_suppressed(compositor, true); } } @@ -66,14 +50,10 @@ impl ToolbarSurfaceManager { if self.top.is_layer(layer) { return self.top.handle_configure(configure); } - if self.side.is_layer(layer) { - return self.side.handle_configure(configure); - } false } pub fn maybe_update_scale(&mut self, output: Option<&wl_output::WlOutput>, scale: i32) { self.top.maybe_update_scale(output, scale); - self.side.maybe_update_scale(output, scale); } } diff --git a/src/backend/wayland/toolbar/main/render.rs b/src/backend/wayland/toolbar/main/render.rs index d4d73105..bf6a6c94 100644 --- a/src/backend/wayland/toolbar/main/render.rs +++ b/src/backend/wayland/toolbar/main/render.rs @@ -12,58 +12,35 @@ impl ToolbarSurfaceManager { hover: Option<(f64, f64)>, render_profile: Option<&RenderColorProfile>, ) { - // Render top toolbar if visible - if self.is_top_visible() { - self.top.set_ui_scale(snapshot.toolbar_scale); - let top_hover = hover.or(self.top_hover).or(self.top.focused_hover()); - let top_hover_start = self.top_hover_start; - if let Err(err) = self.top.render( - shm, - snapshot, - top_hover, - top_hover_start, - render_profile, - |ctx, w, h, snap, hits, hov, hov_start| { - crate::backend::wayland::toolbar::render_top_strip( - ctx, w, h, snap, hits, hov, hov_start, - ) - }, - ) { - self.top.report_render_failure(&err); - } - self.top.sync_top_input_region(snapshot); + if !self.is_top_visible() { + return; } - - // Render side toolbar if visible - if self.is_side_visible() { - self.side.set_ui_scale(snapshot.toolbar_scale); - let side_hover = hover.or(self.side_hover).or(self.side.focused_hover()); - let side_hover_start = self.side_hover_start; - if let Err(err) = self.side.render( - shm, - snapshot, - side_hover, - side_hover_start, - render_profile, - |ctx, w, h, snap, hits, hov, hov_start| { - crate::backend::wayland::toolbar::render_side_palette( - ctx, w, h, snap, hits, hov, hov_start, - ) - }, - ) { - self.side.report_render_failure(&err); - } + self.top.set_ui_scale(snapshot.toolbar_scale); + let top_hover = hover.or(self.top_hover).or(self.top.focused_hover()); + let top_hover_start = self.top_hover_start; + if let Err(err) = self.top.render( + shm, + snapshot, + top_hover, + top_hover_start, + render_profile, + |ctx, w, h, snap, hits, hov, hov_start| { + crate::backend::wayland::toolbar::render_top_strip( + ctx, w, h, snap, hits, hov, hov_start, + ) + }, + ) { + self.top.report_render_failure(&err); } + self.top.sync_top_input_region(snapshot); } pub fn mark_dirty(&mut self) { self.top.mark_dirty(); - self.side.mark_dirty(); } pub fn needs_render(&self) -> bool { - (self.top_visible && self.top.needs_render()) - || (self.side_visible && self.side.needs_render()) + self.top_visible && self.top.needs_render() } /// Store the latest snapshot and report whether it differs from the previous one. diff --git a/src/backend/wayland/toolbar/main/state.rs b/src/backend/wayland/toolbar/main/state.rs index c6e23285..b709102b 100644 --- a/src/backend/wayland/toolbar/main/state.rs +++ b/src/backend/wayland/toolbar/main/state.rs @@ -8,13 +8,9 @@ impl ToolbarSurfaceManager { self.top.layer_surface.is_some() } - pub fn side_created(&self) -> bool { - self.side.layer_surface.is_some() - } - - /// Returns true if any toolbar is visible + /// Returns true if the toolbar is visible pub fn is_visible(&self) -> bool { - self.visible || self.top_visible || self.side_visible + self.top_visible } /// Returns true if the top toolbar is visible @@ -22,51 +18,22 @@ impl ToolbarSurfaceManager { self.top_visible } - /// Returns true if the side toolbar is visible - pub fn is_side_visible(&self) -> bool { - self.side_visible - } - - /// Set combined visibility (shows/hides both toolbars) + /// Set visibility of the top toolbar pub fn set_visible(&mut self, visible: bool) { - self.visible = visible; - if visible { - self.top_visible = true; - self.side_visible = true; - } else { - self.top_visible = false; - self.side_visible = false; - self.top.destroy(); - self.side.destroy(); - self.top_hover = None; - self.side_hover = None; - } - } - - /// Set visibility of the top toolbar only - pub fn set_top_visible(&mut self, visible: bool) { self.top_visible = visible; if !visible { self.top.destroy(); self.top_hover = None; } - // Update combined flag: any toolbar visible keeps overlays alive. - self.visible = self.top_visible || self.side_visible; } - /// Set visibility of the side toolbar only - pub fn set_side_visible(&mut self, visible: bool) { - self.side_visible = visible; - if !visible { - self.side.destroy(); - self.side_hover = None; - } - // Update combined flag: any toolbar visible keeps overlays alive. - self.visible = self.top_visible || self.side_visible; + /// Set visibility of the top toolbar only + pub fn set_top_visible(&mut self, visible: bool) { + self.set_visible(visible); } pub fn is_toolbar_surface(&self, surface: &wl_surface::WlSurface) -> bool { - self.top.is_surface(surface) || self.side.is_surface(surface) + self.top.is_surface(surface) } /// Whether the pointer (or keyboard focus hover) is currently on the @@ -81,18 +48,16 @@ impl ToolbarSurfaceManager { } self.suppressed = suppressed; self.top.set_suppressed(compositor, suppressed); - self.side.set_suppressed(compositor, suppressed); } pub fn is_suppressed(&self) -> bool { self.suppressed } - /// Apply pending input-region changes on both surfaces (no-op unless a - /// render declared partial input rects or suppression state changed). + /// Apply pending input-region changes (no-op unless a render declared + /// partial input rects or suppression state changed). pub fn apply_input_regions(&mut self, compositor: &CompositorState) { self.top.apply_input_region(compositor); - self.side.apply_input_region(compositor); } pub fn set_top_margins(&mut self, top: i32, left: i32) { @@ -100,23 +65,16 @@ impl ToolbarSurfaceManager { self.top.set_margins(top, right, bottom, left); } - pub fn set_side_margins(&mut self, top: i32, left: i32) { - let (_, right, bottom, _) = self.side.margin; - self.side.set_margins(top, right, bottom, left); - } - pub fn destroy_all(&mut self) { self.top.destroy(); - self.side.destroy(); self.top_hover = None; - self.side_hover = None; } pub fn is_toolbar_layer(&self, layer: &LayerSurface) -> bool { - self.top.is_layer(layer) || self.side.is_layer(layer) + self.top.is_layer(layer) } - pub fn configured_states(&self) -> (bool, bool) { - (self.top.configured, self.side.configured) + pub fn top_configured(&self) -> bool { + self.top.configured } } diff --git a/src/backend/wayland/toolbar/main/structs.rs b/src/backend/wayland/toolbar/main/structs.rs index e645a6c8..4f4d0936 100644 --- a/src/backend/wayland/toolbar/main/structs.rs +++ b/src/backend/wayland/toolbar/main/structs.rs @@ -6,49 +6,32 @@ use crate::backend::wayland::toolbar::events::ToolbarCursorHint; use crate::backend::wayland::toolbar::surfaces::ToolbarSurface; use crate::ui::toolbar::ToolbarSnapshot; -/// Tracks the lifetime and visibility of the top + side toolbar surfaces. +/// Tracks the lifetime and visibility of the top toolbar surface. #[derive(Debug)] pub struct ToolbarSurfaceManager { - /// Combined visibility flag (true when any toolbar visible) - pub(super) visible: bool, /// Whether the top toolbar is visible pub(super) top_visible: bool, - /// Whether the side toolbar is visible - pub(super) side_visible: bool, pub(super) suppressed: bool, pub(super) top: ToolbarSurface, - pub(super) side: ToolbarSurface, pub(super) top_hover: Option<(f64, f64)>, - pub(super) side_hover: Option<(f64, f64)>, /// Timestamp when top hover started (for tooltip delay). pub(super) top_hover_start: Option, - /// Timestamp when side hover started (for tooltip delay). - pub(super) side_hover_start: Option, pub(super) last_snapshot: Option, } impl Default for ToolbarSurfaceManager { fn default() -> Self { Self { - visible: false, top_visible: false, - side_visible: false, suppressed: false, - // Anchor top/side toolbars to both axes they offset along so margins take effect. + // Anchor the top toolbar to both axes it offsets along so margins take effect. top: ToolbarSurface::new( "wayscriber-toolbar-top", Anchor::TOP | Anchor::LEFT, (12, 12, 0, 12), ), - side: ToolbarSurface::new( - "wayscriber-toolbar-side", - Anchor::LEFT | Anchor::TOP, - (24, 0, 24, 24), - ), top_hover: None, - side_hover: None, top_hover_start: None, - side_hover_start: None, last_snapshot: None, } } @@ -59,14 +42,11 @@ impl ToolbarSurfaceManager { Self::default() } - /// Get cursor hint for the currently hovered toolbar, if any. + /// Get cursor hint for the top toolbar when hovered. pub fn cursor_hint(&self) -> Option { if self.top_hover.is_some() { return self.top.cursor_hint(); } - if self.side_hover.is_some() { - return self.side.cursor_hint(); - } None } } diff --git a/src/backend/wayland/toolbar/mod.rs b/src/backend/wayland/toolbar/mod.rs index b0a851a8..c07a5dba 100644 --- a/src/backend/wayland/toolbar/mod.rs +++ b/src/backend/wayland/toolbar/mod.rs @@ -8,20 +8,10 @@ pub mod surfaces; pub mod view; pub use events::ToolbarCursorHint; -pub use layout::{side_size, top_size}; +pub use layout::top_size; pub use main::*; -pub use render::{render_side_palette, render_top_strip}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ToolbarFocusTarget { - Top, - Side, -} +pub use render::render_top_strip; pub fn format_binding_label(label: &str, binding: Option<&str>) -> String { crate::label_format::format_binding_label(label, binding) } - -pub fn format_quick_color_tooltip(label: &str, binding: Option<&str>) -> String { - crate::label_format::format_quick_color_tooltip(label, binding) -} diff --git a/src/backend/wayland/toolbar/render.rs b/src/backend/wayland/toolbar/render.rs index 325c91dd..cd03bf00 100644 --- a/src/backend/wayland/toolbar/render.rs +++ b/src/backend/wayland/toolbar/render.rs @@ -1,9 +1,7 @@ mod paint; -pub(in crate::backend::wayland::toolbar) mod side_palette; mod top_strip; mod widgets; -pub use side_palette::render_side_palette; pub use top_strip::render_top_strip; /// Delay before showing toolbar tooltips. diff --git a/src/backend/wayland/toolbar/render/paint.rs b/src/backend/wayland/toolbar/render/paint.rs index 4dbc7b4b..0bfdbffd 100644 --- a/src/backend/wayland/toolbar/render/paint.rs +++ b/src/backend/wayland/toolbar/render/paint.rs @@ -19,10 +19,10 @@ use super::widgets::constants::{ }; use super::widgets::{ draw_button, draw_checkbox, draw_destructive_button, draw_disabled_button, - draw_divider_vertical, draw_drag_handle, draw_group_card, draw_label_center, - draw_label_center_color, draw_label_left, draw_label_left_wrapped, draw_mini_checkbox, - draw_minimize_button, draw_panel_background, draw_pin_button, draw_popover_panel, - draw_round_rect, draw_segmented_control, ellipsize_to_width, point_in_rect, set_icon_color, + draw_divider_vertical, draw_drag_handle, draw_label_center, draw_label_center_color, + draw_label_left, draw_label_left_wrapped, draw_mini_checkbox, draw_minimize_button, + draw_panel_background, draw_pin_button, draw_popover_panel, draw_round_rect, + draw_segmented_control, ellipsize_to_width, point_in_rect, set_icon_color, }; /// Hover ring around an unselected swatch (dimmer sibling of the accent @@ -80,9 +80,8 @@ fn paint_button_body( /// Draw a filled preset slot's color as a small rounded swatch tucked in the /// bottom-right corner. A luminance-driven hairline keeps the swatch defined /// against the slot body regardless of the preset color (a black swatch on -/// the dark body, a white swatch on the accent body). Mirrors the built-in -/// side-palette swatch treatment and the GTK preset slot so black and white -/// presets both stay legible. +/// the dark body, a white swatch on the accent body). It matches the GTK +/// preset slot so black and white presets both stay legible. /// /// A translucent preset paints at its own alpha over the checkerboard, so the /// slot previews the color the preset will actually apply. @@ -167,7 +166,6 @@ fn paint_node(ctx: &cairo::Context, node: &WidgetNode, hover: Option<(f64, f64)> let is_hover = hovered(node, hover) && node.interact.is_some(); match &node.kind { WidgetKind::Panel => draw_panel_background(ctx, x, y, w, h), - WidgetKind::Card => draw_group_card(ctx, x, y, w, h), WidgetKind::Divider { vertical } => { if *vertical { draw_divider_vertical(ctx, x, y, h); @@ -221,10 +219,6 @@ fn paint_node(ctx: &cairo::Context, node: &WidgetNode, hover: Option<(f64, f64)> draw_label_center(ctx, text_style, x, y, w, h, &display); } } - WidgetKind::Icon { glyph } => { - set_color(ctx, COLOR_ICON_DEFAULT); - (glyph.0)(ctx, x, y, w.min(h)); - } WidgetKind::Label(label) => { let text_style = label_style(label.size, label.bold); if label.wrap { @@ -286,8 +280,8 @@ fn paint_node(ctx: &cairo::Context, node: &WidgetNode, hover: Option<(f64, f64)> } WidgetKind::HitArea => {} WidgetKind::Slider { t } => { - // Track and knob share the side-palette slider treatment: a - // rounded track with the accent knob riding the inset travel. + // Track and knob: a rounded track with the accent knob riding the + // inset travel. let track_h = (h * 0.5).min(8.0); let track_y = y + (h - track_h) / 2.0; set_color(ctx, COLOR_TRACK_BACKGROUND); @@ -339,7 +333,7 @@ fn paint_node(ctx: &cairo::Context, node: &WidgetNode, hover: Option<(f64, f64)> // Filled slot: the saved tool glyph in the neutral foreground // so a dark preset color never renders it invisible against // the slot body; the preset color rides along as a separate - // corner swatch instead (the side-palette convention). + // corner swatch instead. Some(glyph) => { let icon_size = (w.min(h) * PRESET_SLOT_ICON_RATIO).round(); set_color(ctx, COLOR_TEXT_SECONDARY); @@ -398,12 +392,8 @@ fn paint_node(ctx: &cairo::Context, node: &WidgetNode, hover: Option<(f64, f64)> draw_popover_panel(ctx, x, y, w, h, *caret_x, *caret_up); } WidgetKind::VScrollbar { t, thumb } => { - // Same treatment as the side palette's scrollbar: a soft track - // with the theme's proportional slider thumb. - set_color( - ctx, - crate::backend::wayland::toolbar::render::side_palette::COLOR_SCROLLBAR_TRACK, - ); + // A soft track with the theme's proportional slider thumb. + set_color(ctx, crate::ui::theme::toolbar::COLOR_SCROLLBAR_TRACK); draw_round_rect(ctx, x, y, w, h, w / 2.0); let _ = ctx.fill(); let thumb_h = (h * thumb.clamp(0.0, 1.0)).max(w * 2.0).min(h); diff --git a/src/backend/wayland/toolbar/render/side_palette/actions.rs b/src/backend/wayland/toolbar/render/side_palette/actions.rs deleted file mode 100644 index 6f61335e..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/actions.rs +++ /dev/null @@ -1,303 +0,0 @@ -mod helpers; - -use super::SidePaletteLayout; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::backend::wayland::toolbar::rows::row_item_width; -use crate::toolbar_icons; -use crate::ui::toolbar::ToolbarSnapshot; -use crate::ui::toolbar::model::{ - ToolbarActionsModel, ToolbarButtonModel, ToolbarCommandGroup, ToolbarCommandGroupKind, -}; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection}; -use crate::ui_text::UiTextStyle; - -use super::super::widgets::constants::{FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, set_color}; -use super::super::widgets::draw_group_card; -use super::section_header::draw_collapsible_header; -use crate::ui::theme::Rgba; - -/// Muted cool-gray group sub-label ("History", "Zoom"). Slightly darker -/// than COLOR_SEGMENT_TEXT_INACTIVE — kept to avoid a visible shift. -const COLOR_GROUP_SUB_LABEL: Rgba = (0.62, 0.65, 0.72, 0.9); -use helpers::{ - ActionButton, ActionIconFn, IconActionLayout, TextActionLayout, render_icon_action_group, - render_icon_action_row_split, render_text_action_group, -}; - -pub(super) fn draw_actions_section(layout: &mut SidePaletteLayout, y: &mut f64) { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let hover = layout.hover; - let x = layout.x; - let card_x = layout.card_x; - let card_w = layout.card_w; - let content_width = layout.content_width; - let section_gap = layout.section_gap; - let use_icons = snapshot.use_icons; - let label_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_LABEL, - }; - - let Some(model) = ToolbarActionsModel::from_snapshot(snapshot) else { - return; - }; - - let actions_card_h = layout.spec.side_actions_height(snapshot); - draw_group_card(ctx, card_x, *y, card_w, actions_card_h); - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::Actions, - ToolbarSideSection::Actions.label(), - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL, - ); - if snapshot.side_section_collapsed(ToolbarSideSection::Actions) { - *y += actions_card_h + section_gap; - return; - } - - let hits = &mut layout.hits; - let actions_y = *y + ToolbarLayoutSpec::SIDE_SECTION_TOGGLE_OFFSET_Y; - - if use_icons { - render_icon_action_sections( - ctx, - hits, - hover, - snapshot, - actions_y, - x, - content_width, - &model, - ); - } else { - render_text_action_sections( - ctx, - hits, - hover, - snapshot, - actions_y, - x, - content_width, - label_style, - &model, - ); - } - - *y += actions_card_h + section_gap; -} - -#[allow(clippy::too_many_arguments)] -fn render_icon_action_sections( - ctx: &cairo::Context, - hits: &mut Vec, - hover: Option<(f64, f64)>, - snapshot: &ToolbarSnapshot, - start_y: f64, - x: f64, - content_width: f64, - model: &ToolbarActionsModel, -) { - let icon_btn_size = ToolbarLayoutSpec::SIDE_ACTION_BUTTON_HEIGHT_ICON; - let icon_gap = ToolbarLayoutSpec::SIDE_ACTION_BUTTON_GAP; - let icon_size = ToolbarLayoutSpec::SIDE_ACTION_ICON_SIZE; - let mut action_y = start_y; - let mut has_group = false; - for group in model.groups() { - let actions = build_action_buttons(snapshot, group); - if actions.is_empty() { - continue; - } - if has_group { - action_y += icon_gap; - } - action_y += draw_group_sub_label(ctx, x, action_y, group.kind); - let layout = IconActionLayout { - x, - content_width, - start_y: action_y, - button_size: icon_btn_size, - icon_size, - gap: icon_gap, - columns: icon_group_columns(group), - add_gap: false, - }; - let (next_y, has_rows) = if group.kind == ToolbarCommandGroupKind::History { - // Destructive history actions (Clear) sit right-aligned, away - // from Undo/Redo. - let (leading, trailing): (Vec<_>, Vec<_>) = actions - .iter() - .cloned() - .partition(|action| !action.event.is_destructive()); - render_icon_action_row_split(ctx, hits, hover, snapshot, layout, &leading, &trailing) - } else { - render_icon_action_group(ctx, hits, hover, snapshot, layout, &actions) - }; - if has_rows { - action_y = next_y; - has_group = true; - } - } -} - -/// Draw the small muted group label ("History", "Zoom") above an action -/// group; returns the height consumed. -fn draw_group_sub_label( - ctx: &cairo::Context, - x: f64, - y: f64, - kind: ToolbarCommandGroupKind, -) -> f64 { - let Some(label) = kind.sub_label() else { - return 0.0; - }; - let style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_LABEL * 0.85, - }; - let layout = crate::ui_text::text_layout(ctx, style, label, None); - set_color(ctx, COLOR_GROUP_SUB_LABEL); - layout.show_at_baseline(ctx, x, y + 11.0); - ToolbarLayoutSpec::SIDE_ACTION_GROUP_LABEL_HEIGHT -} - -#[allow(clippy::too_many_arguments)] -fn render_text_action_sections( - ctx: &cairo::Context, - hits: &mut Vec, - hover: Option<(f64, f64)>, - snapshot: &ToolbarSnapshot, - start_y: f64, - x: f64, - content_width: f64, - label_style: UiTextStyle<'static>, - model: &ToolbarActionsModel, -) { - let action_h = ToolbarLayoutSpec::SIDE_ACTION_BUTTON_HEIGHT_TEXT; - let action_row_gap = ToolbarLayoutSpec::SIDE_ACTION_CONTENT_GAP_TEXT; - let action_group_gap = ToolbarLayoutSpec::SIDE_ACTION_BUTTON_GAP; - let action_col_gap = ToolbarLayoutSpec::SIDE_ACTION_BUTTON_GAP; - let mut action_y = start_y; - let mut has_group = false; - for group in model.groups() { - let actions = build_action_buttons(snapshot, group); - if actions.is_empty() { - continue; - } - if has_group { - action_y += action_group_gap; - } - action_y += draw_group_sub_label(ctx, x, action_y, group.kind); - let (action_w, columns, column_gap, enabled_style) = - text_group_layout(content_width, action_col_gap, group.kind); - let (next_y, has_rows) = render_text_action_group( - ctx, - hits, - hover, - snapshot, - TextActionLayout { - x, - start_y: action_y, - width: action_w, - height: action_h, - column_gap, - group_gap: action_group_gap, - row_gap: action_row_gap, - columns, - add_gap: false, - label_style, - enabled_style, - }, - &actions, - ); - if has_rows { - action_y = next_y; - has_group = true; - } - } -} - -fn build_action_buttons( - snapshot: &ToolbarSnapshot, - group: &ToolbarCommandGroup, -) -> Vec { - group - .buttons - .iter() - .map(|button| ActionButton { - event: button.event.clone(), - icon_fn: icon_for_button(snapshot, button), - enabled: button.enabled, - }) - .collect() -} - -fn icon_group_columns(group: &ToolbarCommandGroup) -> usize { - match group.kind { - ToolbarCommandGroupKind::History => group.buttons.len(), - ToolbarCommandGroupKind::Zoom | ToolbarCommandGroupKind::AdvancedActions => 5, - ToolbarCommandGroupKind::Pages | ToolbarCommandGroupKind::Boards => group.buttons.len(), - } -} - -fn text_group_layout( - content_width: f64, - action_col_gap: f64, - kind: ToolbarCommandGroupKind, -) -> (f64, usize, f64, bool) { - match kind { - ToolbarCommandGroupKind::History => (content_width, 1, 0.0, false), - ToolbarCommandGroupKind::Zoom => ( - row_item_width(content_width, 2, action_col_gap), - 2, - action_col_gap, - true, - ), - ToolbarCommandGroupKind::AdvancedActions => ( - row_item_width(content_width, 2, action_col_gap), - 2, - action_col_gap, - false, - ), - ToolbarCommandGroupKind::Pages | ToolbarCommandGroupKind::Boards => { - (content_width, 1, 0.0, false) - } - } -} - -fn icon_for_button(snapshot: &ToolbarSnapshot, button: &ToolbarButtonModel) -> ActionIconFn { - match &button.event { - ToolbarEvent::Undo => toolbar_icons::draw_icon_undo as ActionIconFn, - ToolbarEvent::Redo => toolbar_icons::draw_icon_redo as ActionIconFn, - ToolbarEvent::ClearCanvas { .. } => toolbar_icons::draw_icon_clear as ActionIconFn, - ToolbarEvent::ZoomIn => toolbar_icons::draw_icon_zoom_in as ActionIconFn, - ToolbarEvent::ZoomOut => toolbar_icons::draw_icon_zoom_out as ActionIconFn, - ToolbarEvent::ResetZoom => toolbar_icons::draw_icon_zoom_reset as ActionIconFn, - ToolbarEvent::ToggleZoomLock => { - if snapshot.zoom_locked { - toolbar_icons::draw_icon_lock as ActionIconFn - } else { - toolbar_icons::draw_icon_unlock as ActionIconFn - } - } - ToolbarEvent::UndoAll => toolbar_icons::draw_icon_undo_all as ActionIconFn, - ToolbarEvent::RedoAll => toolbar_icons::draw_icon_redo_all as ActionIconFn, - ToolbarEvent::UndoAllDelayed => toolbar_icons::draw_icon_undo_all_delay as ActionIconFn, - ToolbarEvent::RedoAllDelayed => toolbar_icons::draw_icon_redo_all_delay as ActionIconFn, - ToolbarEvent::ToggleFreeze => { - if snapshot.frozen_active { - toolbar_icons::draw_icon_unfreeze as ActionIconFn - } else { - toolbar_icons::draw_icon_freeze as ActionIconFn - } - } - _ => toolbar_icons::draw_icon_clear as ActionIconFn, - } -} diff --git a/src/backend/wayland/toolbar/render/side_palette/actions/helpers.rs b/src/backend/wayland/toolbar/render/side_palette/actions/helpers.rs deleted file mode 100644 index b8ec405f..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/actions/helpers.rs +++ /dev/null @@ -1,394 +0,0 @@ -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::format_binding_label; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::rows::{centered_grid_layout, grid_layout}; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot}; -use crate::ui_text::UiTextStyle; - -use super::super::super::widgets::constants::{COLOR_TEXT_DISABLED, set_color}; -use super::super::super::widgets::{ - draw_button, draw_destructive_button, draw_disabled_button, draw_label_center, - draw_label_center_color, point_in_rect, set_icon_color, -}; - -pub(super) type ActionIconFn = fn(&cairo::Context, f64, f64, f64); - -#[derive(Clone)] -pub(super) struct ActionButton { - pub(super) event: ToolbarEvent, - pub(super) icon_fn: ActionIconFn, - pub(super) enabled: bool, -} - -pub(super) struct IconActionLayout { - pub(super) x: f64, - pub(super) content_width: f64, - pub(super) start_y: f64, - pub(super) button_size: f64, - pub(super) icon_size: f64, - pub(super) gap: f64, - pub(super) columns: usize, - pub(super) add_gap: bool, -} - -#[derive(Copy, Clone)] -struct IconActionButtonGeometry { - x: f64, - y: f64, - button_size: f64, - icon_size: f64, -} - -pub(super) struct TextActionLayout { - pub(super) x: f64, - pub(super) start_y: f64, - pub(super) width: f64, - pub(super) height: f64, - pub(super) column_gap: f64, - pub(super) group_gap: f64, - pub(super) row_gap: f64, - pub(super) columns: usize, - pub(super) add_gap: bool, - pub(super) label_style: UiTextStyle<'static>, - pub(super) enabled_style: bool, -} - -#[derive(Copy, Clone)] -struct TextActionButtonGeometry { - x: f64, - y: f64, - width: f64, - height: f64, -} - -struct ActionButtonRenderContext<'a> { - ctx: &'a cairo::Context, - hits: &'a mut Vec, - hover: Option<(f64, f64)>, - snapshot: &'a ToolbarSnapshot, -} - -pub(super) fn render_icon_action_group( - ctx: &cairo::Context, - hits: &mut Vec, - hover: Option<(f64, f64)>, - snapshot: &ToolbarSnapshot, - layout: IconActionLayout, - actions: &[ActionButton], -) -> (f64, bool) { - let mut render_ctx = ActionButtonRenderContext { - ctx, - hits, - hover, - snapshot, - }; - let mut action_y = layout.start_y; - if layout.add_gap { - action_y += layout.gap; - } - - let grid = centered_grid_layout( - layout.x, - layout.content_width, - action_y, - layout.button_size, - layout.gap, - layout.columns, - actions.len(), - ); - for (item, action) in grid.items.iter().zip(actions.iter()) { - render_icon_action_button( - &mut render_ctx, - IconActionButtonGeometry { - x: item.x, - y: item.y, - button_size: layout.button_size, - icon_size: layout.icon_size, - }, - action, - ); - } - - if grid.rows > 0 { - (action_y + grid.height, true) - } else { - (action_y, false) - } -} - -/// One left-aligned run of square icon buttons plus a right-aligned run -/// (destructive actions), separated by the leftover width — the mockup's -/// "Undo Redo ......... Clear" row. -pub(super) fn render_icon_action_row_split( - ctx: &cairo::Context, - hits: &mut Vec, - hover: Option<(f64, f64)>, - snapshot: &ToolbarSnapshot, - layout: IconActionLayout, - leading: &[ActionButton], - trailing: &[ActionButton], -) -> (f64, bool) { - if leading.is_empty() && trailing.is_empty() { - return (layout.start_y, false); - } - let mut render_ctx = ActionButtonRenderContext { - ctx, - hits, - hover, - snapshot, - }; - let mut action_y = layout.start_y; - if layout.add_gap { - action_y += layout.gap; - } - - for (index, action) in leading.iter().enumerate() { - let bx = layout.x + index as f64 * (layout.button_size + layout.gap); - render_icon_action_button( - &mut render_ctx, - IconActionButtonGeometry { - x: bx, - y: action_y, - button_size: layout.button_size, - icon_size: layout.icon_size, - }, - action, - ); - } - for (index, action) in trailing.iter().enumerate() { - let bx = layout.x + layout.content_width - - (index + 1) as f64 * layout.button_size - - index as f64 * layout.gap; - render_icon_action_button( - &mut render_ctx, - IconActionButtonGeometry { - x: bx, - y: action_y, - button_size: layout.button_size, - icon_size: layout.icon_size, - }, - action, - ); - } - - (action_y + layout.button_size, true) -} - -pub(super) fn render_text_action_group( - ctx: &cairo::Context, - hits: &mut Vec, - hover: Option<(f64, f64)>, - snapshot: &ToolbarSnapshot, - layout: TextActionLayout, - actions: &[ActionButton], -) -> (f64, bool) { - let mut render_ctx = ActionButtonRenderContext { - ctx, - hits, - hover, - snapshot, - }; - let mut action_y = layout.start_y; - if layout.add_gap { - action_y += layout.group_gap; - } - - let grid = grid_layout( - layout.x, - action_y, - layout.width, - layout.height, - layout.column_gap, - layout.row_gap, - layout.columns, - actions.len(), - ); - for (item, action) in grid.items.iter().zip(actions.iter()) { - render_text_action_button( - &mut render_ctx, - layout.label_style, - TextActionButtonGeometry { - x: item.x, - y: item.y, - width: item.w, - height: item.h, - }, - action, - layout.enabled_style, - ); - } - - if grid.rows > 0 { - (action_y + grid.height, true) - } else { - (action_y, false) - } -} - -pub(super) fn button_label(event: &ToolbarEvent, snapshot: &ToolbarSnapshot) -> &'static str { - event.short_label(snapshot, "Action") -} - -fn tooltip_label(event: &ToolbarEvent, snapshot: &ToolbarSnapshot) -> &'static str { - event.tooltip_label(snapshot, "Action") -} - -fn render_icon_action_button( - render_ctx: &mut ActionButtonRenderContext<'_>, - geometry: IconActionButtonGeometry, - action: &ActionButton, -) { - let is_hover = render_ctx - .hover - .map(|(hx, hy)| { - point_in_rect( - hx, - hy, - geometry.x, - geometry.y, - geometry.button_size, - geometry.button_size, - ) - }) - .unwrap_or(false); - - let is_destructive = action.event.is_destructive(); - if action.enabled { - if is_destructive { - draw_destructive_button( - render_ctx.ctx, - geometry.x, - geometry.y, - geometry.button_size, - geometry.button_size, - is_hover, - ); - } else { - draw_button( - render_ctx.ctx, - geometry.x, - geometry.y, - geometry.button_size, - geometry.button_size, - false, - is_hover, - ); - } - set_icon_color(render_ctx.ctx, is_hover); - } else { - draw_disabled_button( - render_ctx.ctx, - geometry.x, - geometry.y, - geometry.button_size, - geometry.button_size, - ); - set_color(render_ctx.ctx, COLOR_TEXT_DISABLED); - } - - let icon_x = geometry.x + (geometry.button_size - geometry.icon_size) / 2.0; - let icon_y = geometry.y + (geometry.button_size - geometry.icon_size) / 2.0; - (action.icon_fn)(render_ctx.ctx, icon_x, icon_y, geometry.icon_size); - - if action.enabled { - render_ctx.hits.push(HitRegion { - focus_id: None, - rect: ( - geometry.x, - geometry.y, - geometry.button_size, - geometry.button_size, - ), - event: action.event.clone(), - kind: HitKind::Click, - tooltip: Some(format_binding_label( - tooltip_label(&action.event, render_ctx.snapshot), - render_ctx - .snapshot - .binding_hints - .binding_for_event(&action.event), - )), - }); - } -} - -fn render_text_action_button( - render_ctx: &mut ActionButtonRenderContext<'_>, - label_style: UiTextStyle<'static>, - layout: TextActionButtonGeometry, - action: &ActionButton, - enabled_style: bool, -) { - let label = button_label(&action.event, render_ctx.snapshot); - let is_hover = render_ctx - .hover - .map(|(hx, hy)| point_in_rect(hx, hy, layout.x, layout.y, layout.width, layout.height)) - .unwrap_or(false); - - if !action.enabled { - draw_disabled_button( - render_ctx.ctx, - layout.x, - layout.y, - layout.width, - layout.height, - ); - } else if action.event.is_destructive() { - draw_destructive_button( - render_ctx.ctx, - layout.x, - layout.y, - layout.width, - layout.height, - is_hover, - ); - } else { - draw_button( - render_ctx.ctx, - layout.x, - layout.y, - layout.width, - layout.height, - enabled_style, - is_hover, - ); - } - - if action.enabled { - draw_label_center( - render_ctx.ctx, - label_style, - layout.x, - layout.y, - layout.width, - layout.height, - label, - ); - } else { - draw_label_center_color( - render_ctx.ctx, - label_style, - layout.x, - layout.y, - layout.width, - layout.height, - label, - COLOR_TEXT_DISABLED, - ); - } - if action.enabled { - render_ctx.hits.push(HitRegion { - focus_id: None, - rect: (layout.x, layout.y, layout.width, layout.height), - event: action.event.clone(), - kind: HitKind::Click, - tooltip: Some(format_binding_label( - tooltip_label(&action.event, render_ctx.snapshot), - render_ctx - .snapshot - .binding_hints - .binding_for_event(&action.event), - )), - }); - } -} diff --git a/src/backend/wayland/toolbar/render/side_palette/arrow.rs b/src/backend/wayland/toolbar/render/side_palette/arrow.rs deleted file mode 100644 index 4929a4ce..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/arrow.rs +++ /dev/null @@ -1,118 +0,0 @@ -use super::super::widgets::constants::{ - COLOR_LABEL_HINT, FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, FONT_SIZE_SMALL, set_color, -}; -use super::super::widgets::*; -use super::SidePaletteLayout; -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::ui::toolbar::{ToolContext, ToolbarEvent, ToolbarSideSection}; -use crate::ui_text::{UiTextStyle, text_layout}; - -use super::section_header::draw_collapsible_header; - -pub(super) fn draw_arrow_section(layout: &mut SidePaletteLayout, y: &mut f64) { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let hover = layout.hover; - let x = layout.x; - let card_x = layout.card_x; - let card_w = layout.card_w; - let content_width = layout.content_width; - let section_gap = layout.section_gap; - let label_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_LABEL, - }; - let hint_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Normal, - size: FONT_SIZE_SMALL, - }; - - if snapshot.side_section_hidden(ToolbarSideSection::ArrowLabels) - || !ToolContext::from_snapshot(snapshot).show_arrow_labels - { - return; - } - - let card_h = layout.spec.side_arrow_labels_height(snapshot); - draw_group_card(ctx, card_x, *y, card_w, card_h); - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::ArrowLabels, - "Arrow labels", - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL, - ); - if snapshot.arrow_label_enabled { - let hint = format!("Next: {}", snapshot.arrow_label_next); - let layout = text_layout(ctx, hint_style, &hint, None); - let ext = layout.ink_extents(); - let chevron_reserve = ToolbarLayoutSpec::SIDE_COLLAPSE_CHEVRON_SIZE + 10.0; - let hint_x = card_x + card_w - ext.width() - chevron_reserve - ext.x_bearing(); - let hint_y = *y + ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL; - set_color(ctx, COLOR_LABEL_HINT); - layout.show_at_baseline(ctx, hint_x, hint_y); - } - if snapshot.side_section_collapsed(ToolbarSideSection::ArrowLabels) { - *y += card_h + section_gap; - return; - } - - let hits = &mut layout.hits; - let toggle_h = ToolbarLayoutSpec::SIDE_TOGGLE_HEIGHT; - let toggle_y = *y + ToolbarLayoutSpec::SIDE_SECTION_TOGGLE_OFFSET_Y; - let toggle_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, toggle_y, content_width, toggle_h)) - .unwrap_or(false); - draw_checkbox( - ctx, - x, - toggle_y, - content_width, - toggle_h, - snapshot.arrow_label_enabled, - toggle_hover, - label_style, - "Auto-number", - ); - hits.push(HitRegion { - focus_id: None, - rect: (x, toggle_y, content_width, toggle_h), - event: ToolbarEvent::ToggleArrowLabels(!snapshot.arrow_label_enabled), - kind: HitKind::Click, - tooltip: Some("Auto-number arrows 1, 2, 3.".to_string()), - }); - - if snapshot.arrow_label_enabled { - let reset_y = - toggle_y + ToolbarLayoutSpec::SIDE_TOGGLE_HEIGHT + ToolbarLayoutSpec::SIDE_TOGGLE_GAP; - let reset_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, reset_y, content_width, toggle_h)) - .unwrap_or(false); - draw_button(ctx, x, reset_y, content_width, toggle_h, false, reset_hover); - draw_label_center( - ctx, - label_style, - x, - reset_y, - content_width, - toggle_h, - "Reset", - ); - hits.push(HitRegion { - focus_id: None, - rect: (x, reset_y, content_width, toggle_h), - event: ToolbarEvent::ResetArrowLabelCounter, - kind: HitKind::Click, - tooltip: Some("Reset numbering to 1.".to_string()), - }); - } - - *y += card_h + section_gap; -} diff --git a/src/backend/wayland/toolbar/render/side_palette/boards.rs b/src/backend/wayland/toolbar/render/side_palette/boards.rs deleted file mode 100644 index 0560a776..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/boards.rs +++ /dev/null @@ -1,165 +0,0 @@ -use super::SidePaletteLayout; -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::format_binding_label; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::backend::wayland::toolbar::rows::capped_grid_columns; -use crate::toolbar_icons; -use crate::ui::toolbar::model::toolbar_boards_model; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection}; -use crate::ui_text::UiTextStyle; - -use super::super::widgets::constants::{ - COLOR_TEXT_DISABLED, FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, set_color, -}; -use super::super::widgets::*; -use super::section_header::draw_collapsible_header; -use crate::ui::theme::Rgba; - -/// Right-aligned keybinding hint next to the section label. Muted -/// blue-gray with no theme token. -const COLOR_BINDING_HINT: Rgba = (0.55, 0.58, 0.65, 0.9); - -pub(super) fn draw_boards_section(layout: &mut SidePaletteLayout, y: &mut f64) { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let hover = layout.hover; - let x = layout.x; - let card_x = layout.card_x; - let card_w = layout.card_w; - let content_width = layout.content_width; - let section_gap = layout.section_gap; - let use_icons = snapshot.use_icons; - let label_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_LABEL, - }; - - let Some(model) = toolbar_boards_model(snapshot) else { - return; - }; - - let boards_card_h = layout.spec.side_boards_height(snapshot); - draw_group_card(ctx, card_x, *y, card_w, boards_card_h); - - // Section label with keybinding hint - let label_y = *y + ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL; - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::Boards, - ToolbarSideSection::Boards.label(), - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL, - ); - - // Draw keybinding hint for board picker (right-aligned) - if let Some(binding) = snapshot - .binding_hints - .binding_for_event(&ToolbarEvent::ToggleBoardPicker) - { - let hint_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Normal, - size: FONT_SIZE_LABEL * 0.85, - }; - let hint_layout = crate::ui_text::text_layout(ctx, hint_style, binding, None); - let hint_width = hint_layout.ink_extents().width(); - let chevron_reserve = ToolbarLayoutSpec::SIDE_COLLAPSE_CHEVRON_SIZE + 10.0; - let hint_x = x + content_width - hint_width - chevron_reserve; - set_color(ctx, COLOR_BINDING_HINT); - hint_layout.show_at_baseline(ctx, hint_x, label_y); - } - if snapshot.side_section_collapsed(ToolbarSideSection::Boards) { - *y += boards_card_h + section_gap; - return; - } - - let hits = &mut layout.hits; - let boards_y = *y + ToolbarLayoutSpec::SIDE_SECTION_TOGGLE_OFFSET_Y; - let btn_h = if use_icons { - ToolbarLayoutSpec::SIDE_ACTION_BUTTON_HEIGHT_ICON - } else { - ToolbarLayoutSpec::SIDE_ACTION_BUTTON_HEIGHT_TEXT - }; - let btn_gap = ToolbarLayoutSpec::SIDE_ACTION_BUTTON_GAP; - let rects = super::side_row_button_rects( - super::SideRowLayout { - x, - row_y: boards_y, - content_width, - btn_h, - btn_gap, - use_icons, - text_columns: capped_grid_columns(model.buttons.len(), 5), - }, - &model.buttons, - ); - for (rect, button) in rects.iter().zip(model.buttons.iter()) { - let label = button.short_label(snapshot, "Board"); - let (bx, by, btn_w, btn_h) = *rect; - let is_hover = button.enabled - && hover - .map(|(hx, hy)| point_in_rect(hx, hy, bx, by, btn_w, btn_h)) - .unwrap_or(false); - if !button.enabled { - draw_disabled_button(ctx, bx, by, btn_w, btn_h); - } else if button.event.is_destructive() { - draw_destructive_button(ctx, bx, by, btn_w, btn_h, is_hover); - } else { - draw_button(ctx, bx, by, btn_w, btn_h, false, is_hover); - } - if use_icons { - if button.enabled { - set_icon_color(ctx, is_hover); - } else { - set_color(ctx, COLOR_TEXT_DISABLED); - } - let icon_size = ToolbarLayoutSpec::SIDE_ACTION_ICON_SIZE; - let icon_x = bx + (btn_w - icon_size) / 2.0; - let icon_y = by + (btn_h - icon_size) / 2.0; - board_icon(&button.event)(ctx, icon_x, icon_y, icon_size); - } else if button.enabled { - draw_label_center(ctx, label_style, bx, by, btn_w, btn_h, label); - } else { - draw_label_center_color( - ctx, - label_style, - bx, - by, - btn_w, - btn_h, - label, - COLOR_TEXT_DISABLED, - ); - } - if button.enabled { - hits.push(HitRegion { - focus_id: None, - rect: (bx, by, btn_w, btn_h), - event: button.event.clone(), - kind: HitKind::Click, - tooltip: Some(format_binding_label( - button.tooltip_label(snapshot, "Board"), - button.binding_hint(snapshot), - )), - }); - } - } - - *y += boards_card_h + section_gap; -} - -fn board_icon(event: &ToolbarEvent) -> fn(&cairo::Context, f64, f64, f64) { - match event { - ToolbarEvent::BoardPrev => toolbar_icons::draw_icon_chevron_left, - ToolbarEvent::BoardNext => toolbar_icons::draw_icon_chevron_right, - ToolbarEvent::BoardNew => toolbar_icons::draw_icon_plus, - ToolbarEvent::BoardDuplicate => toolbar_icons::draw_icon_copy, - ToolbarEvent::BoardDelete => toolbar_icons::draw_icon_clear, - _ => toolbar_icons::draw_icon_clear, - } -} diff --git a/src/backend/wayland/toolbar/render/side_palette/colors.rs b/src/backend/wayland/toolbar/render/side_palette/colors.rs deleted file mode 100644 index 86b34bf9..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/colors.rs +++ /dev/null @@ -1,307 +0,0 @@ -mod helpers; - -use super::{ColorSectionInfo, SidePaletteLayout}; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::config::{QuickColorPalette, QuickColorPaletteEntry}; -use crate::draw::Color; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection}; -use crate::ui_text::UiTextStyle; - -use super::super::widgets::constants::{FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL}; -use super::super::widgets::{draw_group_card, draw_round_rect}; -use super::section_header::draw_collapsible_header; -use helpers::{ - ColorSwatch, ColorSwatchRowLayout, ColorSwatchToggle, draw_color_picker_area, - draw_color_swatch_row, draw_hex_input, draw_preview_swatch_and_icon, -}; - -pub(super) fn draw_colors_section( - layout: &mut SidePaletteLayout, - y: &mut f64, -) -> Option { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let hover = layout.hover; - let x = layout.x; - let card_x = layout.card_x; - let card_w = layout.card_w; - let content_width = layout.content_width; - let section_gap = layout.section_gap; - let label_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_LABEL, - }; - - if snapshot.side_section_hidden(ToolbarSideSection::Colors) { - return None; - } - - let compact_colors = compact_palette_swatches(&snapshot.quick_colors); - let expanded_colors = expanded_palette_swatches(&snapshot.quick_colors); - - let swatch = ToolbarLayoutSpec::SIDE_COLOR_SWATCH; - let swatch_gap = ToolbarLayoutSpec::SIDE_COLOR_SWATCH_GAP; - let colors_card_h = layout.spec.side_colors_height(snapshot); - - let title = super::section_header::scoped_title("Color", snapshot); - draw_group_card(ctx, card_x, *y, card_w, colors_card_h); - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::Colors, - &title, - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_Y, - ); - if snapshot.side_section_collapsed(ToolbarSideSection::Colors) { - *y += colors_card_h + section_gap; - return None; - } - - let hits = &mut layout.hits; - let picker_y = *y + ToolbarLayoutSpec::SIDE_COLOR_PICKER_OFFSET_Y; - let picker_w = content_width; - let picker_h = draw_color_picker_area(ctx, hits, snapshot, x, picker_y, picker_w); - let (preview_row_y, preview_size) = - draw_preview_swatch_and_icon(ctx, hits, hover, snapshot, x, picker_y, picker_h); - - // Shared with the popup and the GTK entry so all three readouts agree, - // including the eight-digit form a translucent color needs. - let hex = crate::input::state::color_to_hex(snapshot.color); - draw_hex_input(ctx, hits, hover, x, preview_row_y, preview_size, &hex); - - let mut row_y = preview_row_y + preview_size + ToolbarLayoutSpec::SIDE_COLOR_PREVIEW_GAP_BOTTOM; - let basic_toggle = if snapshot.show_more_colors || expanded_colors.is_empty() { - None - } else { - Some(ColorSwatchToggle { - event: ToolbarEvent::ToggleMoreColors(true), - tooltip: "More colors", - icon_fn: crate::toolbar_icons::draw_icon_plus, - }) - }; - draw_color_swatch_row( - ctx, - hits, - hover, - snapshot, - ColorSwatchRowLayout { - start_x: x, - row_y, - swatch, - swatch_gap, - }, - &compact_colors, - basic_toggle, - ); - - row_y += swatch + swatch_gap; - if snapshot.show_more_colors { - let rows = expanded_colors - .chunks(ToolbarLayoutSpec::SIDE_COLOR_SWATCHES_PER_ROW) - .collect::>(); - for (row_index, row_colors) in rows.iter().enumerate() { - let is_last_row = row_index + 1 == rows.len(); - draw_color_swatch_row( - ctx, - hits, - hover, - snapshot, - ColorSwatchRowLayout { - start_x: x, - row_y, - swatch, - swatch_gap, - }, - row_colors, - is_last_row.then_some(ColorSwatchToggle { - event: ToolbarEvent::ToggleMoreColors(false), - tooltip: "Hide colors", - icon_fn: crate::toolbar_icons::draw_icon_minus, - }), - ); - row_y += swatch + swatch_gap; - } - } - - *y += colors_card_h + section_gap; - - Some(ColorSectionInfo { - picker_y, - picker_w, - picker_h, - }) -} - -#[cfg(test)] -fn palette_swatches(palette: &QuickColorPalette) -> Vec { - palette - .rendered_entries() - .iter() - .enumerate() - .map(palette_swatch) - .collect() -} - -const COMPACT_PALETTE_INDICES: [usize; ToolbarLayoutSpec::SIDE_COLOR_SWATCHES_PER_ROW] = - [0, 1, 2, 3, 6, 7]; - -fn compact_palette_swatches(palette: &QuickColorPalette) -> Vec { - let entries = palette.rendered_entries(); - compact_palette_indices(palette) - .into_iter() - .filter_map(|index| { - entries - .get(index) - .map(|entry| palette_swatch((index, entry))) - }) - .collect() -} - -fn compact_palette_indices(palette: &QuickColorPalette) -> Vec { - let mut indices = Vec::with_capacity(ToolbarLayoutSpec::SIDE_COLOR_SWATCHES_PER_ROW); - let rendered_len = palette.rendered_len(); - for index in COMPACT_PALETTE_INDICES { - if index < rendered_len { - indices.push(index); - } - } - - if indices.len() < ToolbarLayoutSpec::SIDE_COLOR_SWATCHES_PER_ROW { - for index in 0..rendered_len { - if indices.contains(&index) { - continue; - } - indices.push(index); - if indices.len() == ToolbarLayoutSpec::SIDE_COLOR_SWATCHES_PER_ROW { - break; - } - } - } - - indices -} - -fn expanded_palette_swatches(palette: &QuickColorPalette) -> Vec { - let compact_indices = compact_palette_indices(palette); - palette - .rendered_entries() - .iter() - .enumerate() - .filter(|(index, _)| !compact_indices.contains(index)) - .map(palette_swatch) - .collect() -} - -fn palette_swatch((index, entry): (usize, &QuickColorPaletteEntry)) -> ColorSwatch { - ( - entry.color, - entry.label.clone(), - QuickColorPalette::action_for_index(index), - index, - ) -} - -pub(super) fn draw_preset_hover_highlight( - layout: &SidePaletteLayout, - info: &ColorSectionInfo, - color: Color, -) { - let ctx = layout.ctx; - let x = layout.x; - - ctx.set_source_rgba(color.r, color.g, color.b, 0.85); - ctx.set_line_width(2.0); - draw_round_rect( - ctx, - x - 2.0, - info.picker_y - 2.0, - info.picker_w + 4.0, - info.picker_h + 4.0, - 6.0, - ); - let _ = ctx.stroke(); -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::config::{QUICK_COLOR_RENDER_LIMIT, QuickColorPaletteEntry}; - - #[test] - fn palette_swatches_bind_only_first_eight_quick_color_actions() { - let swatches = palette_swatches(&QuickColorPalette::default()); - - assert_eq!(swatches[0].2, Some(crate::config::Action::SetColorRed)); - assert_eq!(swatches[7].2, Some(crate::config::Action::SetColorBlack)); - assert_eq!(swatches[8].1, "Cyan"); - assert_eq!(swatches[8].2, None); - assert_eq!(swatches[9].2, None); - assert_eq!(swatches[10].2, None); - } - - #[test] - fn compact_palette_swatches_preserve_legacy_one_click_colors() { - let swatches = compact_palette_swatches(&QuickColorPalette::default()); - let labels = swatches - .iter() - .map(|(_, label, _, _)| label.as_str()) - .collect::>(); - - assert_eq!(labels, ["Red", "Green", "Blue", "Yellow", "White", "Black"]); - assert_eq!(swatches[0].2, Some(crate::config::Action::SetColorRed)); - assert_eq!(swatches[3].2, Some(crate::config::Action::SetColorYellow)); - assert_eq!(swatches[4].2, Some(crate::config::Action::SetColorWhite)); - assert_eq!(swatches[5].2, Some(crate::config::Action::SetColorBlack)); - - // The compact row reorders the palette, so each swatch must carry the - // slot it actually renders for the recolor gesture to hit the right one. - let indices = swatches - .iter() - .map(|(_, _, _, index)| *index) - .collect::>(); - assert_eq!(indices, COMPACT_PALETTE_INDICES); - } - - #[test] - fn expanded_palette_swatches_skip_compact_colors_without_losing_actions() { - let swatches = expanded_palette_swatches(&QuickColorPalette::default()); - let labels = swatches - .iter() - .map(|(_, label, _, _)| label.as_str()) - .collect::>(); - - assert_eq!(labels, ["Orange", "Pink", "Cyan", "Purple", "Gray"]); - assert_eq!(swatches[0].2, Some(crate::config::Action::SetColorOrange)); - assert_eq!(swatches[1].2, Some(crate::config::Action::SetColorPink)); - assert_eq!(swatches[2].2, None); - - // Expanded rows start past the compact slots; an action-less slot - // still knows its palette index. - assert_eq!(swatches[0].3, 4); - assert_eq!(swatches[2].3, 8); - } - - #[test] - fn palette_swatches_respect_render_limit() { - let palette = QuickColorPalette::from_entries( - (0..QUICK_COLOR_RENDER_LIMIT + 1) - .map(|index| QuickColorPaletteEntry { - label: format!("Color {index}"), - color: crate::draw::color::RED, - }) - .collect(), - ); - - let swatches = palette_swatches(&palette); - - assert_eq!(swatches.len(), QUICK_COLOR_RENDER_LIMIT); - assert_eq!( - palette.len(), - QUICK_COLOR_RENDER_LIMIT + 1, - "render cap should not truncate the runtime palette" - ); - } -} diff --git a/src/backend/wayland/toolbar/render/side_palette/colors/helpers.rs b/src/backend/wayland/toolbar/render/side_palette/colors/helpers.rs deleted file mode 100644 index 1094cdae..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/colors/helpers.rs +++ /dev/null @@ -1,508 +0,0 @@ -use std::f64::consts::PI; - -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::format_quick_color_tooltip; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::config::Action; -use crate::draw::Color; -use crate::toolbar_icons; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot}; -use crate::ui_text::UiTextStyle; - -use super::super::super::widgets::constants::{ - COLOR_TEXT_SECONDARY, COLOR_TEXT_TERTIARY, FONT_FAMILY_DEFAULT, FONT_SIZE_SECONDARY, set_color, -}; -use super::super::super::widgets::{set_icon_color, *}; -use crate::ui::theme::Rgba; - -/// Preview swatch hover: white halo fill plus a brighter outline. -const COLOR_PREVIEW_HOVER_HALO: Rgba = (1.0, 1.0, 1.0, 0.3); -const COLOR_PREVIEW_BORDER_HOVER: Rgba = (1.0, 1.0, 1.0, 0.7); -/// Preview swatch idle outline: muted blue-gray with no theme token. -const COLOR_PREVIEW_BORDER: Rgba = (0.5, 0.55, 0.6, 0.6); -/// Dark circle behind the expand arrow; deeper than SHADOW_RGBA so the -/// glyph stays legible over any swatch color. -const COLOR_EXPAND_ICON_BACKDROP: Rgba = (0.0, 0.0, 0.0, 0.4); -/// Hex chip hover wash (fainter than COLOR_DIVIDER white). -const COLOR_HEX_HOVER_WASH: Rgba = (1.0, 1.0, 1.0, 0.06); -/// Hex chip fill (hover/idle). Hover is near COLOR_CHECKBOX_HOVER but not -/// equal — kept to avoid a visible shift. -const COLOR_HEX_BG_HOVER: Rgba = (0.35, 0.35, 0.4, 0.9); -const COLOR_HEX_BG: Rgba = (0.2, 0.2, 0.2, 0.6); -/// Hex chip hover outline (COLOR_CLOSE_DEFAULT gray at -0.1 alpha). -const COLOR_HEX_BORDER_HOVER: Rgba = (0.5, 0.5, 0.55, 0.6); -/// Copy icon at rest (brightens to COLOR_TEXT_TERTIARY on hover). -const COLOR_COPY_ICON_IDLE: Rgba = (0.6, 0.6, 0.6, 0.5); -/// Hex value text: dimmer than COLOR_ICON_DEFAULT — kept as-is. -const COLOR_HEX_TEXT: Rgba = (0.85, 0.85, 0.85, 1.0); - -/// Copy-icon box inside the hex chip, and its inset from the chip's right -/// edge. The remaining width is what the hex text is centered in, so the chip -/// width has to cover both (pinned by a test against the widest hex string). -const HEX_ICON_SIZE: f64 = 10.0; -const HEX_ICON_PAD: f64 = 4.0; - -fn hex_text_style() -> UiTextStyle<'static> { - UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Normal, - size: FONT_SIZE_SECONDARY, - } -} - -/// Width the hex value is centered in: the chip minus its copy icon. -fn hex_text_area_width() -> f64 { - ToolbarLayoutSpec::SIDE_COLOR_HEX_INPUT_WIDTH - HEX_ICON_SIZE - HEX_ICON_PAD -} - -/// `(color, label, bound quick-color action, palette slot index)`. The index -/// travels with the swatch so a secondary click can recolor that exact slot, -/// including the slots past the eighth that carry no action. -pub(super) type ColorSwatch = (Color, String, Option, usize); -pub(super) type ColorToggleIconFn = fn(&cairo::Context, f64, f64, f64); - -#[derive(Clone)] -pub(super) struct ColorSwatchToggle { - pub(super) event: ToolbarEvent, - pub(super) tooltip: &'static str, - pub(super) icon_fn: ColorToggleIconFn, -} - -#[derive(Copy, Clone)] -pub(super) struct ColorSwatchRowLayout { - pub(super) start_x: f64, - pub(super) row_y: f64, - pub(super) swatch: f64, - pub(super) swatch_gap: f64, -} - -/// HSV triple the picker should display. The remembered picker triple wins -/// while it still resolves to the current color, so hue and saturation stay -/// put when the RGB value collapses to gray/black; any other color source -/// (swatch, hex, preset) falls back to a plain RGB→HSV conversion. -pub(super) fn effective_hsv(snapshot: &ToolbarSnapshot) -> (f64, f64, f64) { - if let Some((h, s, v)) = snapshot.picker_hsv { - let remembered = crate::draw::color::hsv_to_rgb(h, s, v); - let current = snapshot.color; - if (remembered.r - current.r).abs() < 1e-3 - && (remembered.g - current.g).abs() < 1e-3 - && (remembered.b - current.b).abs() < 1e-3 - { - return (h, s, v); - } - } - crate::draw::color::rgb_to_hsv(snapshot.color.r, snapshot.color.g, snapshot.color.b) -} - -/// Draw the 2-D saturation/value area plus the hue bar; returns the total -/// block height so the rows below can stack after it. -pub(super) fn draw_color_picker_area( - ctx: &cairo::Context, - hits: &mut Vec, - snapshot: &ToolbarSnapshot, - x: f64, - picker_y: f64, - picker_w: f64, -) -> f64 { - let sv_h = ToolbarLayoutSpec::SIDE_COLOR_SV_HEIGHT; - let hue_h = ToolbarLayoutSpec::SIDE_COLOR_HUE_HEIGHT; - let hue_gap = ToolbarLayoutSpec::SIDE_COLOR_HUE_GAP; - let (h, s, v) = effective_hsv(snapshot); - - draw_sat_val_area(ctx, x, picker_y, picker_w, sv_h, h); - hits.push(HitRegion { - focus_id: None, - rect: (x, picker_y, picker_w, sv_h), - event: ToolbarEvent::SetColorHsv { h, s, v }, - kind: HitKind::PickSatVal { hue: h }, - tooltip: None, - }); - draw_color_indicator( - ctx, - x + s * picker_w, - picker_y + (1.0 - v) * sv_h, - snapshot.color, - ); - - let hue_y = picker_y + sv_h + hue_gap; - draw_hue_bar(ctx, x, hue_y, picker_w, hue_h); - hits.push(HitRegion { - focus_id: None, - rect: (x, hue_y, picker_w, hue_h), - event: ToolbarEvent::SetColorHsv { h, s, v }, - kind: HitKind::PickHue { sat: s, val: v }, - tooltip: None, - }); - let hue_color = crate::draw::color::hsv_to_rgb(h, 1.0, 1.0); - draw_color_indicator(ctx, x + h * picker_w, hue_y + hue_h / 2.0, hue_color); - - sv_h + hue_gap + hue_h -} - -pub(super) fn draw_preview_swatch_and_icon( - ctx: &cairo::Context, - hits: &mut Vec, - hover: Option<(f64, f64)>, - snapshot: &ToolbarSnapshot, - x: f64, - picker_y: f64, - picker_h: f64, -) -> (f64, f64) { - let preview_row_y = picker_y + picker_h + ToolbarLayoutSpec::SIDE_COLOR_PREVIEW_GAP_TOP; - let preview_size = ToolbarLayoutSpec::SIDE_COLOR_PREVIEW_SIZE; - let preview_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, preview_row_y, preview_size, preview_size)) - .unwrap_or(false); - - if preview_hover { - set_color(ctx, COLOR_PREVIEW_HOVER_HALO); - draw_round_rect( - ctx, - x - 2.0, - preview_row_y - 2.0, - preview_size + 4.0, - preview_size + 4.0, - 6.0, - ); - let _ = ctx.fill(); - set_color(ctx, COLOR_PREVIEW_BORDER_HOVER); - ctx.set_line_width(1.5); - } else { - set_color(ctx, COLOR_PREVIEW_BORDER); - ctx.set_line_width(1.0); - } - draw_round_rect( - ctx, - x - 1.0, - preview_row_y - 1.0, - preview_size + 2.0, - preview_size + 2.0, - 5.0, - ); - let _ = ctx.stroke(); - - // Not a selection: hover feedback comes from the highlight drawn above, - // so the accent "active" ring never appears on the preview swatch. - draw_swatch(ctx, x, preview_row_y, preview_size, snapshot.color, false); - - let icon_size = ToolbarLayoutSpec::SIDE_COLOR_EXPAND_ICON_SIZE; - let icon_x = x + preview_size - icon_size - 2.0; - let icon_y = preview_row_y + preview_size - icon_size - 2.0; - set_color(ctx, COLOR_EXPAND_ICON_BACKDROP); - ctx.arc( - icon_x + icon_size / 2.0, - icon_y + icon_size / 2.0, - icon_size / 2.0 + 1.0, - 0.0, - PI * 2.0, - ); - let _ = ctx.fill(); - set_color(ctx, COLOR_TEXT_SECONDARY); - ctx.set_line_width(1.2); - ctx.set_line_cap(cairo::LineCap::Round); - let arrow_margin = icon_size * 0.2; - let arrow_x1 = icon_x + arrow_margin; - let arrow_y1 = icon_y + icon_size - arrow_margin; - let arrow_x2 = icon_x + icon_size - arrow_margin; - let arrow_y2 = icon_y + arrow_margin; - ctx.move_to(arrow_x1, arrow_y1); - ctx.line_to(arrow_x2, arrow_y2); - let _ = ctx.stroke(); - let head_len = icon_size * 0.3; - ctx.move_to(arrow_x2 - head_len, arrow_y2); - ctx.line_to(arrow_x2, arrow_y2); - ctx.line_to(arrow_x2, arrow_y2 + head_len); - let _ = ctx.stroke(); - - hits.push(HitRegion { - focus_id: None, - rect: (x, preview_row_y, preview_size, preview_size), - event: ToolbarEvent::OpenColorPickerPopup, - kind: HitKind::Click, - tooltip: Some("Click to pick color".to_string()), - }); - - (preview_row_y, preview_size) -} - -pub(super) fn draw_hex_input( - ctx: &cairo::Context, - hits: &mut Vec, - hover: Option<(f64, f64)>, - x: f64, - preview_row_y: f64, - preview_size: f64, - hex: &str, -) { - let hex_style = hex_text_style(); - - let hex_input_x = x + preview_size + 8.0; - let hex_input_h = ToolbarLayoutSpec::SIDE_COLOR_HEX_INPUT_HEIGHT; - let hex_input_y = preview_row_y + (preview_size - hex_input_h) / 2.0; - let hex_input_w = ToolbarLayoutSpec::SIDE_COLOR_HEX_INPUT_WIDTH; - let hex_icon_size = HEX_ICON_SIZE; - let hex_icon_pad = HEX_ICON_PAD; - - let hex_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, hex_input_x, hex_input_y, hex_input_w, hex_input_h)) - .unwrap_or(false); - - if hex_hover { - set_color(ctx, COLOR_HEX_HOVER_WASH); - draw_round_rect( - ctx, - hex_input_x - 1.0, - hex_input_y - 1.0, - hex_input_w + 2.0, - hex_input_h + 2.0, - 5.0, - ); - let _ = ctx.fill(); - set_color(ctx, COLOR_HEX_BG_HOVER); - } else { - set_color(ctx, COLOR_HEX_BG); - } - draw_round_rect(ctx, hex_input_x, hex_input_y, hex_input_w, hex_input_h, 4.0); - let _ = ctx.fill(); - - if hex_hover { - set_color(ctx, COLOR_HEX_BORDER_HOVER); - ctx.set_line_width(1.0); - draw_round_rect(ctx, hex_input_x, hex_input_y, hex_input_w, hex_input_h, 4.0); - let _ = ctx.stroke(); - } - - let copy_icon_x = hex_input_x + hex_input_w - hex_icon_size - hex_icon_pad; - let copy_icon_y = hex_input_y + (hex_input_h - hex_icon_size) / 2.0; - if hex_hover { - set_color(ctx, COLOR_TEXT_TERTIARY); - } else { - set_color(ctx, COLOR_COPY_ICON_IDLE); - } - toolbar_icons::draw_icon_copy(ctx, copy_icon_x, copy_icon_y, hex_icon_size); - - set_color(ctx, COLOR_HEX_TEXT); - let hex_layout = crate::ui_text::text_layout(ctx, hex_style, hex, None); - let hex_extents = hex_layout.ink_extents(); - let text_area_w = hex_text_area_width(); - hex_layout.show_at_baseline( - ctx, - hex_input_x + (text_area_w - hex_extents.width()) / 2.0, - hex_input_y + hex_input_h / 2.0 + hex_extents.height() / 2.0, - ); - - // The copy icon is pushed first so it wins the overlap with the chip. - hits.push(HitRegion { - focus_id: None, - rect: (copy_icon_x, copy_icon_y, hex_icon_size, hex_icon_size), - event: ToolbarEvent::CopyHexColor, - kind: HitKind::Click, - tooltip: Some("Copy hex color".to_string()), - }); - hits.push(HitRegion { - focus_id: None, - rect: (hex_input_x, hex_input_y, hex_input_w, hex_input_h), - event: ToolbarEvent::EditHexColor, - kind: HitKind::Click, - tooltip: Some("Type a hex color (Enter applies)".to_string()), - }); - - let paste_btn_x = hex_input_x + hex_input_w + 4.0; - let paste_btn_size = 20.0; - let paste_btn_hover = hover - .map(|(hx, hy)| { - point_in_rect( - hx, - hy, - paste_btn_x, - hex_input_y, - paste_btn_size, - paste_btn_size, - ) - }) - .unwrap_or(false); - draw_button( - ctx, - paste_btn_x, - hex_input_y, - paste_btn_size, - paste_btn_size, - false, - paste_btn_hover, - ); - set_icon_color(ctx, paste_btn_hover); - toolbar_icons::draw_icon_paste( - ctx, - paste_btn_x + (paste_btn_size - 12.0) / 2.0, - hex_input_y + (paste_btn_size - 12.0) / 2.0, - 12.0, - ); - hits.push(HitRegion { - focus_id: None, - rect: (paste_btn_x, hex_input_y, paste_btn_size, paste_btn_size), - event: ToolbarEvent::PasteHexColor, - kind: HitKind::Click, - tooltip: Some("Paste hex color from clipboard".to_string()), - }); - - let eyedropper_x = paste_btn_x + paste_btn_size + 4.0; - let eyedropper_hover = hover - .map(|(hx, hy)| { - point_in_rect( - hx, - hy, - eyedropper_x, - hex_input_y, - paste_btn_size, - paste_btn_size, - ) - }) - .unwrap_or(false); - draw_button( - ctx, - eyedropper_x, - hex_input_y, - paste_btn_size, - paste_btn_size, - false, - eyedropper_hover, - ); - set_icon_color(ctx, eyedropper_hover); - toolbar_icons::draw_icon_eyedropper( - ctx, - eyedropper_x + 4.0, - hex_input_y + 4.0, - paste_btn_size - 8.0, - ); - hits.push(HitRegion { - focus_id: None, - rect: (eyedropper_x, hex_input_y, paste_btn_size, paste_btn_size), - event: ToolbarEvent::PickScreenColor, - kind: HitKind::Click, - tooltip: Some("Pick color from screen".to_string()), - }); -} - -pub(super) fn draw_color_swatch_row( - ctx: &cairo::Context, - hits: &mut Vec, - hover: Option<(f64, f64)>, - snapshot: &ToolbarSnapshot, - layout: ColorSwatchRowLayout, - colors: &[ColorSwatch], - toggle: Option, -) { - let mut x = layout.start_x; - for (color, name, action, index) in colors { - draw_swatch( - ctx, - x, - layout.row_y, - layout.swatch, - *color, - *color == snapshot.color, - ); - let binding = action.and_then(|action| snapshot.binding_hints.binding_for_action(action)); - let tooltip = format_quick_color_tooltip(name, binding); - hits.push(HitRegion { - focus_id: None, - rect: (x, layout.row_y, layout.swatch, layout.swatch), - event: ToolbarEvent::SetQuickColor { - color: *color, - action: *action, - index: *index, - }, - kind: HitKind::Click, - tooltip: Some(tooltip), - }); - x += layout.swatch + layout.swatch_gap; - } - - let Some(toggle) = toggle else { - return; - }; - - let is_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, layout.row_y, layout.swatch, layout.swatch)) - .unwrap_or(false); - draw_button( - ctx, - x, - layout.row_y, - layout.swatch, - layout.swatch, - false, - is_hover, - ); - set_icon_color(ctx, is_hover); - (toggle.icon_fn)( - ctx, - x + (layout.swatch - 14.0) / 2.0, - layout.row_y + (layout.swatch - 14.0) / 2.0, - 14.0, - ); - hits.push(HitRegion { - focus_id: None, - rect: (x, layout.row_y, layout.swatch, layout.swatch), - event: toggle.event, - kind: HitKind::Click, - tooltip: Some(toggle.tooltip.to_string()), - }); -} - -#[cfg(test)] -mod tests { - use super::{effective_hsv, hex_text_area_width, hex_text_style}; - use crate::input::state::test_support::make_test_input_state; - use crate::ui::toolbar::{ToolbarBindingHints, ToolbarSnapshot}; - - /// Breathing room around the measured value for font-metric differences - /// between desktop distributions and CI images. - const HEX_TEXT_MIN_HORIZONTAL_SLACK: f64 = 8.0; - - /// The chip shows `#RRGGBBAA` for a translucent color, so it must be wide - /// enough for nine characters beside its copy icon. Too narrow and the - /// centered value runs under that icon. - #[test] - fn the_hex_chip_fits_the_widest_hex_value() { - let style = hex_text_style(); - let area = hex_text_area_width(); - for digit in "0123456789ABCDEF".chars() { - let widest = format!("#{}", digit.to_string().repeat(8)); - assert_eq!( - widest.chars().count(), - crate::input::state::HEX_INPUT_MAX_CHARS - ); - let extents = - crate::ui_text::measure_text(style, &widest, None).expect("hex value measures"); - let required = extents.width() + HEX_TEXT_MIN_HORIZONTAL_SLACK; - assert!( - required <= area, - "{widest} needs {:.1}px plus {HEX_TEXT_MIN_HORIZONTAL_SLACK:.1}px of slack but the chip only offers {area:.1}px", - extents.width() - ); - } - } - - #[test] - fn effective_hsv_prefers_matching_memory_and_rejects_stale() { - let state = make_test_input_state(); - let mut snapshot = - ToolbarSnapshot::from_input_with_bindings(&state, ToolbarBindingHints::default()); - - // White picked at hue 0.4: RGB loses the hue, memory keeps it. - snapshot.color = crate::draw::color::hsv_to_rgb(0.4, 0.0, 1.0); - snapshot.picker_hsv = Some((0.4, 0.0, 1.0)); - assert_eq!(effective_hsv(&snapshot), (0.4, 0.0, 1.0)); - - // The color changed via another source (swatch/hex); stale memory - // must lose to the actual color. - snapshot.color = crate::draw::color::hsv_to_rgb(0.0, 1.0, 1.0); - let (h, s, v) = effective_hsv(&snapshot); - assert!(h.abs() < 1e-9); - assert!((s - 1.0).abs() < 1e-9); - assert!((v - 1.0).abs() < 1e-9); - } -} diff --git a/src/backend/wayland/toolbar/render/side_palette/header.rs b/src/backend/wayland/toolbar/render/side_palette/header.rs deleted file mode 100644 index edc1d8f5..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/header.rs +++ /dev/null @@ -1,283 +0,0 @@ -use super::SidePaletteLayout; -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::toolbar_icons; -use crate::ui::toolbar::model::{ - SideHeaderModel, ToolbarBoardChipPresentation, ToolbarControl, ToolbarControlKind, - ToolbarPresentationPayload, -}; -use crate::ui::toolbar::{SidePane, ToolbarEvent}; -use crate::ui_text::UiTextStyle; - -use super::super::widgets::constants::{ - COLOR_HEADER_BAND, FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, FONT_SIZE_TOOLTIP, RADIUS_CARD, - set_color, -}; -use super::super::widgets::*; -use crate::ui::theme::Rgba; - -/// Board chip fill: cool slate tint (hover is one step lighter). Specific -/// to the chip — no theme token. -const COLOR_BOARD_CHIP_BG_HOVER: Rgba = (0.28, 0.30, 0.34, 0.95); -const COLOR_BOARD_CHIP_BG: Rgba = (0.22, 0.24, 0.28, 0.95); -/// Board chip outline (hover/idle), cool-tinted to match the fill. -const COLOR_BOARD_CHIP_BORDER_HOVER: Rgba = (0.65, 0.7, 0.8, 0.7); -const COLOR_BOARD_CHIP_BORDER: Rgba = (0.65, 0.7, 0.8, 0.45); -/// Placeholder outline where the board color dot would sit. -const COLOR_BOARD_CHIP_EMPTY_DOT: Rgba = (0.62, 0.68, 0.76, 0.7); -/// Chip chevron glyph (hover/idle). -const COLOR_CHEVRON_HOVER: Rgba = (0.7, 0.72, 0.78, 0.95); -const COLOR_CHEVRON: Rgba = (0.7, 0.72, 0.78, 0.7); - -/// Fixed chrome: one header row (drag grip, board chip, pin, close) plus the -/// pane navigation row. Returns the content start y — nothing above it -/// scrolls or moves when panes or sections change. -pub(super) fn draw_header(layout: &mut SidePaletteLayout) -> f64 { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let hits = &mut layout.hits; - let hover = layout.hover; - let spec = &layout.spec; - let x = layout.x; - let width = layout.width; - let content_width = layout.content_width; - let label_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_LABEL, - }; - let header_model = SideHeaderModel::from_snapshot(snapshot); - - let band_y = ToolbarLayoutSpec::SIDE_TOP_PADDING - 2.0; - let band_h = ToolbarLayoutSpec::SIDE_HEADER_ROW1_HEIGHT + 4.0; - let band_x = spec.side_card_x(); - let band_w = spec.side_card_width(width); - draw_round_rect(ctx, band_x, band_y, band_w, band_h, RADIUS_CARD); - set_color(ctx, COLOR_HEADER_BAND); - let _ = ctx.fill(); - - let btn_size = ToolbarLayoutSpec::SIDE_HEADER_BUTTON_SIZE; - let btn_gap = ToolbarLayoutSpec::SIDE_HEADER_BUTTON_GAP; - let btn_margin = ToolbarLayoutSpec::SIDE_HEADER_BUTTON_MARGIN_RIGHT; - - // ========== Header row: drag grip · board chip · pin · close ========== - let row1_y = ToolbarLayoutSpec::SIDE_TOP_PADDING; - let row1_h = ToolbarLayoutSpec::SIDE_HEADER_ROW1_HEIGHT; - - let drag_size = ToolbarLayoutSpec::SIDE_HEADER_DRAG_SIZE; - let drag_y = row1_y + (row1_h - drag_size) / 2.0; - let drag_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, drag_y, drag_size, drag_size)) - .unwrap_or(false); - draw_drag_handle(ctx, x, drag_y, drag_size, drag_size, drag_hover); - hits.push(HitRegion { - focus_id: None, - rect: (x, drag_y, drag_size, drag_size), - event: single_control_event(&header_model.drag), - kind: HitKind::DragMoveSide, - tooltip: header_model.drag.presentation.tooltip.as_string(), - }); - - let btn_y = row1_y + (row1_h - btn_size) / 2.0; - let close_x = width - btn_margin - btn_size; - let pin_x = close_x - btn_size - btn_gap; - - // Board chip fills the middle of the header row. - let chip_x = x + drag_size + 8.0; - let chip_w = pin_x - 8.0 - chip_x; - let chip_h = ToolbarLayoutSpec::SIDE_BOARD_CHIP_HEIGHT; - let chip_y = row1_y + (row1_h - chip_h) / 2.0; - draw_board_chip( - ctx, - hits, - hover, - &header_model, - label_style, - chip_x, - chip_y, - chip_w, - chip_h, - ); - - let pin_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, pin_x, btn_y, btn_size, btn_size)) - .unwrap_or(false); - draw_pin_button(ctx, pin_x, btn_y, btn_size, snapshot.side_pinned, pin_hover); - hits.push(HitRegion { - focus_id: None, - rect: (pin_x, btn_y, btn_size, btn_size), - event: single_control_event(&header_model.pin), - kind: HitKind::Click, - tooltip: header_model.pin.presentation.tooltip.as_string(), - }); - - let close_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, close_x, btn_y, btn_size, btn_size)) - .unwrap_or(false); - draw_side_minimize_button(ctx, close_x, btn_y, btn_size, close_hover); - hits.push(HitRegion { - focus_id: None, - rect: (close_x, btn_y, btn_size, btn_size), - event: single_control_event(&header_model.close), - kind: HitKind::Click, - tooltip: header_model.close.presentation.tooltip.as_string(), - }); - - // ========== Pane navigation row ========== - let nav_y = spec.side_pane_nav_y(); - let nav_h = ToolbarLayoutSpec::SIDE_PANE_NAV_HEIGHT; - let nav_gap = 4.0; - let tab_count = SidePane::ALL.len() as f64; - let tab_w = (content_width - nav_gap * (tab_count - 1.0)) / tab_count; - let nav_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_TOOLTIP, - }; - let mut tab_x = x; - for pane in SidePane::ALL { - let selected = snapshot.active_side_pane == pane; - let tab_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, tab_x, nav_y, tab_w, nav_h)) - .unwrap_or(false); - draw_button(ctx, tab_x, nav_y, tab_w, nav_h, selected, tab_hover); - draw_label_center(ctx, nav_style, tab_x, nav_y, tab_w, nav_h, pane.label()); - hits.push(HitRegion { - focus_id: None, - rect: (tab_x, nav_y, tab_w, nav_h), - event: ToolbarEvent::SetSidePane(pane), - kind: HitKind::Click, - tooltip: Some(format!("{} pane", pane.label())), - }); - tab_x += tab_w + nav_gap; - } - - spec.side_content_start_y() -} - -#[allow(clippy::too_many_arguments)] -fn draw_board_chip( - ctx: &cairo::Context, - hits: &mut Vec, - hover: Option<(f64, f64)>, - header_model: &SideHeaderModel, - label_style: UiTextStyle<'_>, - chip_x: f64, - chip_y: f64, - chip_w: f64, - chip_h: f64, -) { - let chip_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, chip_x, chip_y, chip_w, chip_h)) - .unwrap_or(false); - let board_chip = board_chip_payload(header_model); - draw_round_rect(ctx, chip_x, chip_y, chip_w, chip_h, 6.0); - set_color( - ctx, - if chip_hover { - COLOR_BOARD_CHIP_BG_HOVER - } else { - COLOR_BOARD_CHIP_BG - }, - ); - let _ = ctx.fill(); - set_color( - ctx, - if chip_hover { - COLOR_BOARD_CHIP_BORDER_HOVER - } else { - COLOR_BOARD_CHIP_BORDER - }, - ); - ctx.set_line_width(1.0); - draw_round_rect(ctx, chip_x, chip_y, chip_w, chip_h, 6.0); - let _ = ctx.stroke(); - - // Color dot - let dot_size = ToolbarLayoutSpec::SIDE_BOARD_COLOR_DOT_SIZE; - let dot_x = chip_x + 6.0; - let dot_y = chip_y + (chip_h - dot_size) * 0.5; - if let Some(color) = board_chip.and_then(|chip| chip.color) { - draw_swatch(ctx, dot_x, dot_y, dot_size, color, false); - } else { - set_color(ctx, COLOR_BOARD_CHIP_EMPTY_DOT); - ctx.set_line_width(1.0); - draw_round_rect(ctx, dot_x, dot_y, dot_size, dot_size, 3.0); - let _ = ctx.stroke(); - } - - // Board icon before label - let icon_size = 10.0; - let icon_x = dot_x + dot_size + 4.0; - let icon_y = chip_y + (chip_h - icon_size) * 0.5; - set_icon_color(ctx, chip_hover); - toolbar_icons::draw_icon_board(ctx, icon_x, icon_y, icon_size); - - // Chevron on the right - let chevron_size = ToolbarLayoutSpec::SIDE_BOARD_CHEVRON_SIZE; - let chevron_x = chip_x + chip_w - chevron_size - 2.0; - let chevron_y = chip_y + (chip_h - chevron_size) / 2.0; - draw_chevron_right(ctx, chevron_x, chevron_y, chevron_size, chip_hover); - - // Board label (truncated) - let label_x = icon_x + icon_size + 5.0; - let label_w = chevron_x - label_x - 4.0; - let label = board_chip - .map(|chip| chip.label.as_str()) - .unwrap_or(header_model.board_chip.presentation.label.as_ref()); - let display_label = ellipsize_to_width(ctx, label_style, label, label_w); - draw_label_left( - ctx, - label_style, - label_x, - chip_y, - label_w, - chip_h, - &display_label, - ); - - hits.push(HitRegion { - focus_id: None, - rect: (chip_x, chip_y, chip_w, chip_h), - event: single_control_event(&header_model.board_chip), - kind: HitKind::Click, - tooltip: header_model.board_chip.presentation.tooltip.as_string(), - }); -} - -/// Draw a right-pointing chevron -fn draw_chevron_right(ctx: &cairo::Context, x: f64, y: f64, size: f64, hover: bool) { - set_color( - ctx, - if hover { - COLOR_CHEVRON_HOVER - } else { - COLOR_CHEVRON - }, - ); - ctx.set_line_width(1.5); - - let margin = size * 0.3; - let mid_y = y + size / 2.0; - ctx.move_to(x + margin, y + margin); - ctx.line_to(x + size - margin, mid_y); - ctx.line_to(x + margin, y + size - margin); - let _ = ctx.stroke(); -} - -fn single_control_event(control: &ToolbarControl) -> ToolbarEvent { - let ToolbarControlKind::Single(single) = &control.kind else { - return ToolbarEvent::SetSideMinimized(true); - }; - single.activation.compatibility_event() -} - -fn board_chip_payload(model: &SideHeaderModel) -> Option<&ToolbarBoardChipPresentation> { - match &model.board_chip.presentation.payload { - ToolbarPresentationPayload::BoardChip(chip) => Some(chip), - ToolbarPresentationPayload::None => None, - } -} diff --git a/src/backend/wayland/toolbar/render/side_palette/marker.rs b/src/backend/wayland/toolbar/render/side_palette/marker.rs deleted file mode 100644 index 9496ade6..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/marker.rs +++ /dev/null @@ -1,160 +0,0 @@ -use super::super::widgets::constants::{ - COLOR_TRACK_BACKGROUND, COLOR_TRACK_KNOB, FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, SPACING_STD, - set_color, -}; -use super::super::widgets::*; -use super::SidePaletteLayout; -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::toolbar_icons; -use crate::ui::toolbar::model::ToolbarSliderSpec; -use crate::ui::toolbar::{ToolContext, ToolbarEvent, ToolbarSideSection}; -use crate::ui_text::UiTextStyle; - -use super::section_header::draw_collapsible_header; - -pub(super) fn draw_marker_opacity_section(layout: &mut SidePaletteLayout, y: &mut f64) { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let hover = layout.hover; - let x = layout.x; - let card_x = layout.card_x; - let card_w = layout.card_w; - let section_gap = layout.section_gap; - let width = layout.width; - let label_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_LABEL, - }; - - if !ToolContext::from_snapshot(snapshot).show_marker_opacity { - return; - } - - let slider_card_h = layout.spec.side_marker_opacity_height(snapshot); - let btn_size = ToolbarLayoutSpec::SIDE_NUDGE_SIZE; - let nudge_icon_size = ToolbarLayoutSpec::SIDE_NUDGE_ICON_SIZE; - let value_w = ToolbarLayoutSpec::SIDE_SLIDER_VALUE_WIDTH; - let track_h = ToolbarLayoutSpec::SIDE_TRACK_HEIGHT; - let knob_r = ToolbarLayoutSpec::SIDE_TRACK_KNOB_RADIUS; - - let marker_slider_row_y = *y + ToolbarLayoutSpec::SIDE_SLIDER_ROW_OFFSET; - draw_group_card(ctx, card_x, *y, card_w, slider_card_h); - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::MarkerOpacity, - "Marker opacity", - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_Y, - ); - if snapshot.side_section_collapsed(ToolbarSideSection::MarkerOpacity) { - *y += slider_card_h + section_gap; - return; - } - - let hits = &mut layout.hits; - let minus_x = x; - let minus_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, minus_x, marker_slider_row_y, btn_size, btn_size)) - .unwrap_or(false); - draw_button( - ctx, - minus_x, - marker_slider_row_y, - btn_size, - btn_size, - false, - minus_hover, - ); - set_icon_color(ctx, minus_hover); - toolbar_icons::draw_icon_minus( - ctx, - minus_x + (btn_size - nudge_icon_size) / 2.0, - marker_slider_row_y + (btn_size - nudge_icon_size) / 2.0, - nudge_icon_size, - ); - let opacity_spec = ToolbarSliderSpec::MARKER_OPACITY; - let opacity_step = opacity_spec.step.unwrap_or(0.05); - hits.push(HitRegion { - focus_id: None, - rect: (minus_x, marker_slider_row_y, btn_size, btn_size), - event: ToolbarEvent::NudgeMarkerOpacity(-opacity_step), - kind: HitKind::Click, - tooltip: Some("Decrease opacity".to_string()), - }); - - let plus_x = width - x - btn_size - value_w - 4.0; - let plus_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, plus_x, marker_slider_row_y, btn_size, btn_size)) - .unwrap_or(false); - draw_button( - ctx, - plus_x, - marker_slider_row_y, - btn_size, - btn_size, - false, - plus_hover, - ); - set_icon_color(ctx, plus_hover); - toolbar_icons::draw_icon_plus( - ctx, - plus_x + (btn_size - nudge_icon_size) / 2.0, - marker_slider_row_y + (btn_size - nudge_icon_size) / 2.0, - nudge_icon_size, - ); - hits.push(HitRegion { - focus_id: None, - rect: (plus_x, marker_slider_row_y, btn_size, btn_size), - event: ToolbarEvent::NudgeMarkerOpacity(opacity_step), - kind: HitKind::Click, - tooltip: Some("Increase opacity".to_string()), - }); - - let track_x = minus_x + btn_size + SPACING_STD; - let track_w = plus_x - track_x - SPACING_STD; - let marker_track_y = marker_slider_row_y + (btn_size - track_h) / 2.0; - let knob_x = opacity_spec.knob_center_x(track_x, track_w, knob_r, snapshot.marker_opacity); - - set_color(ctx, COLOR_TRACK_BACKGROUND); - draw_round_rect(ctx, track_x, marker_track_y, track_w, track_h, 4.0); - let _ = ctx.fill(); - set_color(ctx, COLOR_TRACK_KNOB); - ctx.arc( - knob_x, - marker_track_y + track_h / 2.0, - knob_r, - 0.0, - std::f64::consts::PI * 2.0, - ); - let _ = ctx.fill(); - - hits.push(HitRegion { - focus_id: None, - rect: (track_x, marker_track_y - 6.0, track_w, track_h + 12.0), - event: ToolbarEvent::SetMarkerOpacity(snapshot.marker_opacity), - kind: HitKind::DragSetMarkerOpacity { - min: opacity_spec.min, - max: opacity_spec.max, - }, - tooltip: None, - }); - - let opacity_text = format!("{:.0}%", snapshot.marker_opacity * 100.0); - let value_x = width - x - value_w; - draw_label_center( - ctx, - label_style, - value_x, - marker_slider_row_y, - value_w, - btn_size, - &opacity_text, - ); - - *y += slider_card_h + section_gap; -} diff --git a/src/backend/wayland/toolbar/render/side_palette/mod.rs b/src/backend/wayland/toolbar/render/side_palette/mod.rs deleted file mode 100644 index 8f1ac737..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/mod.rs +++ /dev/null @@ -1,436 +0,0 @@ -mod actions; -mod arrow; -mod boards; -mod colors; -mod header; -mod marker; -mod pages; -mod presets; -mod section_header; -pub(in crate::backend::wayland::toolbar) mod session; -mod settings; -mod step; -mod step_marker; -mod text; -mod thickness; - -use anyhow::Result; - -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::ui::toolbar::model; -use crate::ui::toolbar::snapshot::ToolContext; -use crate::ui::toolbar::{SidePane, ToolbarEvent, ToolbarSideSection, ToolbarSnapshot}; - -use std::time::Instant; - -use super::widgets::constants::set_color; -use super::widgets::{draw_panel_background, draw_round_rect, draw_tooltip_with_delay}; -use crate::ui::theme::Rgba; - -/// Scrollbar track tint: a white wash with no theme token (value-coincides -/// with COLOR_DIVIDER but is a control surface, not a separator). The thumb -/// shares the GTK scrollbar slider token. Shared with the top strip's -/// Canvas/Session/Settings popover scrollbar so both scroll surfaces match. -pub(in crate::backend::wayland::toolbar) const COLOR_SCROLLBAR_TRACK: Rgba = (1.0, 1.0, 1.0, 0.08); -use crate::ui::theme::toolbar::COLOR_SCROLLBAR_SLIDER as COLOR_SCROLLBAR_THUMB; - -pub(super) struct SidePaletteLayout<'a> { - pub(super) ctx: &'a cairo::Context, - pub(super) width: f64, - pub(super) snapshot: &'a ToolbarSnapshot, - pub(super) hits: &'a mut Vec, - pub(super) hover: Option<(f64, f64)>, - pub(super) spec: ToolbarLayoutSpec, - pub(super) x: f64, - pub(super) card_x: f64, - pub(super) card_w: f64, - pub(super) content_width: f64, - pub(super) section_gap: f64, -} - -impl<'a> SidePaletteLayout<'a> { - fn new( - ctx: &'a cairo::Context, - width: f64, - snapshot: &'a ToolbarSnapshot, - hits: &'a mut Vec, - hover: Option<(f64, f64)>, - ) -> Self { - let spec = ToolbarLayoutSpec::new(snapshot); - let x = ToolbarLayoutSpec::SIDE_START_X; - let card_x = spec.side_card_x(); - let card_w = spec.side_card_width(width); - let content_width = spec.side_content_width(width); - let section_gap = ToolbarLayoutSpec::SIDE_SECTION_GAP; - Self { - ctx, - width, - snapshot, - hits, - hover, - spec, - x, - card_x, - card_w, - content_width, - section_gap, - } - } -} - -/// Button rects for a Boards/Pages-style command row. Icon mode packs -/// plain buttons left as squares and right-aligns destructive ones with -/// the leftover width as a guard gap; text mode keeps the equal-width -/// grid so labels stay readable. -pub(super) struct SideRowLayout { - pub(super) x: f64, - pub(super) row_y: f64, - pub(super) content_width: f64, - pub(super) btn_h: f64, - pub(super) btn_gap: f64, - pub(super) use_icons: bool, - pub(super) text_columns: usize, -} - -pub(super) fn side_row_button_rects( - layout: SideRowLayout, - buttons: &[model::ToolbarButtonModel], -) -> Vec<(f64, f64, f64, f64)> { - use crate::backend::wayland::toolbar::rows::{grid_layout, row_item_width}; - let SideRowLayout { - x, - row_y, - content_width, - btn_h, - btn_gap, - use_icons, - text_columns, - } = layout; - if use_icons { - let mut leading = 0usize; - let mut trailing = 0usize; - buttons - .iter() - .map(|button| { - if button.event.is_destructive() { - trailing += 1; - let bx = x + content_width - - trailing as f64 * btn_h - - (trailing as f64 - 1.0) * btn_gap; - (bx, row_y, btn_h, btn_h) - } else { - let bx = x + leading as f64 * (btn_h + btn_gap); - leading += 1; - (bx, row_y, btn_h, btn_h) - } - }) - .collect() - } else { - let btn_w = row_item_width(content_width, text_columns, btn_gap); - grid_layout( - x, - row_y, - btn_w, - btn_h, - btn_gap, - btn_gap, - text_columns, - buttons.len(), - ) - .items - .iter() - .map(|item| (item.x, item.y, item.w, item.h)) - .collect() - } -} - -#[cfg(test)] -mod side_row_tests { - use super::*; - use crate::ui::toolbar::ToolbarEvent; - - /// Callers zip these rects with the button list, so a short result - /// silently drops trailing buttons from paint and hit testing. With more - /// buttons than text columns (the capped-columns case a sixth boards - /// button produces), every button must still get a rect, wrapped onto a - /// second row. - #[test] - fn text_grid_produces_a_rect_for_every_button_beyond_the_column_cap() { - let buttons = vec![model::ToolbarButtonModel::new(ToolbarEvent::Undo, true); 6]; - let rects = side_row_button_rects( - SideRowLayout { - x: 0.0, - row_y: 0.0, - content_width: 200.0, - btn_h: 24.0, - btn_gap: 4.0, - use_icons: false, - text_columns: 5, - }, - &buttons, - ); - assert_eq!(rects.len(), buttons.len()); - let first_row_y = rects[0].1; - assert!( - rects[5].1 > first_row_y, - "the sixth button must wrap onto a second row, got {rects:?}" - ); - assert_eq!( - rects - .iter() - .filter(|(_, y, _, _)| *y == first_row_y) - .count(), - 5, - "the first row must hold exactly the column cap" - ); - } -} - -#[derive(Debug, Clone, Copy)] -pub(super) struct ColorSectionInfo { - pub(super) picker_y: f64, - pub(super) picker_w: f64, - pub(super) picker_h: f64, -} - -pub fn render_side_palette( - ctx: &cairo::Context, - width: f64, - height: f64, - snapshot: &ToolbarSnapshot, - hits: &mut Vec, - hover: Option<(f64, f64)>, - hover_start: Option, -) -> Result<()> { - draw_panel_background(ctx, 0.0, 0.0, width, height); - - if snapshot.side_minimized { - draw_side_minimized_tab(ctx, width, height, hits, hover, hover_start); - return Ok(()); - } - - let mut layout = SidePaletteLayout::new(ctx, width, snapshot, hits, hover); - - let content_top = header::draw_header(&mut layout); - - // Scroll bookkeeping: when the pane's natural height exceeds the - // surface, content below the fixed chrome is clipped and translated. - let natural = layout.spec.side_natural_height(snapshot); - let max_scroll = (natural - height).max(0.0); - let scroll = if max_scroll > 0.0 { - snapshot.side_scroll.clamp(0.0, max_scroll) - } else { - 0.0 - }; - let scrolling = max_scroll > 0.0; - - let chrome_hits = layout.hits.len(); - if scrolling { - let _ = ctx.save(); - ctx.rectangle(0.0, content_top, width, height - content_top); - ctx.clip(); - ctx.translate(0.0, -scroll); - // Content is laid out in unscrolled coordinates; hover must be - // compared in the same space. - layout.hover = layout.hover.map(|(hx, hy)| { - ( - hx, - if hy >= content_top { - hy + scroll - } else { - f64::MIN - }, - ) - }); - } - - let mut y = content_top; - draw_active_pane(&mut layout, &mut y); - - if scrolling { - let _ = ctx.restore(); - // Shift pane-content hits into visible coordinates, then clip every - // region to the scroll viewport so partial rows cannot remain active - // through the fixed header or below the surface. - for hit in &mut layout.hits[chrome_hits..] { - hit.rect.1 -= scroll; - } - crate::backend::wayland::toolbar::hit::clip_hit_regions_to_bounds( - layout.hits, - chrome_hits, - (0.0, content_top, width, (height - content_top).max(0.0)), - ); - layout.hover = hover; - draw_scrollbar(&mut layout, content_top, height, natural, scroll); - } - - draw_tooltip_with_delay( - ctx, - layout.hits, - layout.hover, - width, - height, - false, - hover_start, - ); - Ok(()) -} - -fn draw_active_pane(layout: &mut SidePaletteLayout<'_>, y: &mut f64) { - let snapshot = layout.snapshot; - match snapshot.active_side_pane { - SidePane::Session => { - session::draw_session_section(layout, y); - } - SidePane::Settings => { - settings::draw_settings_section(layout, y); - } - SidePane::Draw | SidePane::Canvas => { - draw_pane_sections(layout, y); - } - } -} - -fn draw_pane_sections(layout: &mut SidePaletteLayout<'_>, y: &mut f64) { - let snapshot = layout.snapshot; - let tool_context = ToolContext::from_snapshot(snapshot); - let mut colors_info = None; - let mut hover_preset_color = None; - let mut thickness_block_drawn = false; - let mut text_block_drawn = false; - for section in model::ordered_pane_sections(snapshot) { - match section { - ToolbarSideSection::Colors - if tool_context.needs_color - && !snapshot.side_section_hidden(ToolbarSideSection::Colors) => - { - colors_info = colors::draw_colors_section(layout, y); - } - ToolbarSideSection::Presets - if !snapshot.side_section_hidden(ToolbarSideSection::Presets) => - { - hover_preset_color = presets::draw_presets_section(layout, y); - } - ToolbarSideSection::Thickness - | ToolbarSideSection::EraserMode - | ToolbarSideSection::PolygonSides - if tool_context.needs_thickness && !thickness_block_drawn => - { - thickness_block_drawn = true; - thickness::draw_thickness_section(layout, y); - } - ToolbarSideSection::ArrowLabels - if tool_context.show_arrow_labels - && !snapshot.side_section_hidden(ToolbarSideSection::ArrowLabels) => - { - arrow::draw_arrow_section(layout, y); - } - ToolbarSideSection::StepMarkers - if tool_context.show_step_counter - && !snapshot.side_section_hidden(ToolbarSideSection::StepMarkers) => - { - step_marker::draw_step_marker_section(layout, y); - } - ToolbarSideSection::MarkerOpacity - if tool_context.show_marker_opacity - && !snapshot.side_section_hidden(ToolbarSideSection::MarkerOpacity) => - { - marker::draw_marker_opacity_section(layout, y); - } - ToolbarSideSection::TextSize | ToolbarSideSection::Font - if tool_context.show_font_controls && !text_block_drawn => - { - text_block_drawn = true; - text::draw_text_controls_section(layout, y); - } - ToolbarSideSection::Actions => { - actions::draw_actions_section(layout, y); - } - ToolbarSideSection::Boards => { - boards::draw_boards_section(layout, y); - } - ToolbarSideSection::Pages => { - pages::draw_pages_section(layout, y); - } - ToolbarSideSection::StepUndo => { - step::draw_step_section(layout, y); - } - _ => {} - } - } - if let (Some(color), Some(info)) = (hover_preset_color, &colors_info) { - colors::draw_preset_hover_highlight(layout, info, color); - } -} - -/// Minimized side palette: the whole tab is one restore button. Not an -/// item id on purpose — the way back must not be hideable. -fn draw_side_minimized_tab( - ctx: &cairo::Context, - width: f64, - height: f64, - hits: &mut Vec, - hover: Option<(f64, f64)>, - hover_start: Option, -) { - let is_hover = hover - .map(|(hx, hy)| super::widgets::point_in_rect(hx, hy, 0.0, 0.0, width, height)) - .unwrap_or(false); - super::widgets::set_icon_color(ctx, is_hover); - let icon = (width * 0.75).min(18.0); - crate::toolbar_icons::draw_icon_chevron_right( - ctx, - (width - icon) / 2.0, - (height - icon) / 2.0, - icon, - ); - hits.push(HitRegion { - focus_id: None, - rect: (0.0, 0.0, width, height), - event: ToolbarEvent::SetSideMinimized(false), - kind: HitKind::Click, - tooltip: Some("Show toolbar".to_string()), - }); - draw_tooltip_with_delay(ctx, hits, hover, width, height, false, hover_start); -} - -/// Minimal scrollbar: proportional thumb on the right edge; the whole track -/// is draggable (stylus and touch have no wheel). -fn draw_scrollbar( - layout: &mut SidePaletteLayout<'_>, - content_top: f64, - height: f64, - natural: f64, - scroll: f64, -) { - let ctx = layout.ctx; - let track_w = 4.0; - let track_x = layout.width - track_w - 3.0; - let track_y = content_top + 2.0; - let track_h = height - content_top - 4.0; - if track_h <= 20.0 { - return; - } - - set_color(ctx, COLOR_SCROLLBAR_TRACK); - draw_round_rect(ctx, track_x, track_y, track_w, track_h, 2.0); - let _ = ctx.fill(); - - let viewport = height - content_top; - let thumb_h = (track_h * (viewport / natural)).max(20.0); - let max_scroll = natural - height; - let thumb_y = track_y + (track_h - thumb_h) * (scroll / max_scroll).clamp(0.0, 1.0); - set_color(ctx, COLOR_SCROLLBAR_THUMB); - draw_round_rect(ctx, track_x, thumb_y, track_w, thumb_h, 2.0); - let _ = ctx.fill(); - - layout.hits.push(HitRegion { - focus_id: None, - rect: (track_x - 8.0, track_y, track_w + 11.0, track_h), - event: ToolbarEvent::ScrollSidePane(scroll), - kind: HitKind::DragScrollSide { max_scroll }, - tooltip: None, - }); -} diff --git a/src/backend/wayland/toolbar/render/side_palette/pages.rs b/src/backend/wayland/toolbar/render/side_palette/pages.rs deleted file mode 100644 index 0cb30784..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/pages.rs +++ /dev/null @@ -1,137 +0,0 @@ -use super::SidePaletteLayout; -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::format_binding_label; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::toolbar_icons; -use crate::ui::toolbar::model::toolbar_pages_model; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection}; -use crate::ui_text::UiTextStyle; - -use super::super::widgets::constants::{ - COLOR_TEXT_DISABLED, FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, set_color, -}; -use super::super::widgets::*; -use super::section_header::draw_collapsible_header; - -pub(super) fn draw_pages_section(layout: &mut SidePaletteLayout, y: &mut f64) { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let hover = layout.hover; - let x = layout.x; - let card_x = layout.card_x; - let card_w = layout.card_w; - let content_width = layout.content_width; - let section_gap = layout.section_gap; - let use_icons = snapshot.use_icons; - let label_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_LABEL, - }; - - let Some(model) = toolbar_pages_model(snapshot) else { - return; - }; - - let pages_card_h = layout.spec.side_pages_height(snapshot); - draw_group_card(ctx, card_x, *y, card_w, pages_card_h); - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::Pages, - ToolbarSideSection::Pages.label(), - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL, - ); - if snapshot.side_section_collapsed(ToolbarSideSection::Pages) { - *y += pages_card_h + section_gap; - return; - } - - let hits = &mut layout.hits; - let pages_y = *y + ToolbarLayoutSpec::SIDE_SECTION_TOGGLE_OFFSET_Y; - let btn_h = if use_icons { - ToolbarLayoutSpec::SIDE_ACTION_BUTTON_HEIGHT_ICON - } else { - ToolbarLayoutSpec::SIDE_ACTION_BUTTON_HEIGHT_TEXT - }; - let btn_gap = ToolbarLayoutSpec::SIDE_ACTION_BUTTON_GAP; - let rects = super::side_row_button_rects( - super::SideRowLayout { - x, - row_y: pages_y, - content_width, - btn_h, - btn_gap, - use_icons, - text_columns: model.buttons.len(), - }, - &model.buttons, - ); - for (rect, button) in rects.iter().zip(model.buttons.iter()) { - let label = button.short_label(snapshot, "Page"); - let (bx, by, btn_w, btn_h) = *rect; - let is_hover = button.enabled - && hover - .map(|(hx, hy)| point_in_rect(hx, hy, bx, by, btn_w, btn_h)) - .unwrap_or(false); - if !button.enabled { - draw_disabled_button(ctx, bx, by, btn_w, btn_h); - } else if button.event.is_destructive() { - draw_destructive_button(ctx, bx, by, btn_w, btn_h, is_hover); - } else { - draw_button(ctx, bx, by, btn_w, btn_h, false, is_hover); - } - if use_icons { - if button.enabled { - set_icon_color(ctx, is_hover); - } else { - set_color(ctx, COLOR_TEXT_DISABLED); - } - let icon_size = ToolbarLayoutSpec::SIDE_ACTION_ICON_SIZE; - let icon_x = bx + (btn_w - icon_size) / 2.0; - let icon_y = by + (btn_h - icon_size) / 2.0; - page_icon(&button.event)(ctx, icon_x, icon_y, icon_size); - } else if button.enabled { - draw_label_center(ctx, label_style, bx, by, btn_w, btn_h, label); - } else { - draw_label_center_color( - ctx, - label_style, - bx, - by, - btn_w, - btn_h, - label, - COLOR_TEXT_DISABLED, - ); - } - if button.enabled { - hits.push(HitRegion { - focus_id: None, - rect: (bx, by, btn_w, btn_h), - event: button.event.clone(), - kind: HitKind::Click, - tooltip: Some(format_binding_label( - button.tooltip_label(snapshot, "Page"), - button.binding_hint(snapshot), - )), - }); - } - } - - *y += pages_card_h + section_gap; -} - -fn page_icon(event: &ToolbarEvent) -> fn(&cairo::Context, f64, f64, f64) { - match event { - ToolbarEvent::PagePrev => toolbar_icons::draw_icon_chevron_left, - ToolbarEvent::PageNext => toolbar_icons::draw_icon_chevron_right, - ToolbarEvent::PageNew => toolbar_icons::draw_icon_plus, - ToolbarEvent::PageDuplicate => toolbar_icons::draw_icon_copy, - ToolbarEvent::PageDelete => toolbar_icons::draw_icon_clear, - _ => toolbar_icons::draw_icon_clear, - } -} diff --git a/src/backend/wayland/toolbar/render/side_palette/presets/format.rs b/src/backend/wayland/toolbar/render/side_palette/presets/format.rs deleted file mode 100644 index f9a4d14a..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/presets/format.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::draw::EraserKind; -use crate::input::{EraserMode, Tool}; -use crate::ui::toolbar::PresetSlotSnapshot; -use crate::util::color_to_name; - -pub(super) fn tool_label(tool: Tool) -> &'static str { - crate::ui::toolbar::bindings::tool_label(tool) -} - -pub(super) fn px_label(value: f64) -> String { - if (value - value.round()).abs() < 0.05 { - format!("{:.0}px", value) - } else { - format!("{:.1}px", value) - } -} - -pub(super) fn angle_label(value: f64) -> String { - if (value - value.round()).abs() < 0.05 { - format!("{:.0}deg", value) - } else { - format!("{:.1}deg", value) - } -} - -pub(super) fn on_off(value: bool) -> &'static str { - if value { "on" } else { "off" } -} - -pub(super) fn eraser_kind_label(kind: EraserKind) -> &'static str { - match kind { - EraserKind::Circle => "circle", - EraserKind::Rect => "rect", - } -} - -pub(super) fn eraser_mode_label(mode: EraserMode) -> &'static str { - match mode { - EraserMode::Brush => "brush", - EraserMode::Stroke => "stroke", - } -} - -pub(super) fn truncate_label(value: &str, max_chars: usize) -> String { - if value.chars().count() <= max_chars { - value.to_string() - } else { - let mut truncated = value - .chars() - .take(max_chars.saturating_sub(3)) - .collect::(); - truncated.push_str("..."); - truncated - } -} - -pub(super) fn preset_tooltip_text( - preset: &PresetSlotSnapshot, - slot: usize, - binding: Option<&str>, -) -> String { - let preset_name = preset - .name - .as_deref() - .map(str::trim) - .filter(|name| !name.is_empty()); - let mut extra_details = Vec::new(); - if let Some(fill) = preset.fill_enabled { - extra_details.push(format!("fill:{}", on_off(fill))); - } - if let Some(opacity) = preset.marker_opacity { - let percent = (opacity * 100.0).round() as i32; - extra_details.push(format!("opacity:{}%", percent)); - } - if let Some(kind) = preset.eraser_kind { - extra_details.push(format!("eraser:{}", eraser_kind_label(kind))); - } - if let Some(mode) = preset.eraser_mode { - extra_details.push(format!("mode:{}", eraser_mode_label(mode))); - } - if let Some(font_size) = preset.font_size { - extra_details.push(format!("font:{}", px_label(font_size))); - } - if let Some(text_bg) = preset.text_background_enabled { - extra_details.push(format!("text bg:{}", on_off(text_bg))); - } - let mut arrow_bits = Vec::new(); - if let Some(length) = preset.arrow_length { - arrow_bits.push(format!("len {}", px_label(length))); - } - if let Some(angle) = preset.arrow_angle { - arrow_bits.push(format!("ang {}", angle_label(angle))); - } - if let Some(head_at_end) = preset.arrow_head_at_end { - let head = if head_at_end { "end" } else { "start" }; - arrow_bits.push(format!("head {}", head)); - } - if !arrow_bits.is_empty() { - extra_details.push(format!("arrow:{}", arrow_bits.join(", "))); - } - if let Some(show_status_bar) = preset.show_status_bar { - extra_details.push(format!("status:{}", on_off(show_status_bar))); - } - - let base_summary = format!( - "{}, {}, {}", - tool_label(preset.tool), - color_to_name(&preset.color), - px_label(preset.size) - ); - let summary = if extra_details.is_empty() { - base_summary - } else { - format!("{}; {}", base_summary, extra_details.join("; ")) - }; - let label = if let Some(name) = preset_name { - format!("Apply preset {}: {} ({})", slot, name, summary) - } else { - format!("Apply preset {} ({})", slot, summary) - }; - match binding { - Some(binding) => format!("{label} (key: {binding})"), - None => label, - } -} diff --git a/src/backend/wayland/toolbar/render/side_palette/presets/header.rs b/src/backend/wayland/toolbar/render/side_palette/presets/header.rs deleted file mode 100644 index 0e06ae4e..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/presets/header.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::ui::toolbar::ToolbarSideSection; -use crate::ui_text::{UiTextStyle, text_layout}; - -use super::super::super::widgets::constants::{ - COLOR_LABEL_HINT, FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, FONT_SIZE_SMALL, set_color, -}; -use super::super::section_header::draw_collapsible_header; -use super::SidePaletteLayout; - -pub(super) fn draw_presets_header( - layout: &mut SidePaletteLayout, - section_y: f64, - card_x: f64, - card_w: f64, - slot_count: usize, -) { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let label_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_LABEL, - }; - - draw_collapsible_header( - layout, - section_y, - label_style, - ToolbarSideSection::Presets, - ToolbarSideSection::Presets.label(), - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_Y, - ); - - let apply_hint = { - let mut uses_digit_bindings = true; - for slot in 1..=slot_count { - let expected = slot.to_string(); - if snapshot.binding_hints.apply_preset(slot) != Some(expected.as_str()) { - uses_digit_bindings = false; - break; - } - } - if uses_digit_bindings { - Some(format!("Keys 1-{} apply", slot_count)) - } else { - Some("Keys apply presets".to_string()) - } - }; - if let Some(hint) = apply_hint { - let hint_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Normal, - size: FONT_SIZE_SMALL, - }; - let layout = text_layout(ctx, hint_style, &hint, None); - let ext = layout.ink_extents(); - let chevron_reserve = ToolbarLayoutSpec::SIDE_COLLAPSE_CHEVRON_SIZE + 10.0; - let hint_x = card_x + card_w - ext.width() - chevron_reserve - ext.x_bearing(); - let hint_y = section_y + ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_Y; - set_color(ctx, COLOR_LABEL_HINT); - layout.show_at_baseline(ctx, hint_x, hint_y); - } -} diff --git a/src/backend/wayland/toolbar/render/side_palette/presets/mod.rs b/src/backend/wayland/toolbar/render/side_palette/presets/mod.rs deleted file mode 100644 index 4fc69b13..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/presets/mod.rs +++ /dev/null @@ -1,72 +0,0 @@ -mod format; -mod header; -mod slot; -mod widgets; - -use super::SidePaletteLayout; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::draw::Color; -use crate::ui::toolbar::ToolbarSideSection; - -use super::super::widgets::draw_group_card; -use header::draw_presets_header; -use slot::{PresetSlotLayout, draw_preset_slot}; - -pub(super) fn draw_presets_section(layout: &mut SidePaletteLayout, y: &mut f64) -> Option { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let card_x = layout.card_x; - let card_w = layout.card_w; - let section_gap = layout.section_gap; - - let slot_count = snapshot.preset_slot_count.min(snapshot.presets.len()); - if snapshot.side_section_hidden(ToolbarSideSection::Presets) - || !snapshot.show_presets - || slot_count == 0 - { - return None; - } - - let mut hover_preset_color: Option = None; - - let presets_card_h = layout.spec.side_presets_height(snapshot); - draw_group_card(ctx, card_x, *y, card_w, presets_card_h); - draw_presets_header(layout, *y, card_x, card_w, slot_count); - if snapshot.side_section_collapsed(ToolbarSideSection::Presets) { - *y += presets_card_h + section_gap; - return None; - } - - let slot_size = ToolbarLayoutSpec::SIDE_PRESET_SLOT_SIZE; - let slot_gap = ToolbarLayoutSpec::SIDE_PRESET_SLOT_GAP; - let slot_row_y = *y + ToolbarLayoutSpec::SIDE_PRESET_ROW_OFFSET_Y; - let icon_size = (slot_size * 0.45).round(); - let swatch_size = (slot_size * 0.35).round(); - let number_box = (slot_size * 0.4).round(); - let keycap_pad = (slot_size * 0.1).round().max(3.0); - let keycap_radius = (number_box * 0.25).max(3.0); - - let preset_layout = PresetSlotLayout { - slot_size, - slot_gap, - slot_row_y, - icon_size, - swatch_size, - number_box, - keycap_pad, - keycap_radius, - card_x, - card_w, - section_y: *y, - }; - - for slot_index in 0..slot_count { - if let Some(color) = draw_preset_slot(layout, &preset_layout, slot_index) { - hover_preset_color = Some(color); - } - } - - *y += presets_card_h + section_gap; - - hover_preset_color -} diff --git a/src/backend/wayland/toolbar/render/side_palette/presets/slot/content.rs b/src/backend/wayland/toolbar/render/side_palette/presets/slot/content.rs deleted file mode 100644 index ad567376..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/presets/slot/content.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::draw::Color; -use crate::input::Tool; -use crate::toolbar_icons; -use crate::ui::toolbar::model::{self, SemanticToolIcon}; -use crate::ui::toolbar::{PresetSlotSnapshot, ToolbarEvent, ToolbarSnapshot}; - -use super::super::super::super::widgets::constants::{COLOR_TEXT_SECONDARY, set_color}; -use super::super::super::super::widgets::{draw_round_rect, draw_swatch}; -use super::super::format::{preset_tooltip_text, truncate_label}; -use super::super::widgets::draw_preset_name_tag; -use super::PresetSlotLayout; -use crate::ui::theme::Rgba; - -/// Thickness preview line across a filled slot's bottom edge. -const COLOR_PREVIEW_LINE: Rgba = (1.0, 1.0, 1.0, 0.8); -/// Outline around the mini color swatch in a filled slot. -const COLOR_SWATCH_OUTLINE: Rgba = (1.0, 1.0, 1.0, 0.75); -/// Dashed outline marking an empty slot. -const COLOR_EMPTY_SLOT_DASH: Rgba = (1.0, 1.0, 1.0, 0.35); - -#[allow(clippy::too_many_arguments)] -pub(super) fn draw_preset_content( - ctx: &cairo::Context, - snapshot: &ToolbarSnapshot, - hits: &mut Vec, - layout_spec: &PresetSlotLayout, - slot_x: f64, - slot: usize, - slot_hover: bool, - preset: Option<&PresetSlotSnapshot>, -) -> Option { - let mut hover_preset_color = None; - - if let Some(preset) = preset { - if slot_hover { - hover_preset_color = Some(preset.color); - } - let preset_name = preset - .name - .as_deref() - .map(str::trim) - .filter(|name| !name.is_empty()); - let tooltip = preset_tooltip_text(preset, slot, snapshot.binding_hints.apply_preset(slot)); - hits.push(HitRegion { - focus_id: None, - rect: ( - slot_x, - layout_spec.slot_row_y, - layout_spec.slot_size, - layout_spec.slot_size, - ), - event: ToolbarEvent::ApplyPreset(slot), - kind: HitKind::Click, - tooltip: Some(tooltip), - }); - - set_color(ctx, COLOR_TEXT_SECONDARY); - let icon_x = slot_x + (layout_spec.slot_size - layout_spec.icon_size) / 2.0; - let icon_y = layout_spec.slot_row_y + (layout_spec.slot_size - layout_spec.icon_size) / 2.0; - draw_preset_icon(ctx, preset.tool, icon_x, icon_y, layout_spec.icon_size); - - let preview_thickness = (preset.size / 50.0 * 6.0).clamp(1.0, 6.0); - let preview_y = layout_spec.slot_row_y + layout_spec.slot_size - 6.0; - set_color(ctx, COLOR_PREVIEW_LINE); - ctx.set_line_width(preview_thickness); - ctx.move_to(slot_x + 4.0, preview_y); - ctx.line_to(slot_x + layout_spec.slot_size - 4.0, preview_y); - let _ = ctx.stroke(); - - let swatch_x = slot_x + layout_spec.slot_size - layout_spec.swatch_size - 4.0; - let swatch_y = - layout_spec.slot_row_y + layout_spec.slot_size - layout_spec.swatch_size - 4.0; - draw_swatch( - ctx, - swatch_x, - swatch_y, - layout_spec.swatch_size, - preset.color, - false, - ); - set_color(ctx, COLOR_SWATCH_OUTLINE); - ctx.set_line_width(1.0); - draw_round_rect( - ctx, - swatch_x, - swatch_y, - layout_spec.swatch_size, - layout_spec.swatch_size, - 4.0, - ); - let _ = ctx.stroke(); - - if slot_hover && let Some(name) = preset_name { - let display_name = truncate_label(name, 12); - draw_preset_name_tag( - ctx, - &display_name, - slot_x, - layout_spec.slot_row_y, - layout_spec.slot_size, - layout_spec.card_x, - layout_spec.card_w, - layout_spec.section_y, - ); - } - } else { - set_color(ctx, COLOR_EMPTY_SLOT_DASH); - ctx.set_line_width(1.0); - ctx.set_dash(&[3.0, 2.0], 0.0); - draw_round_rect( - ctx, - slot_x + 1.0, - layout_spec.slot_row_y + 1.0, - layout_spec.slot_size - 2.0, - layout_spec.slot_size - 2.0, - 6.0, - ); - let _ = ctx.stroke(); - ctx.set_dash(&[], 0.0); - } - - hover_preset_color -} - -fn draw_preset_icon(ctx: &cairo::Context, tool: Tool, x: f64, y: f64, size: f64) { - match model::semantic_icon_for_tool(tool) { - SemanticToolIcon::Select => toolbar_icons::draw_icon_select(ctx, x, y, size), - SemanticToolIcon::Pen => toolbar_icons::draw_icon_pen(ctx, x, y, size), - SemanticToolIcon::Line => toolbar_icons::draw_icon_line(ctx, x, y, size), - SemanticToolIcon::Rect => toolbar_icons::draw_icon_rect(ctx, x, y, size), - SemanticToolIcon::Circle => toolbar_icons::draw_icon_circle(ctx, x, y, size), - SemanticToolIcon::Triangle => toolbar_icons::draw_icon_triangle(ctx, x, y, size), - SemanticToolIcon::Parallelogram => { - toolbar_icons::draw_icon_parallelogram(ctx, x, y, size); - } - SemanticToolIcon::Rhombus => toolbar_icons::draw_icon_rhombus(ctx, x, y, size), - SemanticToolIcon::Polygon => toolbar_icons::draw_icon_polygon(ctx, x, y, size), - SemanticToolIcon::FreeformPolygon => { - toolbar_icons::draw_icon_freeform_polygon(ctx, x, y, size); - } - SemanticToolIcon::Arrow => toolbar_icons::draw_icon_arrow(ctx, x, y, size), - SemanticToolIcon::Blur => toolbar_icons::draw_icon_blur(ctx, x, y, size), - SemanticToolIcon::Spotlight => toolbar_icons::draw_icon_spotlight(ctx, x, y, size), - SemanticToolIcon::Marker => toolbar_icons::draw_icon_marker(ctx, x, y, size), - SemanticToolIcon::StepMarker => toolbar_icons::draw_icon_step_marker(ctx, x, y, size), - SemanticToolIcon::Highlight => toolbar_icons::draw_icon_highlight(ctx, x, y, size), - SemanticToolIcon::Eraser => toolbar_icons::draw_icon_eraser(ctx, x, y, size), - } -} diff --git a/src/backend/wayland/toolbar/render/side_palette/presets/slot/feedback.rs b/src/backend/wayland/toolbar/render/side_palette/presets/slot/feedback.rs deleted file mode 100644 index 051270d1..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/presets/slot/feedback.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::draw::ORANGE; -use crate::input::state::PresetFeedbackKind; -use crate::ui::theme::{Rgb, set_color_alpha}; -use crate::ui::toolbar::ToolbarSnapshot; - -use super::super::super::super::widgets::draw_round_rect; -use super::PresetSlotLayout; - -/// Per-kind feedback flash tints (apply/save/clear). -/// TODO(theme-consolidation): near the overlay toast palette but not equal; -/// kept to avoid a visible shift. -const FEEDBACK_APPLY_RGB: Rgb = (0.35, 0.55, 0.95); -const FEEDBACK_SAVE_RGB: Rgb = (0.25, 0.75, 0.4); -const FEEDBACK_CLEAR_RGB: Rgb = (0.9, 0.3, 0.3); - -pub(super) fn draw_preset_feedback( - ctx: &cairo::Context, - snapshot: &ToolbarSnapshot, - layout_spec: &PresetSlotLayout, - slot_index: usize, - slot_x: f64, - preset_exists: bool, -) { - if let Some(feedback) = snapshot - .preset_feedback - .get(slot_index) - .and_then(|feedback| feedback.as_ref()) - { - let fade = (1.0 - feedback.progress as f64).clamp(0.0, 1.0); - if fade > 0.0 { - let tint = match feedback.kind { - PresetFeedbackKind::Apply => FEEDBACK_APPLY_RGB, - PresetFeedbackKind::Save => FEEDBACK_SAVE_RGB, - PresetFeedbackKind::Clear => FEEDBACK_CLEAR_RGB, - }; - set_color_alpha(ctx, tint, 0.35 * fade); - draw_round_rect( - ctx, - slot_x + 1.0, - layout_spec.slot_row_y + 1.0, - layout_spec.slot_size - 2.0, - layout_spec.slot_size - 2.0, - 6.0, - ); - let _ = ctx.fill(); - } - } - if preset_exists && snapshot.active_preset_slot == Some(slot_index + 1) { - ctx.set_source_rgba(ORANGE.r, ORANGE.g, ORANGE.b, 0.95); - ctx.set_line_width(2.0); - draw_round_rect( - ctx, - slot_x + 1.0, - layout_spec.slot_row_y + 1.0, - layout_spec.slot_size - 2.0, - layout_spec.slot_size - 2.0, - 7.0, - ); - let _ = ctx.stroke(); - } -} diff --git a/src/backend/wayland/toolbar/render/side_palette/presets/slot/mod.rs b/src/backend/wayland/toolbar/render/side_palette/presets/slot/mod.rs deleted file mode 100644 index 558be820..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/presets/slot/mod.rs +++ /dev/null @@ -1,246 +0,0 @@ -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::format_binding_label; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::config::action_label; -use crate::draw::Color; -use crate::toolbar_icons; -use crate::ui::toolbar::ToolbarEvent; -use crate::ui::toolbar::bindings::{action_for_clear_preset, action_for_save_preset}; - -use super::super::super::widgets::constants::{ - COLOR_TEXT_PRIMARY, COLOR_TEXT_SECONDARY, set_color, -}; -use super::super::super::widgets::{draw_button, draw_round_rect, point_in_rect}; -use super::SidePaletteLayout; -use super::widgets::draw_keycap; -use crate::ui::theme::{Rgb, Rgba, set_color_alpha}; - -mod content; -mod feedback; - -/// Empty slot well: near-black tint one step darker than -/// COLOR_PANEL_BACKGROUND (alpha varies with hover). -const EMPTY_SLOT_BG_RGB: Rgb = (0.05, 0.05, 0.07); -/// Dim save-hint plus glyph in an idle empty slot. -const COLOR_EMPTY_SLOT_PLUS: Rgba = (1.0, 1.0, 1.0, 0.45); -/// Clear badge fill: muted destructive red, quieter than the destructive -/// root. TODO(theme-consolidation): near COLOR_CLOSE_HOVER. -const COLOR_CLEAR_BADGE: Rgba = (0.75, 0.2, 0.2, 0.9); - -pub(super) struct PresetSlotLayout { - pub(super) slot_size: f64, - pub(super) slot_gap: f64, - pub(super) slot_row_y: f64, - pub(super) icon_size: f64, - pub(super) swatch_size: f64, - pub(super) number_box: f64, - pub(super) keycap_pad: f64, - pub(super) keycap_radius: f64, - pub(super) card_x: f64, - pub(super) card_w: f64, - pub(super) section_y: f64, -} - -/// Size of the hover-revealed clear (✕) badge in a filled slot's corner. -const CLEAR_BADGE_SIZE: f64 = 14.0; - -pub(super) fn draw_preset_slot( - layout: &mut SidePaletteLayout, - layout_spec: &PresetSlotLayout, - slot_index: usize, -) -> Option { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let hits = &mut layout.hits; - let hover = layout.hover; - let x = layout.x; - - let slot = slot_index + 1; - let slot_x = x + slot_index as f64 * (layout_spec.slot_size + layout_spec.slot_gap); - let preset = snapshot - .presets - .get(slot_index) - .and_then(|preset| preset.as_ref()); - let preset_exists = preset.is_some(); - - // Check hover for both filled and empty slots - let slot_hover_any = hover - .map(|(hx, hy)| { - point_in_rect( - hx, - hy, - slot_x, - layout_spec.slot_row_y, - layout_spec.slot_size, - layout_spec.slot_size, - ) - }) - .unwrap_or(false); - - // Hover for filled slots (for apply action) - let slot_hover = slot_hover_any && preset_exists; - // Hover for empty slots (for save hint) - let empty_slot_hover = slot_hover_any && !preset_exists; - - // The clear badge sits over the apply target and hit-testing is - // first-match, so its region must precede the slot's apply region. - let clear_badge = (preset_exists && slot_hover_any).then(|| { - let badge_x = slot_x + layout_spec.slot_size - CLEAR_BADGE_SIZE - 2.0; - let badge_y = layout_spec.slot_row_y + 2.0; - hits.push(HitRegion { - focus_id: None, - rect: (badge_x, badge_y, CLEAR_BADGE_SIZE, CLEAR_BADGE_SIZE), - event: ToolbarEvent::ClearPreset(slot), - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_for_clear_preset(slot) - .map(action_label) - .unwrap_or("Clear Preset"), - snapshot.binding_hints.clear_preset(slot), - )), - }); - (badge_x, badge_y) - }); - - draw_button( - ctx, - slot_x, - layout_spec.slot_row_y, - layout_spec.slot_size, - layout_spec.slot_size, - false, - slot_hover || empty_slot_hover, - ); - - if let Some(preset) = preset { - ctx.set_source_rgba(preset.color.r, preset.color.g, preset.color.b, 0.12); - draw_round_rect( - ctx, - slot_x + 1.0, - layout_spec.slot_row_y + 1.0, - layout_spec.slot_size - 2.0, - layout_spec.slot_size - 2.0, - 6.0, - ); - let _ = ctx.fill(); - ctx.set_source_rgba(preset.color.r, preset.color.g, preset.color.b, 0.35); - ctx.set_line_width(1.0); - draw_round_rect( - ctx, - slot_x + 1.0, - layout_spec.slot_row_y + 1.0, - layout_spec.slot_size - 2.0, - layout_spec.slot_size - 2.0, - 6.0, - ); - let _ = ctx.stroke(); - } else { - // Empty slot: click saves the current tool setup here. - let bg_alpha = if empty_slot_hover { 0.45 } else { 0.35 }; - set_color_alpha(ctx, EMPTY_SLOT_BG_RGB, bg_alpha); - draw_round_rect( - ctx, - slot_x + 1.0, - layout_spec.slot_row_y + 1.0, - layout_spec.slot_size - 2.0, - layout_spec.slot_size - 2.0, - 6.0, - ); - let _ = ctx.fill(); - - hits.push(HitRegion { - focus_id: None, - rect: ( - slot_x, - layout_spec.slot_row_y, - layout_spec.slot_size, - layout_spec.slot_size, - ), - event: ToolbarEvent::SavePreset(slot), - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_for_save_preset(slot) - .map(action_label) - .unwrap_or("Save Preset"), - snapshot.binding_hints.save_preset(slot), - )), - }); - - let plus = layout_spec.icon_size.min(14.0); - set_color( - ctx, - if empty_slot_hover { - COLOR_TEXT_SECONDARY - } else { - COLOR_EMPTY_SLOT_PLUS - }, - ); - toolbar_icons::draw_icon_plus( - ctx, - slot_x + (layout_spec.slot_size - plus) / 2.0, - layout_spec.slot_row_y + (layout_spec.slot_size - plus) / 2.0, - plus, - ); - } - - let hover_preset_color = content::draw_preset_content( - ctx, - snapshot, - hits, - layout_spec, - slot_x, - slot, - slot_hover, - preset, - ); - - let key_x = slot_x + layout_spec.keycap_pad; - let key_y = layout_spec.slot_row_y + layout_spec.keycap_pad; - draw_keycap( - ctx, - key_x, - key_y, - layout_spec.number_box, - layout_spec.keycap_radius, - &slot.to_string(), - preset_exists, - ); - - feedback::draw_preset_feedback( - ctx, - snapshot, - layout_spec, - slot_index, - slot_x, - preset_exists, - ); - - if let Some((badge_x, badge_y)) = clear_badge { - draw_clear_badge(ctx, badge_x, badge_y); - } - - hover_preset_color -} - -fn draw_clear_badge(ctx: &cairo::Context, x: f64, y: f64) { - let size = CLEAR_BADGE_SIZE; - set_color(ctx, COLOR_CLEAR_BADGE); - ctx.arc( - x + size / 2.0, - y + size / 2.0, - size / 2.0, - 0.0, - std::f64::consts::PI * 2.0, - ); - let _ = ctx.fill(); - set_color(ctx, COLOR_TEXT_PRIMARY); - let inset = size * 0.3; - ctx.set_line_width(1.6); - ctx.set_line_cap(cairo::LineCap::Round); - ctx.move_to(x + inset, y + inset); - ctx.line_to(x + size - inset, y + size - inset); - let _ = ctx.stroke(); - ctx.move_to(x + size - inset, y + inset); - ctx.line_to(x + inset, y + size - inset); - let _ = ctx.stroke(); -} diff --git a/src/backend/wayland/toolbar/render/side_palette/presets/widgets.rs b/src/backend/wayland/toolbar/render/side_palette/presets/widgets.rs deleted file mode 100644 index 7cc33fd4..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/presets/widgets.rs +++ /dev/null @@ -1,95 +0,0 @@ -use super::super::super::widgets::constants::{ - COLOR_CARD_BACKGROUND, COLOR_TEXT_PRIMARY, FONT_FAMILY_DEFAULT, FONT_SIZE_SECONDARY, - FONT_SIZE_SMALL, set_color, -}; -use super::super::super::widgets::{draw_label_center_color, draw_round_rect}; -use crate::ui::theme::{Rgb, rgba, set_color_alpha, with_alpha}; -use crate::ui_text::{UiTextStyle, text_layout}; - -/// White root for the keycap's border/text alpha ladder. -const WHITE_RGB: Rgb = (1.0, 1.0, 1.0); - -pub(super) fn draw_keycap( - ctx: &cairo::Context, - key_x: f64, - key_y: f64, - size: f64, - radius: f64, - label: &str, - active: bool, -) { - // Consistent alpha progressions: active is +0.35 from inactive - let (bg_alpha, border_alpha, text_alpha) = if active { - (0.65, 0.50, 0.95) - } else { - (0.30, 0.25, 0.55) - }; - set_color(ctx, with_alpha(COLOR_CARD_BACKGROUND, bg_alpha)); - draw_round_rect(ctx, key_x, key_y, size, size, radius); - let _ = ctx.fill(); - set_color_alpha(ctx, WHITE_RGB, border_alpha); - ctx.set_line_width(1.0); - draw_round_rect(ctx, key_x, key_y, size, size, radius); - let _ = ctx.stroke(); - let keycap_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_SECONDARY, - }; - draw_label_center_color( - ctx, - keycap_style, - key_x, - key_y, - size, - size, - label, - rgba(WHITE_RGB, text_alpha), - ); -} - -#[allow(clippy::too_many_arguments)] -pub(super) fn draw_preset_name_tag( - ctx: &cairo::Context, - name: &str, - slot_x: f64, - slot_row_y: f64, - slot_size: f64, - card_x: f64, - card_w: f64, - section_y: f64, -) { - let tag_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Normal, - size: FONT_SIZE_SMALL, - }; - let layout = text_layout(ctx, tag_style, name, None); - let extents = layout.ink_extents(); - let pad_x = 5.0; - let pad_y = 2.0; - let label_w = extents.width() + pad_x * 2.0; - let label_h = extents.height() + pad_y * 2.0; - let mut label_x = slot_x + (slot_size - label_w) / 2.0; - let label_y = (slot_row_y - label_h - 2.0).max(section_y + 2.0); - let min_x = card_x + 2.0; - let max_x = card_x + card_w - label_w - 2.0; - if label_x < min_x { - label_x = min_x; - } - if label_x > max_x { - label_x = max_x; - } - // Near-opaque card tint so the tag reads over any slot content. - set_color(ctx, with_alpha(COLOR_CARD_BACKGROUND, 0.92)); - draw_round_rect(ctx, label_x, label_y, label_w, label_h, 4.0); - let _ = ctx.fill(); - set_color(ctx, COLOR_TEXT_PRIMARY); - layout.show_at_baseline( - ctx, - label_x + pad_x - extents.x_bearing(), - label_y + pad_y - extents.y_bearing(), - ); -} diff --git a/src/backend/wayland/toolbar/render/side_palette/section_header.rs b/src/backend/wayland/toolbar/render/side_palette/section_header.rs deleted file mode 100644 index 5d708290..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/section_header.rs +++ /dev/null @@ -1,96 +0,0 @@ -use super::SidePaletteLayout; -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection}; -use crate::ui_text::UiTextStyle; - -use super::super::widgets::constants::{COLOR_LABEL_SECTION, set_color}; -use super::super::widgets::{draw_section_label, point_in_rect}; - -/// Scope a section title to the tool it currently edits ("Color — Pen"), -/// making the per-tool settings model visible. Falls back to the base -/// title when the contextual UI is off (sections then apply broadly). -pub(super) fn scoped_title(base: &str, snapshot: &crate::ui::toolbar::ToolbarSnapshot) -> String { - if !snapshot.context_aware_ui { - return base.to_string(); - } - let scope = if snapshot.text_active { - "Text" - } else if snapshot.note_active { - "Note" - } else { - crate::ui::toolbar::bindings::tool_label(snapshot.active_tool) - }; - format!("{base} — {scope}") -} - -pub(super) fn draw_collapsible_header( - layout: &mut SidePaletteLayout, - y: f64, - label_style: UiTextStyle<'static>, - section: ToolbarSideSection, - label: &str, - label_offset_y: f64, -) { - let collapsed = layout.snapshot.side_section_collapsed(section); - let hit_h = ToolbarLayoutSpec::SIDE_COLLAPSE_HEADER_HIT_HEIGHT; - let hover = layout - .hover - .map(|(hx, hy)| point_in_rect(hx, hy, layout.card_x, y, layout.card_w, hit_h)) - .unwrap_or(false); - - draw_section_label(layout.ctx, label_style, layout.x, y + label_offset_y, label); - - let size = ToolbarLayoutSpec::SIDE_COLLAPSE_CHEVRON_SIZE; - let chevron_x = layout.x + layout.content_width - size; - let chevron_y = y + (hit_h - size) / 2.0; - draw_header_chevron(layout.ctx, chevron_x, chevron_y, size, collapsed, hover); - - layout.hits.push(HitRegion { - focus_id: None, - rect: (layout.card_x, y, layout.card_w, hit_h), - event: ToolbarEvent::ToggleSideSectionCollapsed(section, !collapsed), - kind: HitKind::Click, - tooltip: Some(format!( - "{} {}", - if collapsed { "Expand" } else { "Collapse" }, - label - )), - }); -} - -fn draw_header_chevron( - ctx: &cairo::Context, - x: f64, - y: f64, - size: f64, - collapsed: bool, - hover: bool, -) { - set_color( - ctx, - ( - COLOR_LABEL_SECTION.0, - COLOR_LABEL_SECTION.1, - COLOR_LABEL_SECTION.2, - if hover { 1.0 } else { COLOR_LABEL_SECTION.3 }, - ), - ); - ctx.set_line_width(1.6); - ctx.set_line_cap(cairo::LineCap::Round); - ctx.set_line_join(cairo::LineJoin::Round); - let margin = size * 0.28; - if collapsed { - let mid_y = y + size / 2.0; - ctx.move_to(x + margin, y + margin); - ctx.line_to(x + size - margin, mid_y); - ctx.line_to(x + margin, y + size - margin); - } else { - let mid_x = x + size / 2.0; - ctx.move_to(x + margin, y + margin); - ctx.line_to(mid_x, y + size - margin); - ctx.line_to(x + size - margin, y + margin); - } - let _ = ctx.stroke(); -} diff --git a/src/backend/wayland/toolbar/render/side_palette/session.rs b/src/backend/wayland/toolbar/render/side_palette/session.rs deleted file mode 100644 index d2ba7d25..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/session.rs +++ /dev/null @@ -1,442 +0,0 @@ -use super::SidePaletteLayout; -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::format_binding_label; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::backend::wayland::toolbar::rows::{grid_layout, row_item_width}; -use crate::toolbar_icons; -use crate::ui::toolbar::ToolbarEvent; -use crate::ui::toolbar::model::{ToolbarSessionButton, ToolbarSessionModel}; -use crate::ui_text::{UiTextStyle, draw_text_baseline, text_layout}; - -use super::super::widgets::constants::{ - COLOR_TEXT_DISABLED, COLOR_TEXT_PRIMARY, FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, - FONT_SIZE_SECONDARY, set_color, -}; -use super::super::widgets::{ - draw_button, draw_destructive_button, draw_group_card, draw_label_center, point_in_rect, - set_icon_color, -}; -use super::section_header::draw_collapsible_header; -use crate::ui::toolbar::ToolbarSideSection; - -const SESSION_BUTTON_ICON_SIZE: f64 = 12.0; -const SESSION_BUTTON_ICON_GAP: f64 = 4.0; -const SESSION_BUTTON_ICON_LABEL_SIZE: f64 = FONT_SIZE_SECONDARY; -const SESSION_RECENT_ICON_SIZE: f64 = 13.0; -const SESSION_RECENT_ICON_GAP: f64 = 6.0; -const SESSION_RECENT_PADDING_X: f64 = 7.0; - -pub(super) fn draw_session_section(layout: &mut SidePaletteLayout, y: &mut f64) { - let Some(model) = ToolbarSessionModel::from_snapshot(layout.snapshot) else { - return; - }; - - let ctx = layout.ctx; - let card_h = layout.spec.side_session_height(layout.snapshot); - draw_group_card(ctx, layout.card_x, *y, layout.card_w, card_h); - - let label_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_LABEL, - }; - let meta_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Normal, - size: FONT_SIZE_SECONDARY, - }; - let path_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Normal, - size: 9.5, - }; - - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::Session, - ToolbarSideSection::Session.label(), - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL, - ); - if layout - .snapshot - .side_section_collapsed(ToolbarSideSection::Session) - { - *y += card_h + layout.section_gap; - return; - } - - let meta_y = *y + ToolbarLayoutSpec::SIDE_SECTION_TOGGLE_OFFSET_Y; - draw_text_baseline( - ctx, - meta_style, - &truncate_middle(&model.active_name, 28), - layout.x, - meta_y + 12.0, - None, - ); - draw_text_baseline( - ctx, - path_style, - &truncate_start(&model.active_path_label, 34), - layout.x, - meta_y + 27.0, - None, - ); - - let mut row_y = meta_y - + ToolbarLayoutSpec::SIDE_SESSION_META_HEIGHT - + ToolbarLayoutSpec::SIDE_SESSION_ROW_GAP; - let controls_h = draw_session_controls(layout, &model, row_y, label_style, meta_style); - row_y += controls_h + ToolbarLayoutSpec::SIDE_SESSION_ROW_GAP; - draw_recent_sessions(layout, &model, row_y, meta_style); - - *y += card_h + layout.section_gap; -} - -fn draw_session_controls( - layout: &mut SidePaletteLayout, - model: &ToolbarSessionModel, - y: f64, - label_style: UiTextStyle<'static>, - meta_style: UiTextStyle<'static>, -) -> f64 { - if model.overwrite_confirmation.is_some() { - draw_save_as_overwrite_confirmation(layout, model, y, label_style, meta_style) - } else { - draw_session_buttons(layout, model, y, label_style) - } -} - -fn draw_session_buttons( - layout: &mut SidePaletteLayout, - model: &ToolbarSessionModel, - y: f64, - label_style: UiTextStyle<'static>, -) -> f64 { - let button_gap = ToolbarLayoutSpec::SIDE_SESSION_ROW_GAP; - let button_h = ToolbarLayoutSpec::SIDE_SESSION_BUTTON_HEIGHT; - let columns = model.button_columns(); - let button_w = row_item_width(layout.content_width, columns, button_gap); - let button_label_style = if layout.snapshot.use_icons { - UiTextStyle { - size: SESSION_BUTTON_ICON_LABEL_SIZE, - ..label_style - } - } else { - label_style - }; - let grid = grid_layout( - layout.x, - y, - button_w, - button_h, - button_gap, - button_gap, - columns, - model.buttons.len(), - ); - for (item, button) in grid.items.iter().zip(model.buttons.iter()) { - draw_session_button( - layout, - button, - item.x, - item.y, - item.w, - item.h, - button_label_style, - ); - } - grid.height -} - -fn draw_save_as_overwrite_confirmation( - layout: &mut SidePaletteLayout, - model: &ToolbarSessionModel, - y: f64, - label_style: UiTextStyle<'static>, - meta_style: UiTextStyle<'static>, -) -> f64 { - let Some(confirmation) = model.overwrite_confirmation.as_ref() else { - return 0.0; - }; - let message = format!("Replace {}?", truncate_middle(&confirmation.label, 20)); - draw_text_baseline(layout.ctx, meta_style, &message, layout.x, y + 11.0, None); - - let button_gap = ToolbarLayoutSpec::SIDE_SESSION_ROW_GAP; - let button_h = ToolbarLayoutSpec::SIDE_SESSION_BUTTON_HEIGHT; - let button_y = y + ToolbarLayoutSpec::SIDE_SESSION_CONFIRM_LABEL_HEIGHT; - let button_w = row_item_width(layout.content_width, 2, button_gap); - let grid = grid_layout( - layout.x, button_y, button_w, button_h, button_gap, button_gap, 2, 2, - ); - let actions = [ - ("Replace", confirmation.confirm_event(), true), - ("Cancel", confirmation.cancel_event(), false), - ]; - for (item, (label, event, destructive)) in grid.items.iter().zip(actions) { - let hover = layout - .hover - .map(|(hx, hy)| point_in_rect(hx, hy, item.x, item.y, item.w, item.h)) - .unwrap_or(false); - if destructive { - draw_destructive_button(layout.ctx, item.x, item.y, item.w, item.h, hover); - } else { - draw_button(layout.ctx, item.x, item.y, item.w, item.h, false, hover); - } - draw_label_center( - layout.ctx, - label_style, - item.x, - item.y, - item.w, - item.h, - label, - ); - layout.hits.push(HitRegion { - focus_id: None, - rect: (item.x, item.y, item.w, item.h), - event, - kind: HitKind::Click, - tooltip: Some(label.to_string()), - }); - } - - ToolbarLayoutSpec::SIDE_SESSION_CONFIRM_LABEL_HEIGHT + grid.height -} - -fn draw_session_button( - layout: &mut SidePaletteLayout, - button: &ToolbarSessionButton, - x: f64, - y: f64, - w: f64, - h: f64, - label_style: UiTextStyle<'static>, -) { - let hover = layout - .hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, y, w, h)) - .unwrap_or(false) - && button.enabled; - if matches!(button.event, ToolbarEvent::ClearSession) && button.enabled { - draw_destructive_button(layout.ctx, x, y, w, h, hover); - } else { - draw_button(layout.ctx, x, y, w, h, false, hover); - } - - if layout.snapshot.use_icons { - draw_session_button_icon_label(layout, button, x, y, w, h, label_style, hover); - } else { - draw_label_center(layout.ctx, label_style, x, y, w, h, button.label); - } - - if button.enabled { - layout.hits.push(HitRegion { - focus_id: None, - rect: (x, y, w, h), - event: button.event.clone(), - kind: HitKind::Click, - tooltip: Some(button.label.to_string()), - }); - } -} - -#[allow(clippy::too_many_arguments)] -fn draw_session_button_icon_label( - layout: &SidePaletteLayout, - button: &ToolbarSessionButton, - x: f64, - y: f64, - w: f64, - h: f64, - label_style: UiTextStyle<'static>, - hover: bool, -) { - let Some(icon) = session_button_icon(&button.event) else { - return; - }; - - let label_layout = text_layout(layout.ctx, label_style, button.label, None); - let label_ext = label_layout.ink_extents(); - let group_w = SESSION_BUTTON_ICON_SIZE + SESSION_BUTTON_ICON_GAP + label_ext.width(); - let group_x = (x + (w - group_w) / 2.0).max(x + 3.0); - let icon_y = y + (h - SESSION_BUTTON_ICON_SIZE) / 2.0; - let label_x = - group_x + SESSION_BUTTON_ICON_SIZE + SESSION_BUTTON_ICON_GAP - label_ext.x_bearing(); - let label_y = y + (h - label_ext.height()) / 2.0 - label_ext.y_bearing(); - - if button.enabled { - set_icon_color(layout.ctx, hover); - } else { - set_color(layout.ctx, COLOR_TEXT_DISABLED); - } - icon(layout.ctx, group_x, icon_y, SESSION_BUTTON_ICON_SIZE); - - if button.enabled { - set_color(layout.ctx, COLOR_TEXT_PRIMARY); - } else { - set_color(layout.ctx, COLOR_TEXT_DISABLED); - } - label_layout.show_at_baseline(layout.ctx, label_x, label_y); -} - -fn session_button_icon(event: &ToolbarEvent) -> Option { - match event { - ToolbarEvent::OpenSession => Some(toolbar_icons::draw_icon_file), - ToolbarEvent::SaveSessionAs => Some(toolbar_icons::draw_icon_save), - ToolbarEvent::SessionInfo => Some(toolbar_icons::draw_icon_info), - ToolbarEvent::ClearSession => Some(toolbar_icons::draw_icon_clear), - ToolbarEvent::OpenConfigurator => Some(toolbar_icons::draw_icon_settings), - _ => None, - } -} - -fn draw_recent_sessions( - layout: &mut SidePaletteLayout, - model: &ToolbarSessionModel, - mut y: f64, - label_style: UiTextStyle<'static>, -) { - for recent in &model.recents { - let hover = layout - .hover - .map(|(hx, hy)| { - point_in_rect( - hx, - hy, - layout.x, - y, - layout.content_width, - ToolbarLayoutSpec::SIDE_SESSION_RECENT_HEIGHT, - ) - }) - .unwrap_or(false); - draw_button( - layout.ctx, - layout.x, - y, - layout.content_width, - ToolbarLayoutSpec::SIDE_SESSION_RECENT_HEIGHT, - false, - hover, - ); - set_icon_color(layout.ctx, hover); - let icon_x = layout.x + SESSION_RECENT_PADDING_X; - let icon_y = - y + (ToolbarLayoutSpec::SIDE_SESSION_RECENT_HEIGHT - SESSION_RECENT_ICON_SIZE) / 2.0; - toolbar_icons::draw_icon_file(layout.ctx, icon_x, icon_y, SESSION_RECENT_ICON_SIZE); - set_color(layout.ctx, COLOR_TEXT_PRIMARY); - draw_text_baseline( - layout.ctx, - label_style, - &truncate_middle(strip_session_extension(&recent.label), 24), - icon_x + SESSION_RECENT_ICON_SIZE + SESSION_RECENT_ICON_GAP, - y + 14.0, - None, - ); - let tooltip_path = recent.path.display().to_string(); - layout.hits.push(HitRegion { - focus_id: None, - rect: ( - layout.x, - y, - layout.content_width, - ToolbarLayoutSpec::SIDE_SESSION_RECENT_HEIGHT, - ), - event: recent.event(), - kind: HitKind::Click, - tooltip: Some(format_binding_label( - &format!("Open {}", recent.label), - Some(tooltip_path.as_str()), - )), - }); - y += - ToolbarLayoutSpec::SIDE_SESSION_RECENT_HEIGHT + ToolbarLayoutSpec::SIDE_SESSION_ROW_GAP; - } -} - -/// Middle-ellipsize so both the head and the distinguishing tail survive. -/// Tail truncation made e.g. two different "lecture-05-…" files render -/// identically in the recents list. Shared with the top strip's Session -/// popover so both surfaces truncate identically. -pub(in crate::backend::wayland::toolbar) fn truncate_middle( - value: &str, - max_chars: usize, -) -> String { - let count = value.chars().count(); - if count <= max_chars { - return value.to_string(); - } - let keep = max_chars.saturating_sub(1).max(2); - let head = keep.div_ceil(2); - let tail = keep - head; - let mut truncated: String = value.chars().take(head).collect(); - truncated.push('…'); - truncated.extend(value.chars().skip(count - tail)); - truncated -} - -/// Keep the tail of a path — the leading directories are the least -/// informative part of a session path. -pub(in crate::backend::wayland::toolbar) fn truncate_start( - value: &str, - max_chars: usize, -) -> String { - let count = value.chars().count(); - if count <= max_chars { - return value.to_string(); - } - let keep = max_chars.saturating_sub(1); - let mut truncated = String::from("…"); - truncated.extend(value.chars().skip(count - keep)); - truncated -} - -/// Drop the constant session-file extension in list rows; it costs the -/// characters that distinguish one session from another. -pub(in crate::backend::wayland::toolbar) fn strip_session_extension(value: &str) -> &str { - value - .strip_suffix(".wayscriber-session") - .or_else(|| value.strip_suffix(".wayscriber")) - .unwrap_or(value) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn truncate_middle_keeps_head_and_tail() { - assert_eq!(truncate_middle("short", 10), "short"); - let truncated = truncate_middle("lecture-05-quantum-mechanics-part-two", 20); - assert_eq!(truncated.chars().count(), 20); - assert!(truncated.starts_with("lecture-05")); - assert!(truncated.ends_with("part-two")); - assert!(truncated.contains('…')); - } - - #[test] - fn truncate_start_keeps_path_tail() { - assert_eq!(truncate_start("/tmp/x", 10), "/tmp/x"); - let truncated = truncate_start("/home/user/.local/share/wayscriber/sessions", 20); - assert_eq!(truncated.chars().count(), 20); - assert!(truncated.starts_with('…')); - assert!(truncated.ends_with("wayscriber/sessions")); - } - - #[test] - fn strip_session_extension_drops_known_suffixes() { - assert_eq!( - strip_session_extension("lecture.wayscriber-session"), - "lecture" - ); - assert_eq!(strip_session_extension("lecture.wayscriber"), "lecture"); - assert_eq!(strip_session_extension("lecture.txt"), "lecture.txt"); - } -} diff --git a/src/backend/wayland/toolbar/render/side_palette/settings.rs b/src/backend/wayland/toolbar/render/side_palette/settings.rs deleted file mode 100644 index 57cc5a60..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/settings.rs +++ /dev/null @@ -1,490 +0,0 @@ -use super::SidePaletteLayout; -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::backend::wayland::toolbar::rows::{grid_layout, row_item_width}; -use crate::toolbar_icons; -use crate::ui::toolbar::model::{ToolbarActivation, ToolbarIcon, ToolbarSettingsModel}; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection}; -use crate::ui_text::UiTextStyle; - -use super::super::widgets::constants::{ - FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, FONT_SIZE_SECONDARY, FONT_SIZE_TOOLTIP, -}; -use super::super::widgets::*; -use super::section_header::draw_collapsible_header; - -pub(super) fn draw_settings_section(layout: &mut SidePaletteLayout, y: &mut f64) { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let hover = layout.hover; - let x = layout.x; - let card_x = layout.card_x; - let card_w = layout.card_w; - let content_width = layout.content_width; - let section_gap = layout.section_gap; - let use_icons = snapshot.use_icons; - let label_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_LABEL, - }; - let toggle_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_TOOLTIP, - }; - - let Some(settings_model) = ToolbarSettingsModel::from_snapshot(snapshot) else { - return; - }; - - let settings_card_h = layout.spec.side_settings_height(snapshot); - draw_group_card(ctx, card_x, *y, card_w, settings_card_h); - let customizing = snapshot.customize_items_open; - let dedicated_panel = customizing; - let header_label = if customizing { - "Customize toolbar" - } else { - ToolbarSideSection::Settings.label() - }; - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::Settings, - header_label, - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL, - ); - if !dedicated_panel && snapshot.side_section_collapsed(ToolbarSideSection::Settings) { - *y += settings_card_h + section_gap; - return; - } - - let hits = &mut layout.hits; - let toggle_h = ToolbarLayoutSpec::SIDE_TOGGLE_HEIGHT; - let toggle_gap = ToolbarLayoutSpec::SIDE_TOGGLE_GAP; - - let mut toggle_y = *y + ToolbarLayoutSpec::SIDE_SECTION_TOGGLE_OFFSET_Y; - - // Layout-mode presets: Simple / Regular / Advanced. Non-destructive — - // switching re-baselines the sections without touching explicit - // user overrides. - if !dedicated_panel { - let seg_h = ToolbarLayoutSpec::SIDE_SEGMENT_HEIGHT; - let seg_w = content_width; - let mode_control = crate::ui::toolbar::model::layout_mode_control(snapshot.layout_mode); - draw_layout_mode_segments( - ctx, - hits, - hover, - &mode_control, - x, - toggle_y, - seg_w, - seg_h, - toggle_style, - ); - toggle_y += seg_h + toggle_gap; - } - - let toggle_col_gap = toggle_gap; - let toggle_col_w = row_item_width(content_width, 2, toggle_col_gap); - let toggle_rows = settings_model.toggle_rows(); - let toggle_row_count = toggle_rows.len(); - for (row_index, row) in toggle_rows.into_iter().enumerate() { - let row_y = toggle_y + row_index as f64 * (toggle_h + toggle_gap); - // A lone wide toggle spans the full content width; narrow toggles - // sit in half-width cells. - let full_row = row.len() == 1 && row[0].wide; - for (col, toggle) in row.into_iter().enumerate() { - let (cell_x, cell_w) = if full_row { - (x, content_width) - } else { - ( - x + col as f64 * (toggle_col_w + toggle_col_gap), - toggle_col_w, - ) - }; - let toggle_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, cell_x, row_y, cell_w, toggle_h)) - .unwrap_or(false); - draw_checkbox( - ctx, - cell_x, - row_y, - cell_w, - toggle_h, - toggle.checked, - toggle_hover, - toggle_style, - toggle.label.as_ref(), - ); - hits.push(HitRegion { - focus_id: None, - rect: (cell_x, row_y, cell_w, toggle_h), - event: activation_event(&toggle.activation), - kind: HitKind::Click, - tooltip: toggle.tooltip.as_string(), - }); - } - } - - let mut notices_y = toggle_y; - if toggle_row_count > 0 { - notices_y += toggle_row_count as f64 * (toggle_h + toggle_gap) - toggle_gap; - } - notices_y += toggle_gap; - for notice in settings_model.notices() { - let notice_h = layout - .spec - .side_settings_notice_height(notice, content_width); - draw_label_left_wrapped( - ctx, - button_label_style(), - x, - notices_y, - content_width, - notice_h, - notice.text.as_ref(), - ); - notices_y += notice_h + toggle_gap; - } - - let buttons_y = notices_y; - let button_h = ToolbarLayoutSpec::SIDE_SETTINGS_BUTTON_HEIGHT; - let button_gap = ToolbarLayoutSpec::SIDE_SETTINGS_BUTTON_GAP; - let button_w = row_item_width(content_width, 2, button_gap); - let icon_size = 16.0; - - let buttons = settings_model.buttons(); - let button_layout = grid_layout( - x, - buttons_y, - button_w, - button_h, - button_gap, - button_gap, - 2, - buttons.len(), - ); - let button_label_style = button_label_style(); - for (item, button) in button_layout.items.iter().zip(buttons.iter()) { - let button_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, item.x, item.y, item.w, item.h)) - .unwrap_or(false); - if button.event.is_destructive() { - draw_destructive_button(ctx, item.x, item.y, item.w, item.h, button_hover); - } else { - draw_button(ctx, item.x, item.y, item.w, item.h, false, button_hover); - } - if use_icons { - // Icon plus a left-aligned label: the icon-only glyphs were - // ambiguous and left the row looking sparse and crammed at once. - set_icon_color(ctx, button_hover); - let icon_x = item.x + 6.0; - draw_settings_icon( - ctx, - button.icon, - icon_x, - item.y + (item.h - icon_size) / 2.0, - icon_size, - ); - let text_x = icon_x + icon_size + 5.0; - let text_w = item.x + item.w - text_x - 4.0; - let display = - ellipsize_to_width(ctx, button_label_style, button.label.as_ref(), text_w); - draw_label_left( - ctx, - button_label_style, - text_x, - item.y, - text_w, - item.h, - &display, - ); - } else { - draw_label_center( - ctx, - label_style, - item.x, - item.y, - item.w, - item.h, - button.label.as_ref(), - ); - } - hits.push(HitRegion { - focus_id: None, - rect: (item.x, item.y, item.w, item.h), - event: button.event.clone(), - kind: HitKind::Click, - tooltip: button.tooltip.as_string(), - }); - } - - let mut customize_y = buttons_y; - if button_layout.rows > 0 { - customize_y += button_layout.height; - } - customize_y += toggle_gap; - let groups = settings_model.groups(); - if !groups.is_empty() { - draw_label_left( - ctx, - label_style, - x, - customize_y, - content_width, - toggle_h, - "Choose a group", - ); - customize_y += toggle_h + toggle_gap; - } - let group_layout = grid_layout( - x, - customize_y, - button_w, - button_h, - button_gap, - button_gap, - 2, - groups.len(), - ); - for (item, group) in group_layout.items.iter().zip(groups.iter()) { - let group_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, item.x, item.y, item.w, item.h)) - .unwrap_or(false); - draw_button(ctx, item.x, item.y, item.w, item.h, false, group_hover); - draw_label_center( - ctx, - label_style, - item.x, - item.y, - item.w, - item.h, - group.label.as_ref(), - ); - hits.push(HitRegion { - focus_id: None, - rect: (item.x, item.y, item.w, item.h), - event: group.event.clone(), - kind: HitKind::Click, - tooltip: group.tooltip.as_string(), - }); - } - - let mut items_y = customize_y; - if group_layout.rows > 0 { - items_y += group_layout.height + toggle_gap; - } - let item_overrides = settings_model.item_overrides(); - if !item_overrides.is_empty() { - draw_label_left( - ctx, - label_style, - x, - items_y, - content_width, - toggle_h, - snapshot - .customize_items_group - .map_or("Uncheck items to hide", |group| group.label()), - ); - items_y += toggle_h + toggle_gap; - } - let item_layout = grid_layout( - x, - items_y, - content_width, - toggle_h, - toggle_col_gap, - toggle_gap, - 1, - item_overrides.len(), - ); - for (item, override_item) in item_layout.items.iter().zip(item_overrides.iter()) { - let item_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, item.x, item.y, item.w, item.h)) - .unwrap_or(false); - let order = override_item.order.as_ref(); - let order_gap = 4.0; - let handle_w = if order.is_some() { 24.0 } else { 0.0 }; - let move_btn_w = if order.is_some() { 28.0 } else { 0.0 }; - let move_buttons_w = if order.is_some() { - move_btn_w * 2.0 + order_gap * 2.0 - } else { - 0.0 - }; - if order.is_some() { - let handle_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, item.x, item.y, handle_w, item.h)) - .unwrap_or(false); - draw_button(ctx, item.x, item.y, handle_w, item.h, false, handle_hover); - draw_label_center(ctx, toggle_style, item.x, item.y, handle_w, item.h, "="); - } - let checkbox_x = item.x + handle_w + if order.is_some() { order_gap } else { 0.0 }; - let checkbox_w = - item.w - handle_w - move_buttons_w - if order.is_some() { order_gap } else { 0.0 }; - draw_checkbox( - ctx, - checkbox_x, - item.y, - checkbox_w, - item.h, - override_item.shown, - item_hover, - toggle_style, - override_item.label.as_ref(), - ); - hits.push(HitRegion { - focus_id: None, - rect: (checkbox_x, item.y, checkbox_w, item.h), - event: activation_event(&override_item.activation), - kind: HitKind::Click, - tooltip: override_item.tooltip.as_string(), - }); - if let Some(order) = order { - let up_x = item.x + item.w - move_btn_w * 2.0 - order_gap; - let down_x = item.x + item.w - move_btn_w; - for (button_x, label, enabled, activation, tooltip) in [ - (up_x, "^", order.can_move_up, &order.move_up, "Move up"), - ( - down_x, - "v", - order.can_move_down, - &order.move_down, - "Move down", - ), - ] { - let button_hover = enabled - && hover - .map(|(hx, hy)| point_in_rect(hx, hy, button_x, item.y, move_btn_w, item.h)) - .unwrap_or(false); - draw_button( - ctx, - button_x, - item.y, - move_btn_w, - item.h, - false, - button_hover, - ); - draw_label_center( - ctx, - toggle_style, - button_x, - item.y, - move_btn_w, - item.h, - label, - ); - if enabled { - hits.push(HitRegion { - focus_id: None, - rect: (button_x, item.y, move_btn_w, item.h), - event: activation_event(activation), - kind: HitKind::Click, - tooltip: Some(format!("{} {}", tooltip, override_item.label)), - }); - } - } - hits.push(HitRegion { - focus_id: None, - rect: (item.x, item.y, item.w, item.h), - event: ToolbarEvent::StartToolbarItemDrag { - group: order.group, - id: override_item.id, - }, - kind: HitKind::DragToolbarItem { - group: order.group, - id: override_item.id, - target_index: order.index, - }, - tooltip: Some(format!("Drag {} to reorder", override_item.label)), - }); - } - } - - *y += settings_card_h + section_gap; -} - -fn button_label_style() -> UiTextStyle<'static> { - UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Normal, - size: FONT_SIZE_SECONDARY, - } -} - -fn activation_event(activation: &ToolbarActivation) -> crate::ui::toolbar::ToolbarEvent { - activation.compatibility_event() -} - -fn draw_settings_icon(ctx: &cairo::Context, icon: ToolbarIcon, x: f64, y: f64, size: f64) { - match icon { - ToolbarIcon::Back => draw_back_icon(ctx, x, y, size), - ToolbarIcon::Settings => toolbar_icons::draw_icon_settings(ctx, x, y, size), - ToolbarIcon::Search => toolbar_icons::draw_icon_search(ctx, x, y, size), - ToolbarIcon::Visibility => toolbar_icons::draw_icon_visibility(ctx, x, y, size), - ToolbarIcon::File => toolbar_icons::draw_icon_file(ctx, x, y, size), - ToolbarIcon::Info => toolbar_icons::draw_icon_info(ctx, x, y, size), - ToolbarIcon::More | ToolbarIcon::Board => {} - } -} - -fn draw_back_icon(ctx: &cairo::Context, x: f64, y: f64, size: f64) { - let mid_y = y + size * 0.5; - ctx.set_line_width(2.0); - ctx.move_to(x + size * 0.65, y + size * 0.25); - ctx.line_to(x + size * 0.35, mid_y); - ctx.line_to(x + size * 0.65, y + size * 0.75); - let _ = ctx.stroke(); -} - -#[allow(clippy::too_many_arguments)] -fn draw_layout_mode_segments( - ctx: &cairo::Context, - hits: &mut Vec, - hover: Option<(f64, f64)>, - control: &crate::ui::toolbar::model::ToolbarControl, - x: f64, - y: f64, - w: f64, - h: f64, - label_style: UiTextStyle<'_>, -) { - let crate::ui::toolbar::model::ToolbarControlKind::Segmented(segmented) = &control.kind else { - return; - }; - let segments = segmented.segments(); - if segments.is_empty() { - return; - } - let active = segmented - .active_segment() - .and_then(|active| segments.iter().position(|s| s.id == active)) - .unwrap_or(0); - // Same treatment as the pane navigation row: equal-width segments with - // a small gap, spanning the full content width. - let seg_gap = 4.0; - let seg_w = (w - seg_gap * (segments.len() as f64 - 1.0)) / segments.len() as f64; - for (index, segment) in segments.iter().enumerate() { - let seg_x = x + (seg_w + seg_gap) * index as f64; - let is_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, seg_x, y, seg_w, h)) - .unwrap_or(false); - draw_button(ctx, seg_x, y, seg_w, h, index == active, is_hover); - draw_label_center(ctx, label_style, seg_x, y, seg_w, h, segment.label.as_ref()); - hits.push(HitRegion { - focus_id: None, - rect: (seg_x, y, seg_w, h), - event: segment.activation.compatibility_event(), - kind: HitKind::Click, - tooltip: segment.tooltip.as_string(), - }); - } -} diff --git a/src/backend/wayland/toolbar/render/side_palette/step/custom_rows.rs b/src/backend/wayland/toolbar/render/side_palette/step/custom_rows.rs deleted file mode 100644 index 212516c7..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/step/custom_rows.rs +++ /dev/null @@ -1,250 +0,0 @@ -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::toolbar_icons; -use crate::ui::toolbar::model::{ToolbarSliderSpec, delay_secs_from_t, delay_t_from_ms}; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot}; -use crate::ui_text::UiTextStyle; - -use super::super::super::widgets::constants::{FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, set_color}; -use super::super::super::widgets::*; -use super::{COLOR_DELAY_KNOB, COLOR_DELAY_TRACK}; - -pub(super) fn draw_custom_rows( - ctx: &cairo::Context, - hits: &mut Vec, - x: f64, - y: f64, - card_w: f64, - snapshot: &ToolbarSnapshot, - hover: Option<(f64, f64)>, -) { - let label_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_LABEL, - }; - let mut context = CustomRowContext { - ctx, - hits, - x, - card_w, - snapshot, - hover, - label_style, - }; - let undo_row_h = context.draw_row(y, true); - let redo_y = y + undo_row_h + 8.0; - context.draw_row(redo_y, false); -} - -struct CustomRowContext<'a> { - ctx: &'a cairo::Context, - hits: &'a mut Vec, - x: f64, - card_w: f64, - snapshot: &'a ToolbarSnapshot, - hover: Option<(f64, f64)>, - label_style: UiTextStyle<'a>, -} - -impl<'a> CustomRowContext<'a> { - fn draw_row(&mut self, y: f64, is_undo: bool) -> f64 { - let row_h = 26.0; - let btn_w = if self.snapshot.use_icons { 42.0 } else { 90.0 }; - let steps_btn_w = 26.0; - let gap = 6.0; - let label = if is_undo { "Step Undo" } else { "Step Redo" }; - let steps = if is_undo { - self.snapshot.custom_undo_steps - } else { - self.snapshot.custom_redo_steps - }; - let delay_ms = if is_undo { - self.snapshot.custom_undo_delay_ms - } else { - self.snapshot.custom_redo_delay_ms - }; - - let btn_hover = self - .hover - .map(|(hx, hy)| point_in_rect(hx, hy, self.x, y, btn_w, row_h)) - .unwrap_or(false); - - if self.snapshot.use_icons { - let icon_size = 20.0; - draw_button(self.ctx, self.x, y, btn_w, row_h, false, btn_hover); - set_icon_color(self.ctx, btn_hover); - if is_undo { - toolbar_icons::draw_icon_step_undo( - self.ctx, - self.x + (btn_w - icon_size) / 2.0, - y + (row_h - icon_size) / 2.0, - icon_size, - ); - } else { - toolbar_icons::draw_icon_step_redo( - self.ctx, - self.x + (btn_w - icon_size) / 2.0, - y + (row_h - icon_size) / 2.0, - icon_size, - ); - } - } else { - draw_button(self.ctx, self.x, y, btn_w, row_h, false, btn_hover); - draw_label_left( - self.ctx, - self.label_style, - self.x + 10.0, - y, - btn_w - 20.0, - row_h, - label, - ); - } - self.hits.push(HitRegion { - focus_id: None, - rect: (self.x, y, btn_w, row_h), - event: if is_undo { - ToolbarEvent::CustomUndo - } else { - ToolbarEvent::CustomRedo - }, - kind: HitKind::Click, - tooltip: Some(if is_undo { - "Step undo".to_string() - } else { - "Step redo".to_string() - }), - }); - - let steps_x = self.x + btn_w + gap; - let minus_hover = self - .hover - .map(|(hx, hy)| point_in_rect(hx, hy, steps_x, y, steps_btn_w, row_h)) - .unwrap_or(false); - draw_button(self.ctx, steps_x, y, steps_btn_w, row_h, false, minus_hover); - set_icon_color(self.ctx, minus_hover); - toolbar_icons::draw_icon_minus( - self.ctx, - steps_x + (steps_btn_w - ToolbarLayoutSpec::SIDE_NUDGE_ICON_SIZE) / 2.0, - y + (row_h - ToolbarLayoutSpec::SIDE_NUDGE_ICON_SIZE) / 2.0, - ToolbarLayoutSpec::SIDE_NUDGE_ICON_SIZE, - ); - self.hits.push(HitRegion { - focus_id: None, - rect: (steps_x, y, steps_btn_w, row_h), - event: if is_undo { - ToolbarEvent::SetCustomUndoSteps(steps.saturating_sub(1).max(1)) - } else { - ToolbarEvent::SetCustomRedoSteps(steps.saturating_sub(1).max(1)) - }, - kind: HitKind::Click, - tooltip: Some(if is_undo { - "Decrease undo steps".to_string() - } else { - "Decrease redo steps".to_string() - }), - }); - - let steps_val_x = steps_x + steps_btn_w + 4.0; - draw_label_center( - self.ctx, - self.label_style, - steps_val_x, - y, - 54.0, - row_h, - &format!("{} steps", steps), - ); - - let steps_plus_x = steps_val_x + 58.0; - let plus_hover = self - .hover - .map(|(hx, hy)| point_in_rect(hx, hy, steps_plus_x, y, steps_btn_w, row_h)) - .unwrap_or(false); - draw_button( - self.ctx, - steps_plus_x, - y, - steps_btn_w, - row_h, - false, - plus_hover, - ); - set_icon_color(self.ctx, plus_hover); - toolbar_icons::draw_icon_plus( - self.ctx, - steps_plus_x + (steps_btn_w - ToolbarLayoutSpec::SIDE_NUDGE_ICON_SIZE) / 2.0, - y + (row_h - ToolbarLayoutSpec::SIDE_NUDGE_ICON_SIZE) / 2.0, - ToolbarLayoutSpec::SIDE_NUDGE_ICON_SIZE, - ); - self.hits.push(HitRegion { - focus_id: None, - rect: (steps_plus_x, y, steps_btn_w, row_h), - event: if is_undo { - ToolbarEvent::SetCustomUndoSteps(steps.saturating_add(1)) - } else { - ToolbarEvent::SetCustomRedoSteps(steps.saturating_add(1)) - }, - kind: HitKind::Click, - tooltip: Some(if is_undo { - "Increase undo steps".to_string() - } else { - "Increase redo steps".to_string() - }), - }); - - let slider_y = y + row_h + 8.0; - let slider_h = ToolbarLayoutSpec::SIDE_DELAY_SLIDER_HEIGHT; - let slider_r = ToolbarLayoutSpec::SIDE_DELAY_SLIDER_KNOB_RADIUS; - let slider_w = self.card_w - ToolbarLayoutSpec::SIDE_CARD_INSET * 2.0; - set_color(self.ctx, COLOR_DELAY_TRACK); - draw_round_rect(self.ctx, self.x, slider_y, slider_w, slider_h, 3.0); - let _ = self.ctx.fill(); - let t = delay_t_from_ms(delay_ms); - let knob_x = ToolbarSliderSpec::DELAY_SECONDS.knob_center_x( - self.x, - slider_w, - slider_r, - delay_ms as f64 / 1000.0, - ); - set_color(self.ctx, COLOR_DELAY_KNOB); - self.ctx.arc( - knob_x, - slider_y + slider_h / 2.0, - slider_r, - 0.0, - std::f64::consts::PI * 2.0, - ); - let _ = self.ctx.fill(); - let hit_pad = ToolbarLayoutSpec::SIDE_DELAY_SLIDER_HIT_PADDING; - self.hits.push(HitRegion { - focus_id: None, - rect: ( - self.x, - slider_y - hit_pad, - slider_w, - slider_h + hit_pad * 2.0, - ), - event: if is_undo { - ToolbarEvent::SetCustomUndoDelay(delay_secs_from_t(t)) - } else { - ToolbarEvent::SetCustomRedoDelay(delay_secs_from_t(t)) - }, - kind: if is_undo { - HitKind::DragCustomUndoDelay - } else { - HitKind::DragCustomRedoDelay - }, - tooltip: Some(if is_undo { - format!("Undo step delay: {:.1}s (drag)", delay_ms as f64 / 1000.0) - } else { - format!("Redo step delay: {:.1}s (drag)", delay_ms as f64 / 1000.0) - }), - }); - - slider_y + slider_h + 10.0 - y - } -} diff --git a/src/backend/wayland/toolbar/render/side_palette/step/delay_sliders.rs b/src/backend/wayland/toolbar/render/side_palette/step/delay_sliders.rs deleted file mode 100644 index e10d74d5..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/step/delay_sliders.rs +++ /dev/null @@ -1,136 +0,0 @@ -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::ui::toolbar::model::{ToolbarSliderSpec, delay_secs_from_t, delay_t_from_ms}; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot}; -use crate::ui_text::{UiTextStyle, draw_text_baseline}; - -use super::super::super::widgets::constants::{ - FONT_FAMILY_DEFAULT, FONT_SIZE_SECONDARY, set_color, -}; -use super::super::super::widgets::draw_round_rect; -use super::{COLOR_DELAY_KNOB, COLOR_DELAY_TRACK}; -use crate::ui::theme::Rgba; - -/// Slider label text: COLOR_LABEL_HINT gray at +0.1 alpha — kept to avoid -/// dimming these labels. -const COLOR_SLIDER_LABEL: Rgba = (0.7, 0.7, 0.75, 0.9); - -pub(super) fn draw_delay_sliders( - ctx: &cairo::Context, - hits: &mut Vec, - x: f64, - slider_start_y: f64, - sliders_w: f64, - snapshot: &ToolbarSnapshot, -) { - let slider_h = ToolbarLayoutSpec::SIDE_DELAY_SLIDER_HEIGHT; - let slider_knob_r = ToolbarLayoutSpec::SIDE_DELAY_SLIDER_KNOB_RADIUS; - let label_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_SECONDARY, - }; - - let undo_label = format!( - "Undo delay: {:.1}s", - snapshot.undo_all_delay_ms as f64 / 1000.0 - ); - set_color(ctx, COLOR_SLIDER_LABEL); - draw_text_baseline( - ctx, - label_style, - &undo_label, - x, - slider_start_y + 10.0, - None, - ); - - let undo_slider_y = slider_start_y + ToolbarLayoutSpec::SIDE_DELAY_SLIDER_UNDO_OFFSET_Y; - set_color(ctx, COLOR_DELAY_TRACK); - draw_round_rect(ctx, x, undo_slider_y, sliders_w, slider_h, 3.0); - let _ = ctx.fill(); - let undo_t = delay_t_from_ms(snapshot.undo_all_delay_ms); - let undo_knob_x = ToolbarSliderSpec::DELAY_SECONDS.knob_center_x( - x, - sliders_w, - slider_knob_r, - snapshot.undo_all_delay_ms as f64 / 1000.0, - ); - set_color(ctx, COLOR_DELAY_KNOB); - ctx.arc( - undo_knob_x, - undo_slider_y + slider_h / 2.0, - slider_knob_r, - 0.0, - std::f64::consts::PI * 2.0, - ); - let _ = ctx.fill(); - let hit_pad = ToolbarLayoutSpec::SIDE_DELAY_SLIDER_HIT_PADDING; - hits.push(HitRegion { - focus_id: None, - rect: ( - x, - undo_slider_y - hit_pad, - sliders_w, - slider_h + hit_pad * 2.0, - ), - event: ToolbarEvent::SetUndoDelay(delay_secs_from_t(undo_t)), - kind: HitKind::DragUndoDelay, - tooltip: Some(format!( - "Undo-all delay: {:.1}s (drag)", - snapshot.undo_all_delay_ms as f64 / 1000.0 - )), - }); - - let redo_label = format!( - "Redo delay: {:.1}s", - snapshot.redo_all_delay_ms as f64 / 1000.0 - ); - set_color(ctx, COLOR_SLIDER_LABEL); - draw_text_baseline( - ctx, - label_style, - &redo_label, - x + sliders_w / 2.0 + 10.0, - slider_start_y + 10.0, - None, - ); - - let redo_slider_y = slider_start_y + ToolbarLayoutSpec::SIDE_DELAY_SLIDER_REDO_OFFSET_Y; - set_color(ctx, COLOR_DELAY_TRACK); - draw_round_rect(ctx, x, redo_slider_y, sliders_w, slider_h, 3.0); - let _ = ctx.fill(); - let redo_t = delay_t_from_ms(snapshot.redo_all_delay_ms); - let redo_knob_x = ToolbarSliderSpec::DELAY_SECONDS.knob_center_x( - x, - sliders_w, - slider_knob_r, - snapshot.redo_all_delay_ms as f64 / 1000.0, - ); - set_color(ctx, COLOR_DELAY_KNOB); - ctx.arc( - redo_knob_x, - redo_slider_y + slider_h / 2.0, - slider_knob_r, - 0.0, - std::f64::consts::PI * 2.0, - ); - let _ = ctx.fill(); - hits.push(HitRegion { - focus_id: None, - rect: ( - x, - redo_slider_y - hit_pad, - sliders_w, - slider_h + hit_pad * 2.0, - ), - event: ToolbarEvent::SetRedoDelay(delay_secs_from_t(redo_t)), - kind: HitKind::DragRedoDelay, - tooltip: Some(format!( - "Redo-all delay: {:.1}s (drag)", - snapshot.redo_all_delay_ms as f64 / 1000.0 - )), - }); -} diff --git a/src/backend/wayland/toolbar/render/side_palette/step/mod.rs b/src/backend/wayland/toolbar/render/side_palette/step/mod.rs deleted file mode 100644 index 216f14bb..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/step/mod.rs +++ /dev/null @@ -1,135 +0,0 @@ -mod custom_rows; -mod delay_sliders; - -use super::super::widgets::*; -use super::SidePaletteLayout; -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection}; -use crate::ui_text::UiTextStyle; - -use super::section_header::draw_collapsible_header; -use crate::ui::theme::Rgba; - -/// Delay-slider track fill, shared by the delay sliders and the custom -/// step rows. Darker than COLOR_TRACK_BACKGROUND — snapping would visibly -/// lighten these tracks. -const COLOR_DELAY_TRACK: Rgba = (0.4, 0.4, 0.45, 0.7); -/// Delay-slider knob: legacy pre-unification accent blue, kept to avoid a -/// visible hue shift. TODO(theme-consolidation): converge on -/// COLOR_TRACK_KNOB. -const COLOR_DELAY_KNOB: Rgba = (0.25, 0.5, 0.95, 0.9); - -pub(super) fn draw_step_section(layout: &mut SidePaletteLayout, y: &mut f64) { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let hover = layout.hover; - let x = layout.x; - let card_x = layout.card_x; - let card_w = layout.card_w; - let content_width = layout.content_width; - let section_gap = layout.section_gap; - let label_style = UiTextStyle { - family: super::super::widgets::constants::FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: super::super::widgets::constants::FONT_SIZE_LABEL, - }; - - if snapshot.side_section_hidden(ToolbarSideSection::StepUndo) - || !snapshot.show_step_section - || snapshot.active_side_pane != crate::ui::toolbar::SidePane::Canvas - { - return; - } - - let custom_toggle_h = ToolbarLayoutSpec::SIDE_TOGGLE_HEIGHT; - let toggle_gap = ToolbarLayoutSpec::SIDE_TOGGLE_GAP; - let toggles_h = custom_toggle_h * 2.0 + toggle_gap; - let custom_content_h = if snapshot.custom_section_enabled { - ToolbarLayoutSpec::SIDE_CUSTOM_SECTION_HEIGHT - } else { - 0.0 - }; - let custom_card_h = layout.spec.side_step_height(snapshot); - draw_group_card(ctx, card_x, *y, card_w, custom_card_h); - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::StepUndo, - "Step Undo/Redo", - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL, - ); - if snapshot.side_section_collapsed(ToolbarSideSection::StepUndo) { - *y += custom_card_h + section_gap; - return; - } - - let hits = &mut layout.hits; - let custom_toggle_y = *y + ToolbarLayoutSpec::SIDE_SECTION_TOGGLE_OFFSET_Y; - let toggle_w = content_width; - - let step_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, custom_toggle_y, toggle_w, custom_toggle_h)) - .unwrap_or(false); - draw_checkbox( - ctx, - x, - custom_toggle_y, - toggle_w, - custom_toggle_h, - snapshot.custom_section_enabled, - step_hover, - label_style, - "Step buttons", - ); - hits.push(HitRegion { - focus_id: None, - rect: (x, custom_toggle_y, toggle_w, custom_toggle_h), - event: ToolbarEvent::ToggleCustomSection(!snapshot.custom_section_enabled), - kind: HitKind::Click, - tooltip: Some("Step buttons: undo/redo several strokes at once.".to_string()), - }); - - let delay_toggle_y = custom_toggle_y + custom_toggle_h + toggle_gap; - let delay_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, delay_toggle_y, toggle_w, custom_toggle_h)) - .unwrap_or(false); - draw_checkbox( - ctx, - x, - delay_toggle_y, - toggle_w, - custom_toggle_h, - snapshot.show_delay_sliders, - delay_hover, - label_style, - "Delay sliders", - ); - hits.push(HitRegion { - focus_id: None, - rect: (x, delay_toggle_y, toggle_w, custom_toggle_h), - event: ToolbarEvent::ToggleDelaySliders(!snapshot.show_delay_sliders), - kind: HitKind::Click, - tooltip: Some("Delay sliders: undo/redo delays.".to_string()), - }); - - let custom_y = delay_toggle_y + custom_toggle_h + toggle_gap; - - if snapshot.custom_section_enabled { - custom_rows::draw_custom_rows(ctx, hits, x, custom_y, card_w, snapshot, hover); - } - - if snapshot.show_delay_sliders { - let slider_start_y = *y - + ToolbarLayoutSpec::SIDE_STEP_HEADER_HEIGHT - + toggles_h - + custom_content_h - + ToolbarLayoutSpec::SIDE_STEP_SLIDER_TOP_PADDING; - delay_sliders::draw_delay_sliders(ctx, hits, x, slider_start_y, content_width, snapshot); - } - - *y += custom_card_h + section_gap; -} diff --git a/src/backend/wayland/toolbar/render/side_palette/step_marker.rs b/src/backend/wayland/toolbar/render/side_palette/step_marker.rs deleted file mode 100644 index 0cba3af5..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/step_marker.rs +++ /dev/null @@ -1,90 +0,0 @@ -use super::super::widgets::constants::{ - COLOR_LABEL_HINT, FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, FONT_SIZE_SMALL, set_color, -}; -use super::super::widgets::*; -use super::SidePaletteLayout; -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::ui::toolbar::{ToolContext, ToolbarEvent, ToolbarSideSection}; -use crate::ui_text::{UiTextStyle, text_layout}; - -use super::section_header::draw_collapsible_header; - -pub(super) fn draw_step_marker_section(layout: &mut SidePaletteLayout, y: &mut f64) { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let hover = layout.hover; - let x = layout.x; - let card_x = layout.card_x; - let card_w = layout.card_w; - let content_width = layout.content_width; - let section_gap = layout.section_gap; - let label_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_LABEL, - }; - let hint_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Normal, - size: FONT_SIZE_SMALL, - }; - - if snapshot.side_section_hidden(ToolbarSideSection::StepMarkers) - || !ToolContext::from_snapshot(snapshot).show_step_counter - { - return; - } - - let card_h = layout.spec.side_step_markers_height(snapshot); - draw_group_card(ctx, card_x, *y, card_w, card_h); - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::StepMarkers, - "Step markers", - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL, - ); - let hint = format!("Next: {}", snapshot.step_marker_next); - let layout_text = text_layout(ctx, hint_style, &hint, None); - let ext = layout_text.ink_extents(); - let chevron_reserve = ToolbarLayoutSpec::SIDE_COLLAPSE_CHEVRON_SIZE + 10.0; - let hint_x = card_x + card_w - ext.width() - chevron_reserve - ext.x_bearing(); - let hint_y = *y + ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL; - set_color(ctx, COLOR_LABEL_HINT); - layout_text.show_at_baseline(ctx, hint_x, hint_y); - if snapshot.side_section_collapsed(ToolbarSideSection::StepMarkers) { - *y += card_h + section_gap; - return; - } - - let hits = &mut layout.hits; - let reset_y = *y + ToolbarLayoutSpec::SIDE_SECTION_TOGGLE_OFFSET_Y; - let reset_h = ToolbarLayoutSpec::SIDE_TOGGLE_HEIGHT; - let reset_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, reset_y, content_width, reset_h)) - .unwrap_or(false); - draw_button(ctx, x, reset_y, content_width, reset_h, false, reset_hover); - draw_label_center( - ctx, - label_style, - x, - reset_y, - content_width, - reset_h, - "Reset", - ); - hits.push(HitRegion { - focus_id: None, - rect: (x, reset_y, content_width, reset_h), - event: ToolbarEvent::ResetStepMarkerCounter, - kind: HitKind::Click, - tooltip: Some("Reset numbering to 1.".to_string()), - }); - - *y += card_h + section_gap; -} diff --git a/src/backend/wayland/toolbar/render/side_palette/text.rs b/src/backend/wayland/toolbar/render/side_palette/text.rs deleted file mode 100644 index 630676cd..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/text.rs +++ /dev/null @@ -1,228 +0,0 @@ -use super::super::widgets::constants::{ - COLOR_TRACK_BACKGROUND, COLOR_TRACK_KNOB, FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, SPACING_STD, - set_color, -}; -use super::super::widgets::*; -use super::SidePaletteLayout; -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::draw::FontDescriptor; -use crate::toolbar_icons; -use crate::ui::toolbar::model::ToolbarSliderSpec; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection}; -use crate::ui_text::UiTextStyle; - -use super::section_header::draw_collapsible_header; - -pub(super) fn draw_text_controls_section(layout: &mut SidePaletteLayout, y: &mut f64) { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let hover = layout.hover; - let x = layout.x; - let card_x = layout.card_x; - let card_w = layout.card_w; - let content_width = layout.content_width; - let section_gap = layout.section_gap; - let width = layout.width; - let label_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_LABEL, - }; - - let show_text_controls = - snapshot.text_active || snapshot.note_active || snapshot.show_text_controls; - if !show_text_controls { - return; - } - - let btn_size = ToolbarLayoutSpec::SIDE_NUDGE_SIZE; - let nudge_icon_size = ToolbarLayoutSpec::SIDE_NUDGE_ICON_SIZE; - let value_w = ToolbarLayoutSpec::SIDE_SLIDER_VALUE_WIDTH; - let track_h = ToolbarLayoutSpec::SIDE_TRACK_HEIGHT; - let knob_r = ToolbarLayoutSpec::SIDE_TRACK_KNOB_RADIUS; - - if !snapshot.side_section_hidden(ToolbarSideSection::TextSize) { - let slider_card_h = layout.spec.side_text_size_height(snapshot); - draw_group_card(ctx, card_x, *y, card_w, slider_card_h); - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::TextSize, - "Text size", - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_Y, - ); - if snapshot.side_section_collapsed(ToolbarSideSection::TextSize) { - *y += slider_card_h + section_gap; - } else { - let hits = &mut layout.hits; - - let font_size_spec = ToolbarSliderSpec::FONT_SIZE; - let fs_step = font_size_spec.step.unwrap_or(2.0); - let fs_slider_row_y = *y + ToolbarLayoutSpec::SIDE_SLIDER_ROW_OFFSET; - - let fs_minus_x = x; - let fs_minus_hover = hover - .map(|(hx, hy)| { - point_in_rect(hx, hy, fs_minus_x, fs_slider_row_y, btn_size, btn_size) - }) - .unwrap_or(false); - draw_button( - ctx, - fs_minus_x, - fs_slider_row_y, - btn_size, - btn_size, - false, - fs_minus_hover, - ); - set_icon_color(ctx, fs_minus_hover); - toolbar_icons::draw_icon_minus( - ctx, - fs_minus_x + (btn_size - nudge_icon_size) / 2.0, - fs_slider_row_y + (btn_size - nudge_icon_size) / 2.0, - nudge_icon_size, - ); - hits.push(HitRegion { - focus_id: None, - rect: (fs_minus_x, fs_slider_row_y, btn_size, btn_size), - event: ToolbarEvent::NudgeFontSize(-fs_step), - kind: HitKind::Click, - tooltip: Some("Decrease font size".to_string()), - }); - - let fs_plus_x = width - x - btn_size - value_w - 4.0; - let fs_plus_hover = hover - .map(|(hx, hy)| { - point_in_rect(hx, hy, fs_plus_x, fs_slider_row_y, btn_size, btn_size) - }) - .unwrap_or(false); - draw_button( - ctx, - fs_plus_x, - fs_slider_row_y, - btn_size, - btn_size, - false, - fs_plus_hover, - ); - set_icon_color(ctx, fs_plus_hover); - toolbar_icons::draw_icon_plus( - ctx, - fs_plus_x + (btn_size - nudge_icon_size) / 2.0, - fs_slider_row_y + (btn_size - nudge_icon_size) / 2.0, - nudge_icon_size, - ); - hits.push(HitRegion { - focus_id: None, - rect: (fs_plus_x, fs_slider_row_y, btn_size, btn_size), - event: ToolbarEvent::NudgeFontSize(fs_step), - kind: HitKind::Click, - tooltip: Some("Increase font size".to_string()), - }); - - let fs_track_x = fs_minus_x + btn_size + SPACING_STD; - let fs_track_w = fs_plus_x - fs_track_x - SPACING_STD; - let fs_track_y = fs_slider_row_y + (btn_size - track_h) / 2.0; - let fs_knob_x = - font_size_spec.knob_center_x(fs_track_x, fs_track_w, knob_r, snapshot.font_size); - - set_color(ctx, COLOR_TRACK_BACKGROUND); - draw_round_rect(ctx, fs_track_x, fs_track_y, fs_track_w, track_h, 4.0); - let _ = ctx.fill(); - set_color(ctx, COLOR_TRACK_KNOB); - ctx.arc( - fs_knob_x, - fs_track_y + track_h / 2.0, - knob_r, - 0.0, - std::f64::consts::PI * 2.0, - ); - let _ = ctx.fill(); - - hits.push(HitRegion { - focus_id: None, - rect: (fs_track_x, fs_track_y - 6.0, fs_track_w, track_h + 12.0), - event: ToolbarEvent::SetFontSize(snapshot.font_size), - kind: HitKind::DragSetFontSize, - tooltip: None, - }); - - let fs_text = format!("{:.0}pt", snapshot.font_size); - draw_label_center( - ctx, - label_style, - width - x - value_w, - fs_slider_row_y, - value_w, - btn_size, - &fs_text, - ); - - *y += slider_card_h + section_gap; - } - } - - if snapshot.side_section_hidden(ToolbarSideSection::Font) { - return; - } - - let font_card_h = layout.spec.side_font_height(snapshot); - draw_group_card(ctx, card_x, *y, card_w, font_card_h); - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::Font, - "Font", - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL, - ); - if snapshot.side_section_collapsed(ToolbarSideSection::Font) { - *y += font_card_h + section_gap; - return; - } - - let hits = &mut layout.hits; - let font_btn_h = ToolbarLayoutSpec::SIDE_FONT_BUTTON_HEIGHT; - let font_gap = ToolbarLayoutSpec::SIDE_FONT_BUTTON_GAP; - let font_btn_w = (content_width - font_gap) / 2.0; - let fonts = [ - FontDescriptor::new("Sans".to_string(), "bold".to_string(), "normal".to_string()), - FontDescriptor::new( - "Monospace".to_string(), - "normal".to_string(), - "normal".to_string(), - ), - ]; - let mut fx = x; - let fy = *y + ToolbarLayoutSpec::SIDE_SECTION_TOGGLE_OFFSET_Y; - for font in fonts { - let is_active = font.family == snapshot.font.family; - let font_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, fx, fy, font_btn_w, font_btn_h)) - .unwrap_or(false); - draw_button(ctx, fx, fy, font_btn_w, font_btn_h, is_active, font_hover); - draw_label_left( - ctx, - label_style, - fx + 8.0, - fy, - font_btn_w, - font_btn_h, - &font.family, - ); - hits.push(HitRegion { - focus_id: None, - rect: (fx, fy, font_btn_w, font_btn_h), - event: ToolbarEvent::SetFont(font.clone()), - kind: HitKind::Click, - tooltip: None, - }); - fx += font_btn_w + font_gap; - } - - *y += font_card_h + section_gap; -} diff --git a/src/backend/wayland/toolbar/render/side_palette/thickness.rs b/src/backend/wayland/toolbar/render/side_palette/thickness.rs deleted file mode 100644 index 14bb87b0..00000000 --- a/src/backend/wayland/toolbar/render/side_palette/thickness.rs +++ /dev/null @@ -1,338 +0,0 @@ -use super::super::widgets::constants::{ - COLOR_TRACK_BACKGROUND, COLOR_TRACK_KNOB, FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, SPACING_STD, - set_color, -}; -use super::super::widgets::*; -use super::SidePaletteLayout; -use crate::backend::wayland::toolbar::events::HitKind; -use crate::backend::wayland::toolbar::format_binding_label; -use crate::backend::wayland::toolbar::hit::HitRegion; -use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::config::Action; -use crate::input::EraserMode; -use crate::toolbar_icons; -use crate::ui::toolbar::model::ToolbarSliderSpec; -use crate::ui::toolbar::{ToolContext, ToolbarEvent, ToolbarSideSection}; -use crate::ui_text::UiTextStyle; - -use super::section_header::draw_collapsible_header; - -pub(super) fn draw_thickness_section(layout: &mut SidePaletteLayout, y: &mut f64) { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let hover = layout.hover; - let x = layout.x; - let card_x = layout.card_x; - let card_w = layout.card_w; - let content_width = layout.content_width; - let section_gap = layout.section_gap; - let width = layout.width; - let label_style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_LABEL, - }; - let tool_context = ToolContext::from_snapshot(snapshot); - - if !snapshot.side_section_hidden(ToolbarSideSection::Thickness) { - let slider_card_h = layout.spec.side_thickness_height(snapshot); - draw_group_card(ctx, card_x, *y, card_w, slider_card_h); - let thickness_label = tool_context.thickness_label; - // Generic titles gain the tool scope; specific ones ("Eraser size") - // already name their target. - let title = if thickness_label == "Thickness" { - super::section_header::scoped_title(thickness_label, snapshot) - } else { - thickness_label.to_string() - }; - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::Thickness, - &title, - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_Y, - ); - if snapshot.side_section_collapsed(ToolbarSideSection::Thickness) { - *y += slider_card_h + section_gap; - } else { - let hits = &mut layout.hits; - - let btn_size = ToolbarLayoutSpec::SIDE_NUDGE_SIZE; - let nudge_icon_size = ToolbarLayoutSpec::SIDE_NUDGE_ICON_SIZE; - let value_w = ToolbarLayoutSpec::SIDE_SLIDER_VALUE_WIDTH; - let thickness_slider_row_y = *y + ToolbarLayoutSpec::SIDE_SLIDER_ROW_OFFSET; - let track_h = ToolbarLayoutSpec::SIDE_TRACK_HEIGHT; - let knob_r = ToolbarLayoutSpec::SIDE_TRACK_KNOB_RADIUS; - let thickness_spec = ToolbarSliderSpec::THICKNESS; - let nudge_step = thickness_spec.step.unwrap_or(1.0); - - let minus_x = x; - let minus_hover = hover - .map(|(hx, hy)| { - point_in_rect(hx, hy, minus_x, thickness_slider_row_y, btn_size, btn_size) - }) - .unwrap_or(false); - draw_button( - ctx, - minus_x, - thickness_slider_row_y, - btn_size, - btn_size, - false, - minus_hover, - ); - set_icon_color(ctx, minus_hover); - toolbar_icons::draw_icon_minus( - ctx, - minus_x + (btn_size - nudge_icon_size) / 2.0, - thickness_slider_row_y + (btn_size - nudge_icon_size) / 2.0, - nudge_icon_size, - ); - hits.push(HitRegion { - focus_id: None, - rect: (minus_x, thickness_slider_row_y, btn_size, btn_size), - event: ToolbarEvent::NudgeThickness(-nudge_step), - kind: HitKind::Click, - tooltip: Some("Decrease thickness".to_string()), - }); - - let plus_x = width - x - btn_size - value_w - 4.0; - let plus_hover = hover - .map(|(hx, hy)| { - point_in_rect(hx, hy, plus_x, thickness_slider_row_y, btn_size, btn_size) - }) - .unwrap_or(false); - draw_button( - ctx, - plus_x, - thickness_slider_row_y, - btn_size, - btn_size, - false, - plus_hover, - ); - set_icon_color(ctx, plus_hover); - toolbar_icons::draw_icon_plus( - ctx, - plus_x + (btn_size - nudge_icon_size) / 2.0, - thickness_slider_row_y + (btn_size - nudge_icon_size) / 2.0, - nudge_icon_size, - ); - hits.push(HitRegion { - focus_id: None, - rect: (plus_x, thickness_slider_row_y, btn_size, btn_size), - event: ToolbarEvent::NudgeThickness(nudge_step), - kind: HitKind::Click, - tooltip: Some("Increase thickness".to_string()), - }); - - let track_x = minus_x + btn_size + SPACING_STD; - let track_w = plus_x - track_x - SPACING_STD; - let thickness_track_y = thickness_slider_row_y + (btn_size - track_h) / 2.0; - let knob_x = thickness_spec.knob_center_x(track_x, track_w, knob_r, snapshot.thickness); - - set_color(ctx, COLOR_TRACK_BACKGROUND); - draw_round_rect(ctx, track_x, thickness_track_y, track_w, track_h, 4.0); - let _ = ctx.fill(); - set_color(ctx, COLOR_TRACK_KNOB); - ctx.arc( - knob_x, - thickness_track_y + track_h / 2.0, - knob_r, - 0.0, - std::f64::consts::PI * 2.0, - ); - let _ = ctx.fill(); - - hits.push(HitRegion { - focus_id: None, - rect: (track_x, thickness_track_y - 6.0, track_w, track_h + 12.0), - event: ToolbarEvent::SetThickness(snapshot.thickness), - kind: HitKind::DragSetThickness { - min: thickness_spec.min, - max: thickness_spec.max, - }, - tooltip: None, - }); - - let thickness_text = format!("{:.0}px", snapshot.thickness); - let value_x = width - x - value_w; - draw_label_center( - ctx, - label_style, - value_x, - thickness_slider_row_y, - value_w, - btn_size, - &thickness_text, - ); - *y += slider_card_h + section_gap; - } - } - - if tool_context.show_eraser_mode - && !snapshot.side_section_hidden(ToolbarSideSection::EraserMode) - { - let eraser_card_h = layout.spec.side_eraser_mode_height(snapshot); - let toggle_h = ToolbarLayoutSpec::SIDE_TOGGLE_HEIGHT; - let toggle_w = content_width; - draw_group_card(ctx, card_x, *y, card_w, eraser_card_h); - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::EraserMode, - "Eraser mode", - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL, - ); - if snapshot.side_section_collapsed(ToolbarSideSection::EraserMode) { - *y += eraser_card_h + section_gap; - } else { - let hits = &mut layout.hits; - - let toggle_y = *y + ToolbarLayoutSpec::SIDE_SECTION_TOGGLE_OFFSET_Y; - let toggle_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, toggle_y, toggle_w, toggle_h)) - .unwrap_or(false); - let stroke_active = snapshot.eraser_mode == EraserMode::Stroke; - draw_checkbox( - ctx, - x, - toggle_y, - toggle_w, - toggle_h, - stroke_active, - toggle_hover, - label_style, - "Erase by stroke", - ); - let toggle_tooltip = format_binding_label( - "Erase by stroke", - snapshot - .binding_hints - .binding_for_action(Action::ToggleEraserMode), - ); - hits.push(HitRegion { - focus_id: None, - rect: (x, toggle_y, toggle_w, toggle_h), - event: ToolbarEvent::SetEraserMode(if stroke_active { - EraserMode::Brush - } else { - EraserMode::Stroke - }), - kind: HitKind::Click, - tooltip: Some(toggle_tooltip), - }); - *y += eraser_card_h + section_gap; - } - } - - if tool_context.show_polygon_sides_control - && !snapshot.side_section_hidden(ToolbarSideSection::PolygonSides) - { - draw_polygon_sides_section(layout, y, label_style); - } -} - -fn draw_polygon_sides_section( - layout: &mut SidePaletteLayout, - y: &mut f64, - label_style: UiTextStyle<'static>, -) { - let ctx = layout.ctx; - let snapshot = layout.snapshot; - let hover = layout.hover; - let x = layout.x; - let card_x = layout.card_x; - let card_w = layout.card_w; - let content_width = layout.content_width; - let section_gap = layout.section_gap; - let width = layout.width; - let card_h = layout.spec.side_polygon_sides_height(snapshot); - let btn_size = ToolbarLayoutSpec::SIDE_NUDGE_SIZE; - let icon_size = ToolbarLayoutSpec::SIDE_NUDGE_ICON_SIZE; - let value_w = ToolbarLayoutSpec::SIDE_SLIDER_VALUE_WIDTH; - let row_y = *y + ToolbarLayoutSpec::SIDE_SLIDER_ROW_OFFSET; - - draw_group_card(ctx, card_x, *y, card_w, card_h); - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::PolygonSides, - "Sides", - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_Y, - ); - if snapshot.side_section_collapsed(ToolbarSideSection::PolygonSides) { - *y += card_h + section_gap; - return; - } - - let minus_x = x; - let minus_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, minus_x, row_y, btn_size, btn_size)) - .unwrap_or(false); - draw_button(ctx, minus_x, row_y, btn_size, btn_size, false, minus_hover); - set_icon_color(ctx, minus_hover); - toolbar_icons::draw_icon_minus( - ctx, - minus_x + (btn_size - icon_size) / 2.0, - row_y + (btn_size - icon_size) / 2.0, - icon_size, - ); - layout.hits.push(HitRegion { - focus_id: None, - rect: (minus_x, row_y, btn_size, btn_size), - event: ToolbarEvent::NudgePolygonSides(-1), - kind: HitKind::Click, - tooltip: Some("Decrease polygon sides".to_string()), - }); - - let plus_x = width - x - btn_size - value_w - 4.0; - let plus_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, plus_x, row_y, btn_size, btn_size)) - .unwrap_or(false); - draw_button(ctx, plus_x, row_y, btn_size, btn_size, false, plus_hover); - set_icon_color(ctx, plus_hover); - toolbar_icons::draw_icon_plus( - ctx, - plus_x + (btn_size - icon_size) / 2.0, - row_y + (btn_size - icon_size) / 2.0, - icon_size, - ); - layout.hits.push(HitRegion { - focus_id: None, - rect: (plus_x, row_y, btn_size, btn_size), - event: ToolbarEvent::NudgePolygonSides(1), - kind: HitKind::Click, - tooltip: Some("Increase polygon sides".to_string()), - }); - - let value_x = width - x - value_w; - draw_label_center( - ctx, - label_style, - value_x, - row_y, - value_w, - btn_size, - &snapshot.polygon_sides.to_string(), - ); - - layout.hits.push(HitRegion { - focus_id: None, - rect: ( - minus_x + btn_size, - row_y, - content_width - btn_size * 2.0, - btn_size, - ), - event: ToolbarEvent::SetPolygonSides(snapshot.polygon_sides), - kind: HitKind::Click, - tooltip: None, - }); - - *y += card_h + section_gap; -} diff --git a/src/backend/wayland/toolbar/render/widgets/background.rs b/src/backend/wayland/toolbar/render/widgets/background.rs index fa393d63..f6a9a76c 100644 --- a/src/backend/wayland/toolbar/render/widgets/background.rs +++ b/src/backend/wayland/toolbar/render/widgets/background.rs @@ -1,7 +1,4 @@ -use super::constants::{ - COLOR_CARD_BACKGROUND, COLOR_PANEL_BACKGROUND, COLOR_PANEL_BORDER, RADIUS_CARD, RADIUS_PANEL, - set_color, -}; +use super::constants::{COLOR_PANEL_BACKGROUND, COLOR_PANEL_BORDER, RADIUS_PANEL, set_color}; use super::draw_round_rect; use crate::ui::theme::Rgba; /// Two-layer popover drop shadow approximating the GTK popover's @@ -36,18 +33,6 @@ pub(in crate::backend::wayland::toolbar::render) fn draw_panel_background( let _ = ctx.stroke(); } -pub(in crate::backend::wayland::toolbar::render) fn draw_group_card( - ctx: &cairo::Context, - x: f64, - y: f64, - w: f64, - h: f64, -) { - set_color(ctx, COLOR_CARD_BACKGROUND); - draw_round_rect(ctx, x, y, w, h, RADIUS_CARD); - let _ = ctx.fill(); -} - /// Anchored popover panel: opaque background with a border and a caret /// pointing at the anchor. `caret_x` comes from the popover placement math /// (`view::popover`); `caret_up` is true when the popover opened below its diff --git a/src/backend/wayland/toolbar/render/widgets/buttons.rs b/src/backend/wayland/toolbar/render/widgets/buttons.rs index caab507e..0b95412b 100644 --- a/src/backend/wayland/toolbar/render/widgets/buttons.rs +++ b/src/backend/wayland/toolbar/render/widgets/buttons.rs @@ -83,24 +83,6 @@ pub(in crate::backend::wayland::toolbar::render) fn draw_minimize_button( ); } -/// Minimize the vertical side palette into its edge restore tab. -pub(in crate::backend::wayland::toolbar::render) fn draw_side_minimize_button( - ctx: &cairo::Context, - x: f64, - y: f64, - size: f64, - hover: bool, -) { - draw_collapse_button( - ctx, - x, - y, - size, - hover, - crate::toolbar_icons::draw_icon_side_minimize, - ); -} - fn draw_collapse_button( ctx: &cairo::Context, x: f64, diff --git a/src/backend/wayland/toolbar/render/widgets/color.rs b/src/backend/wayland/toolbar/render/widgets/color.rs deleted file mode 100644 index b37c17d0..00000000 --- a/src/backend/wayland/toolbar/render/widgets/color.rs +++ /dev/null @@ -1,189 +0,0 @@ -use super::constants::{ - COLOR_ACCENT, COLOR_SWATCH_HAIRLINE, COLOR_SWATCH_HAIRLINE_DARK, set_color, -}; -use super::draw_round_rect; -use crate::draw::Color; -use crate::ui::theme::{Rgba, SHADOW_RGBA}; -/// Outline around the gradient picker areas (sat/val square, hue bar). -const COLOR_PICKER_OUTLINE: Rgba = (1.0, 1.0, 1.0, 0.4); -/// Outer white ring of the picker's position indicator dot. -const COLOR_INDICATOR_RING: Rgba = (1.0, 1.0, 1.0, 0.9); - -/// Rounded-square quick-color swatch: the fill sits one pixel inside the -/// hit rect, a subtle inner hairline keeps every fill defined against the -/// panel (boosted for dark colors), and the active state draws a 2px accent -/// ring with a ~2px gap around the fill. -/// -/// A translucent color is painted at its own alpha over the checkerboard, so -/// the swatch shows the transparency the canvas will draw with instead of -/// presenting it as an opaque color. -pub(in crate::backend::wayland::toolbar::render) fn draw_swatch( - ctx: &cairo::Context, - x: f64, - y: f64, - size: f64, - color: Color, - active: bool, -) { - let fill_path = - |ctx: &cairo::Context| draw_round_rect(ctx, x + 1.0, y + 1.0, size - 2.0, size - 2.0, 5.0); - crate::ui::checkerboard_behind(ctx, color.a, fill_path); - ctx.set_source_rgba(color.r, color.g, color.b, color.a); - fill_path(ctx); - let _ = ctx.fill(); - - let luminance = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b; - if luminance < 0.3 { - set_color(ctx, COLOR_SWATCH_HAIRLINE_DARK); - } else { - set_color(ctx, COLOR_SWATCH_HAIRLINE); - } - ctx.set_line_width(1.0); - draw_round_rect(ctx, x + 1.5, y + 1.5, size - 3.0, size - 3.0, 4.5); - let _ = ctx.stroke(); - - if active { - set_color(ctx, COLOR_ACCENT); - ctx.set_line_width(2.0); - draw_round_rect(ctx, x - 2.0, y - 2.0, size + 4.0, size + 4.0, 7.0); - let _ = ctx.stroke(); - } -} - -fn set_hue_gradient(ctx: &cairo::Context, x: f64, y: f64, w: f64) { - let hue_grad = cairo::LinearGradient::new(x, y, x + w, y); - hue_grad.add_color_stop_rgba(0.0, 1.0, 0.0, 0.0, 1.0); - hue_grad.add_color_stop_rgba(0.17, 1.0, 1.0, 0.0, 1.0); - hue_grad.add_color_stop_rgba(0.33, 0.0, 1.0, 0.0, 1.0); - hue_grad.add_color_stop_rgba(0.5, 0.0, 1.0, 1.0, 1.0); - hue_grad.add_color_stop_rgba(0.66, 0.0, 0.0, 1.0, 1.0); - hue_grad.add_color_stop_rgba(0.83, 1.0, 0.0, 1.0, 1.0); - hue_grad.add_color_stop_rgba(1.0, 1.0, 0.0, 0.0, 1.0); - let _ = ctx.set_source(&hue_grad); -} - -/// Draw the 2-D saturation/value area for a fixed hue: white→hue across x, -/// transparent→black down y. -pub(in crate::backend::wayland::toolbar::render) fn draw_sat_val_area( - ctx: &cairo::Context, - x: f64, - y: f64, - w: f64, - h: f64, - hue: f64, -) { - let hue_color = crate::draw::color::hsv_to_rgb(hue, 1.0, 1.0); - - let sat_grad = cairo::LinearGradient::new(x, y, x + w, y); - sat_grad.add_color_stop_rgba(0.0, 1.0, 1.0, 1.0, 1.0); - sat_grad.add_color_stop_rgba(1.0, hue_color.r, hue_color.g, hue_color.b, 1.0); - ctx.rectangle(x, y, w, h); - let _ = ctx.set_source(&sat_grad); - let _ = ctx.fill(); - - let val_grad = cairo::LinearGradient::new(x, y, x, y + h); - val_grad.add_color_stop_rgba(0.0, 0.0, 0.0, 0.0, 0.0); - val_grad.add_color_stop_rgba(1.0, 0.0, 0.0, 0.0, 1.0); - ctx.rectangle(x, y, w, h); - let _ = ctx.set_source(&val_grad); - let _ = ctx.fill(); - - set_color(ctx, COLOR_PICKER_OUTLINE); - ctx.rectangle(x + 0.5, y + 0.5, w - 1.0, h - 1.0); - ctx.set_line_width(1.0); - let _ = ctx.stroke(); -} - -/// Draw the horizontal hue bar. -pub(in crate::backend::wayland::toolbar::render) fn draw_hue_bar( - ctx: &cairo::Context, - x: f64, - y: f64, - w: f64, - h: f64, -) { - ctx.rectangle(x, y, w, h); - set_hue_gradient(ctx, x, y, w); - let _ = ctx.fill(); - - set_color(ctx, COLOR_PICKER_OUTLINE); - ctx.rectangle(x + 0.5, y + 0.5, w - 1.0, h - 1.0); - ctx.set_line_width(1.0); - let _ = ctx.stroke(); -} - -/// Draw a color indicator dot on the gradient picker. Painted opaque on -/// purpose: it marks a position on the gradient, not the color's alpha. -pub(in crate::backend::wayland::toolbar::render) fn draw_color_indicator( - ctx: &cairo::Context, - x: f64, - y: f64, - color: Color, -) { - let radius = 5.0; - - // Draw outer white ring - set_color(ctx, COLOR_INDICATOR_RING); - ctx.arc(x, y, radius + 1.5, 0.0, std::f64::consts::PI * 2.0); - let _ = ctx.fill(); - - // Draw inner color circle - ctx.set_source_rgba(color.r, color.g, color.b, 1.0); - ctx.arc(x, y, radius, 0.0, std::f64::consts::PI * 2.0); - let _ = ctx.fill(); - - // Draw dark outline for visibility - set_color(ctx, SHADOW_RGBA); - ctx.set_line_width(1.0); - ctx.arc(x, y, radius + 1.5, 0.0, std::f64::consts::PI * 2.0); - let _ = ctx.stroke(); -} - -#[cfg(test)] -mod tests { - use super::*; - use cairo::{Context, Format, ImageSurface}; - - /// `(r, g, b)` of one pixel, 0-255. Rgb24 packs pixels as native-endian - /// 32-bit words, so on little-endian the byte order is B, G, R, unused. - fn pixel_at(surface: &mut ImageSurface, x: i32, y: i32) -> (u8, u8, u8) { - let stride = surface.stride() as usize; - let offset = y as usize * stride + x as usize * 4; - let data = surface.data().expect("pixel data"); - (data[offset + 2], data[offset + 1], data[offset]) - } - - /// Paint one swatch of `color` on white and sample its middle. - fn swatch_center(color: Color) -> (u8, u8, u8) { - let size = 24.0; - let surface = ImageSurface::create(Format::Rgb24, 32, 32).expect("surface"); - { - let ctx = Context::new(&surface).expect("context"); - ctx.set_source_rgb(1.0, 1.0, 1.0); - let _ = ctx.paint(); - draw_swatch(&ctx, 4.0, 4.0, size, color, false); - } - let mut surface = surface; - pixel_at(&mut surface, 4 + size as i32 / 2, 4 + size as i32 / 2) - } - - #[test] - fn a_translucent_swatch_shows_its_transparency() { - let red = Color { - r: 1.0, - g: 0.0, - b: 0.0, - a: 1.0, - }; - assert_eq!(swatch_center(red), (255, 0, 0), "opaque fills stay exact"); - - // Half-alpha red over the checkerboard lets the gray through, so the - // swatch cannot be mistaken for the opaque color. - let (r, g, b) = swatch_center(Color { a: 0.5, ..red }); - assert!(r < 255, "translucent swatch painted at full red: {r}"); - assert!( - g > 0 && b > 0, - "checkerboard did not show through: ({r}, {g}, {b})" - ); - } -} diff --git a/src/backend/wayland/toolbar/render/widgets/labels.rs b/src/backend/wayland/toolbar/render/widgets/labels.rs index 71f1ab9c..0e8c5ada 100644 --- a/src/backend/wayland/toolbar/render/widgets/labels.rs +++ b/src/backend/wayland/toolbar/render/widgets/labels.rs @@ -1,5 +1,5 @@ -use super::constants::{COLOR_LABEL_SECTION, COLOR_TEXT_PRIMARY, set_color}; -use crate::ui_text::{UiTextStyle, draw_text_baseline, text_layout}; +use super::constants::{COLOR_TEXT_PRIMARY, set_color}; +use crate::ui_text::{UiTextStyle, text_layout}; pub(in crate::backend::wayland::toolbar::render) fn draw_label_center( ctx: &cairo::Context, @@ -97,14 +97,3 @@ pub(in crate::backend::wayland::toolbar::render) fn draw_label_left_wrapped( set_color(ctx, COLOR_TEXT_PRIMARY); layout.show_at_baseline(ctx, x, ty); } - -pub(in crate::backend::wayland::toolbar::render) fn draw_section_label( - ctx: &cairo::Context, - style: UiTextStyle<'_>, - x: f64, - y: f64, - text: &str, -) { - set_color(ctx, COLOR_LABEL_SECTION); - draw_text_baseline(ctx, style, text, x, y, None); -} diff --git a/src/backend/wayland/toolbar/render/widgets/mod.rs b/src/backend/wayland/toolbar/render/widgets/mod.rs index 2411dcb2..945f0e88 100644 --- a/src/backend/wayland/toolbar/render/widgets/mod.rs +++ b/src/backend/wayland/toolbar/render/widgets/mod.rs @@ -1,24 +1,22 @@ mod background; mod buttons; mod checkbox; -mod color; pub mod constants; mod icons; mod labels; mod primitives; mod tooltip; -pub(super) use background::{draw_group_card, draw_panel_background, draw_popover_panel}; +pub(super) use background::{draw_panel_background, draw_popover_panel}; pub(super) use buttons::{ draw_button, draw_destructive_button, draw_disabled_button, draw_drag_handle, - draw_minimize_button, draw_pin_button, draw_segmented_control, draw_side_minimize_button, + draw_minimize_button, draw_pin_button, draw_segmented_control, }; pub(super) use checkbox::{draw_checkbox, draw_mini_checkbox}; -pub(super) use color::{draw_color_indicator, draw_hue_bar, draw_sat_val_area, draw_swatch}; pub(super) use icons::set_icon_color; pub(super) use labels::{ draw_label_center, draw_label_center_color, draw_label_left, draw_label_left_wrapped, - draw_section_label, ellipsize_to_width, + ellipsize_to_width, }; pub(super) use primitives::{draw_divider_vertical, draw_round_rect, point_in_rect}; pub(super) use tooltip::draw_tooltip_with_delay; diff --git a/src/backend/wayland/toolbar/rows.rs b/src/backend/wayland/toolbar/rows.rs index 708d3882..d8d2765b 100644 --- a/src/backend/wayland/toolbar/rows.rs +++ b/src/backend/wayland/toolbar/rows.rs @@ -9,7 +9,6 @@ pub(in crate::backend::wayland::toolbar) struct GridItem { #[derive(Debug, Clone)] pub(in crate::backend::wayland::toolbar) struct GridLayout { pub(in crate::backend::wayland::toolbar) items: Vec, - pub(in crate::backend::wayland::toolbar) rows: usize, pub(in crate::backend::wayland::toolbar) height: f64, } @@ -24,13 +23,6 @@ pub(in crate::backend::wayland::toolbar) fn row_item_width( (content_width - gap * (columns as f64 - 1.0)) / columns as f64 } -pub(in crate::backend::wayland::toolbar) fn capped_grid_columns( - item_count: usize, - max_columns: usize, -) -> usize { - item_count.clamp(1, max_columns.max(1)) -} - #[allow(clippy::too_many_arguments)] pub(in crate::backend::wayland::toolbar) fn grid_layout( start_x: f64, @@ -45,7 +37,6 @@ pub(in crate::backend::wayland::toolbar) fn grid_layout( if columns == 0 || total_items == 0 { return GridLayout { items: Vec::new(), - rows: 0, height: 0.0, }; } @@ -69,59 +60,7 @@ pub(in crate::backend::wayland::toolbar) fn grid_layout( 0.0 }; - GridLayout { - items, - rows, - height, - } -} - -pub(in crate::backend::wayland::toolbar) fn centered_grid_layout( - start_x: f64, - content_width: f64, - start_y: f64, - item_size: f64, - gap: f64, - columns: usize, - total_items: usize, -) -> GridLayout { - if columns == 0 || total_items == 0 { - return GridLayout { - items: Vec::new(), - rows: 0, - height: 0.0, - }; - } - - let rows = total_items.div_ceil(columns); - let mut items = Vec::with_capacity(total_items); - for index in 0..total_items { - let row = index / columns; - let col = index % columns; - let row_start = row * columns; - let row_end = (row_start + columns).min(total_items); - let items_in_row = row_end - row_start; - let row_width = items_in_row as f64 * item_size + gap * (items_in_row as f64 - 1.0); - let row_x = start_x + (content_width - row_width) / 2.0; - items.push(GridItem { - x: row_x + (item_size + gap) * col as f64, - y: start_y + (item_size + gap) * row as f64, - w: item_size, - h: item_size, - }); - } - - let height = if rows > 0 { - item_size * rows as f64 + gap * (rows as f64 - 1.0) - } else { - 0.0 - }; - - GridLayout { - items, - rows, - height, - } + GridLayout { items, height } } #[cfg(test)] @@ -132,25 +71,7 @@ mod tests { fn grid_layout_reports_rows_and_height() { let layout = grid_layout(10.0, 5.0, 20.0, 10.0, 2.0, 3.0, 3, 7); - assert_eq!(layout.rows, 3); assert_eq!(layout.items.len(), 7); assert!((layout.height - 36.0).abs() < 1e-6); } - - #[test] - fn capped_grid_columns_stays_between_one_and_limit() { - assert_eq!(capped_grid_columns(0, 5), 1); - assert_eq!(capped_grid_columns(1, 5), 1); - assert_eq!(capped_grid_columns(5, 5), 5); - assert_eq!(capped_grid_columns(6, 5), 5); - } - - #[test] - fn centered_grid_layout_reports_rows_and_height() { - let layout = centered_grid_layout(0.0, 100.0, 5.0, 10.0, 2.0, 4, 6); - - assert_eq!(layout.rows, 2); - assert_eq!(layout.items.len(), 6); - assert!((layout.height - 22.0).abs() < 1e-6); - } } diff --git a/src/backend/wayland/toolbar/view/node.rs b/src/backend/wayland/toolbar/view/node.rs index 72937b30..fa943f07 100644 --- a/src/backend/wayland/toolbar/view/node.rs +++ b/src/backend/wayland/toolbar/view/node.rs @@ -8,18 +8,13 @@ use crate::ui::toolbar::ToolbarEvent; /// Stable identity of a widget across rebuilds of the tree. /// -/// Ids are dotted paths (e.g. `top.tool.pen`, `side.draw.colors.swatch.3`) +/// Ids are dotted paths (e.g. `top.tool.pen`, `top.menu.settings.toggle.0`) /// so keyboard focus survives a rebuild even when node indices shift, and /// golden dumps stay readable. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct WidgetId(Cow<'static, str>); impl WidgetId { - #[allow(dead_code)] // Reserved for the side-palette tree port; top ids use From directly. - pub fn new(id: impl Into>) -> Self { - Self(id.into()) - } - pub fn as_str(&self) -> &str { &self.0 } @@ -138,9 +133,6 @@ impl ButtonStyle { pub enum WidgetKind { /// Full-surface panel background. Panel, - /// Rounded group-card background. - #[allow(dead_code)] // Used by the staged side-palette tree port. - Card, /// Thin separator line. Divider { vertical: bool }, /// Drag grip for moving a toolbar. @@ -156,9 +148,6 @@ pub enum WidgetKind { label: LabelSpec, style: ButtonStyle, }, - /// Standalone icon glyph (no button body). - #[allow(dead_code)] // Used by the staged side-palette tree port. - Icon { glyph: IconFn }, /// Standalone text. Label(LabelSpec), /// Small labelled checkbox. @@ -186,8 +175,8 @@ pub enum WidgetKind { /// A presets-island slot. Filled slots draw the saved tool glyph in the /// neutral foreground and carry the preset `color` as a separate corner /// swatch, so a dark preset color never renders the glyph invisible - /// against the slot body (the side-palette convention). Empty slots - /// (`glyph` is `None`) draw the 1-based slot number carried in `label`. + /// against the slot body. Empty slots (`glyph` is `None`) draw the 1-based + /// slot number carried in `label`. /// `active` marks the slot whose preset is currently applied. PresetSlot { glyph: Option, diff --git a/src/backend/wayland/toolbar/view/top.rs b/src/backend/wayland/toolbar/view/top.rs index f2021d27..6b6e31d9 100644 --- a/src/backend/wayland/toolbar/view/top.rs +++ b/src/backend/wayland/toolbar/view/top.rs @@ -301,8 +301,8 @@ pub fn top_extra_height(snapshot: &ToolbarSnapshot) -> f64 { /// Scroll bounds for the open Canvas/Session/Settings popover as /// (natural_height, viewport_height), both in pre-scale spec units; `None` -/// while no menu popover is open. The wheel path scrolls against these the -/// way `side_scroll_bounds` served the retired side palette. +/// while no menu popover is open. The wheel path and scrollbar drag use these +/// same bounds. pub fn top_popover_scroll_bounds(snapshot: &ToolbarSnapshot) -> Option<(f64, f64)> { let plan = plan_top_strip(snapshot); menus::menu_scroll_bounds_planned(snapshot, &plan) diff --git a/src/backend/wayland/toolbar/view/top/build.rs b/src/backend/wayland/toolbar/view/top/build.rs index b9b0e7c3..9510bcc8 100644 --- a/src/backend/wayland/toolbar/view/top/build.rs +++ b/src/backend/wayland/toolbar/view/top/build.rs @@ -388,7 +388,7 @@ pub(super) fn build_top_view_planned( } } - // --- Canvas/Session/Settings popovers: the re-hosted side panes ---------- + // --- Canvas/Session/Settings popovers ----------------------------------- if let Some(anchor) = overflow_anchor { // Match GTK's native popovers: these large panels attach directly to // the overflow button and cover the detached style pill beneath it. diff --git a/src/backend/wayland/toolbar/view/top/menus.rs b/src/backend/wayland/toolbar/view/top/menus.rs index cebb9e73..d8b70d04 100644 --- a/src/backend/wayland/toolbar/view/top/menus.rs +++ b/src/backend/wayland/toolbar/view/top/menus.rs @@ -1,22 +1,19 @@ //! Canvas/Session/Settings popovers anchored to the top strip's overflow toggle. //! -//! With `[ui.toolbar] side_layout = "pill"` the side palette is retired, so -//! the Canvas, Session, and Settings panes are re-hosted here as popovers -//! opened from the matching overflow-menu entries. Their content reuses the -//! renderer-neutral models and events the side panes render, minus the side -//! palette's chrome (pane tabs, collapsible section headers). +//! Canvas, Session, and Settings live here as popovers opened from the +//! matching overflow-menu entries. Their content reuses the renderer-neutral +//! models and events shared with the GTK top bar. //! //! Content taller than [`MENU_MAX_CONTENT_H`] scrolls internally behind a -//! proportional scrollbar (the side palette's scroll pattern): the tree -//! painter has no clip, so rows are either fully inside the viewport or -//! withheld entirely (paint and hits alike). +//! proportional scrollbar. The tree painter has no clip, so rows are either +//! fully inside the viewport or withheld entirely (paint and hits alike). use crate::backend::wayland::toolbar::events::HitKind; use crate::backend::wayland::toolbar::format_binding_label; -use crate::backend::wayland::toolbar::render::side_palette::session::{ +use crate::backend::wayland::toolbar::rows::{grid_layout, row_item_width}; +use crate::ui::toolbar::session_format::{ strip_session_extension, truncate_middle, truncate_start, }; -use crate::backend::wayland::toolbar::rows::{grid_layout, row_item_width}; use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot, model}; use crate::ui_text::{UiTextStyle, measure_text}; @@ -24,9 +21,8 @@ use super::super::node::{ButtonStyle, Interaction, LabelSpec, WidgetKind, Widget use super::super::popover; use super::super::tree::WidgetTree; -/// Content width inside the popover padding (mirrors the side palette's -/// content column). Shared with the GTK popover viewport via the theme -/// token so the frontends cannot drift. +/// Content width inside the popover padding. Shared with the GTK popover +/// viewport via the theme token so the frontends cannot drift. pub(super) const MENU_CONTENT_W: f64 = crate::ui::theme::toolbar::MENU_CONTENT_W; /// Content column for the Canvas popover. It matches the text-menu width so /// the Step Undo/Redo cluster has comfortable spacing, and is shared with the @@ -59,10 +55,13 @@ const MENU_ITEM_GAP: f64 = 4.0; pub(super) const MENU_ANCHOR_GAP: f64 = 6.0; pub(super) const MENU_BOTTOM_MARGIN: f64 = 4.0; /// Floor so a pathologically short output never collapses the popover to -/// nothing (mirrors the side palette's `MIN_SIDE_VIEWPORT`). +/// nothing. const MENU_MIN_VIEWPORT: f64 = 160.0; /// Gap between two Canvas-popover sections (Boards/Pages/Advanced/Zoom/Step). const CANVAS_SECTION_GAP: f64 = 8.0; +/// Extra separation between safe commands and destructive commands in a +/// Canvas row. This matches the GTK adapter's guarded split. +pub(super) const CANVAS_DESTRUCTIVE_GUARD_GAP: f64 = 12.0; const CANVAS_STEP_ROW_H: f64 = 24.0; const CANVAS_STEP_BTN_W: f64 = 66.0; const CANVAS_STEPPER_W: f64 = 24.0; @@ -410,7 +409,7 @@ fn settings_menu_content(snapshot: &ToolbarSnapshot) -> Option> LabelSpec::new(segment.label.as_ref(), MENU_LABEL_FONT, true), ButtonStyle::active(active == Some(segment.id)), Some(Interaction::click( - segment.activation.compatibility_event(), + segment.activation.clone(), segment.tooltip.as_string(), )), )); @@ -438,7 +437,7 @@ fn settings_menu_content(snapshot: &ToolbarSnapshot) -> Option> label: LabelSpec::new(toggle.label.as_ref(), MENU_META_FONT, true), }, Some(Interaction::click( - toggle.activation.compatibility_event(), + toggle.activation.clone(), toggle.tooltip.as_string(), )), )); @@ -594,7 +593,7 @@ fn settings_menu_content(snapshot: &ToolbarSnapshot) -> Option> label: LabelSpec::new(item.label.as_ref(), MENU_META_FONT, true), }, Some(Interaction::click( - item.activation.compatibility_event(), + item.activation.clone(), item.tooltip.as_string(), )), )); @@ -630,7 +629,7 @@ fn settings_menu_content(snapshot: &ToolbarSnapshot) -> Option> }, enabled.then(|| { Interaction::click( - activation.compatibility_event(), + activation.clone(), Some(format!("{} {}", tooltip, item.label)), ) }), @@ -676,6 +675,9 @@ fn canvas_button_suffix(event: &ToolbarEvent) -> &'static str { ToolbarEvent::PageNew => "page-new", ToolbarEvent::PageDuplicate => "page-duplicate", ToolbarEvent::PageDelete => "page-delete", + ToolbarEvent::Undo => "undo", + ToolbarEvent::Redo => "redo", + ToolbarEvent::ClearCanvas { .. } => "clear-canvas", ToolbarEvent::ZoomIn => "zoom-in", ToolbarEvent::ZoomOut => "zoom-out", ToolbarEvent::ResetZoom => "reset-zoom", @@ -690,8 +692,8 @@ fn canvas_button_suffix(event: &ToolbarEvent) -> &'static str { } /// One Canvas-popover command section: a bold section header over a single -/// row of the group's buttons, laid out edge-to-edge like the side pane's -/// command rows. Advances `y` past the rendered rows. +/// edge-to-edge row of the group's buttons. Advances `y` past the rendered +/// rows. fn push_canvas_command_section( nodes: &mut Vec, y: &mut f64, @@ -708,19 +710,26 @@ fn push_canvas_command_section( )); *y += MENU_HEADER_H + MENU_ITEM_GAP; - let columns = group.buttons.len().clamp(1, 5); - let button_w = row_item_width(CANVAS_MENU_CONTENT_W, columns, MENU_GAP); - let grid = grid_layout( - 0.0, - *y, - button_w, - MENU_BUTTON_H, - MENU_GAP, - MENU_GAP, - columns, - group.buttons.len(), - ); - for (item, button) in grid.items.iter().zip(group.buttons.iter()) { + let (safe, destructive): (Vec<_>, Vec<_>) = group + .buttons + .iter() + .partition(|button| !button.event.is_destructive()); + let split = !safe.is_empty() && !destructive.is_empty(); + let ordinary_gaps = safe.len().saturating_sub(1) + destructive.len().saturating_sub(1); + let total_gap = ordinary_gaps as f64 * MENU_GAP + + if split { + CANVAS_DESTRUCTIVE_GUARD_GAP + } else { + 0.0 + }; + let button_w = (CANVAS_MENU_CONTENT_W - total_gap) / group.buttons.len().max(1) as f64; + let mut x = 0.0; + for (index, button) in safe.iter().chain(destructive.iter()).enumerate() { + if split && index == safe.len() { + x += CANVAS_DESTRUCTIVE_GUARD_GAP; + } else if index > 0 { + x += MENU_GAP; + } let label = button.short_label(snapshot, noun); let style = if !button.enabled { ButtonStyle::disabled() @@ -738,15 +747,16 @@ fn push_canvas_command_section( "top.menu.canvas.{key}.{}", canvas_button_suffix(&button.event) ), - (item.x, item.y, item.w, item.h), + (x, *y, button_w, MENU_BUTTON_H), LabelSpec::new(label, MENU_LABEL_FONT, true), style, button .enabled .then(|| Interaction::click(button.event.clone(), Some(tooltip))), )); + x += button_w; } - *y += grid.height; + *y += MENU_BUTTON_H; } /// One Step Undo/Redo row: the multi-step button, the −/count/+ stepper @@ -972,16 +982,21 @@ fn push_canvas_step_section(nodes: &mut Vec, y: &mut f64, snapshot: } } -/// Canvas popover content: the re-homed side Canvas pane minus its -/// collapsible-card chrome — the Boards, Pages, Advanced, and Zoom command -/// sections (each gated on its display toggle) followed by the Step -/// Undo/Redo configuration. `None` when every section is toggled off. +/// Canvas popover content: Actions, Boards, Pages, Advanced, and Zoom command +/// sections (each gated on its display toggle), followed by the Step Undo/Redo +/// configuration. `None` when every section is toggled off. fn canvas_menu_content(snapshot: &ToolbarSnapshot) -> Option> { let mut nodes = Vec::new(); let mut y = 0.0; let mut rendered = false; let sections = [ + ( + "actions", + "Actions", + "Action", + model::toolbar_actions_model_for_popover(snapshot), + ), ( "boards", "Boards", diff --git a/src/backend/wayland/toolbar/view/top/tests.rs b/src/backend/wayland/toolbar/view/top/tests.rs index d0ab780b..802b079b 100644 --- a/src/backend/wayland/toolbar/view/top/tests.rs +++ b/src/backend/wayland/toolbar/view/top/tests.rs @@ -1202,6 +1202,7 @@ fn canvas_popover_sections_are_gated_by_their_display_toggles() { let build_open = |mutate: &dyn Fn(&mut ToolbarSnapshot)| { let mut s = snapshot(); s.canvas_popover_open = true; + s.show_actions_section = false; s.show_boards_section = false; s.show_pages_section = false; s.show_zoom_actions = false; @@ -1211,6 +1212,28 @@ fn canvas_popover_sections_are_gated_by_their_display_toggles() { build(&s) }; + let actions = build_open(&|s| { + s.show_actions_section = true; + s.undo_available = true; + s.redo_available = true; + }); + assert!(canvas_tree_has_event(&actions, &ToolbarEvent::Undo)); + assert!(canvas_tree_has_event(&actions, &ToolbarEvent::Redo)); + assert!(canvas_tree_has_event( + &actions, + &ToolbarEvent::ClearCanvas { instant: false } + )); + let redo = actions + .node_by_id(&"top.menu.canvas.actions.redo".into()) + .expect("redo action"); + let clear = actions + .node_by_id(&"top.menu.canvas.actions.clear-canvas".into()) + .expect("clear action"); + assert!( + clear.rect.0 - (redo.rect.0 + redo.rect.2) >= menus::CANVAS_DESTRUCTIVE_GUARD_GAP - 1e-9, + "destructive Clear must be isolated from safe actions" + ); + let boards = build_open(&|s| s.show_boards_section = true); assert!( boards @@ -1472,8 +1495,7 @@ fn session_popover_re_hosts_the_session_pane_content() { .expect("recent row"); assert_eq!(node.interact.as_ref().unwrap().event, recent.event()); } - // Meta labels are decor; the pane's collapsible header is side chrome - // and deliberately absent. + // Meta labels are decor; this popover has no collapsible header. assert!(tree.node_by_id(&"top.menu.session.name".into()).is_some()); assert!(tree.node_by_id(&"top.menu.session.path".into()).is_some()); @@ -1552,7 +1574,7 @@ fn settings_popover_re_hosts_the_settings_pane_content() { .expect("mode segment"); assert_eq!( node.interact.as_ref().unwrap().event, - segment.activation.compatibility_event() + segment.activation.clone() ); assert!(matches!( &node.kind, @@ -1570,7 +1592,7 @@ fn settings_popover_re_hosts_the_settings_pane_content() { .unwrap_or_else(|| panic!("toggle {index} ({})", toggle.label)); assert_eq!( node.interact.as_ref().unwrap().event, - toggle.activation.compatibility_event(), + toggle.activation.clone(), "{}", toggle.label ); @@ -1648,7 +1670,7 @@ fn settings_popover_customization_rows_keep_reorder_and_drag_events() { seen += 1; assert_eq!( check.interact.as_ref().unwrap().event, - item.activation.compatibility_event() + item.activation.clone() ); let order = item.order.as_ref().expect("top tools rows are orderable"); let drag = tree @@ -1664,10 +1686,7 @@ fn settings_popover_customization_rows_keep_reorder_and_drag_events() { .expect("move up"); assert_eq!( up.interact.as_ref().map(|interact| &interact.event), - order - .can_move_up - .then(|| order.move_up.compatibility_event()) - .as_ref() + order.can_move_up.then(|| order.move_up.clone()).as_ref() ); let down = tree .node_by_id(&format!("top.menu.settings.item.{index}.down").into()) @@ -1676,7 +1695,7 @@ fn settings_popover_customization_rows_keep_reorder_and_drag_events() { down.interact.as_ref().map(|interact| &interact.event), order .can_move_down - .then(|| order.move_down.compatibility_event()) + .then(|| order.move_down.clone()) .as_ref() ); } @@ -1786,7 +1805,7 @@ fn tall_content_fills_a_tall_screen_without_scrolling() { snapshot.settings_popover_open = true; snapshot.customize_items_open = true; snapshot.customize_items_group = - Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::SideSections); + Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::TopControls); // Without a known output height, the fixed fallback cap still scrolls. let (natural, capped) = @@ -1877,8 +1896,7 @@ fn popover_height_budget_is_relative_to_the_toolbar_surface_origin() { } /// The wheel path scrolls the open popover against the same bounds the -/// tree's scrollbar drags against — the popover must stay wheel-scrollable -/// like the side panes it re-hosts (and the GTK `ScrolledWindow`). +/// tree's scrollbar drags against, matching the GTK `ScrolledWindow`. #[test] fn top_popover_scroll_bounds_serve_the_wheel_path() { let mut snapshot = snapshot(); diff --git a/src/config/AGENTS.md b/src/config/AGENTS.md index 21b1eaf9..fe199e79 100644 --- a/src/config/AGENTS.md +++ b/src/config/AGENTS.md @@ -15,6 +15,7 @@ - Preserve serde compatibility, documented defaults, validation warning behavior, and feature-gated schema output. - Do not silently drop invalid user config; validation should make coercion or rejection explicit. - Keep action labels/categories aligned with help overlay, command palette, keybindings, configurator labels, and docs. +- **Exception (top-only toolbar retirement):** panel-era typed fields may leave the serde model when they are deliberately retired. Authored `config.toml` values for those exact paths stay in the raw document as `RetiredSetting` (see `RETIRED_SETTING_PATHS` in `document.rs`) rather than as inert typed fields. This is a documented Rust source-compatibility break; do not treat it as a silent unknown-key drop. ## Coupled Changes - Any config field change may require `config.example.toml`, `docs/CONFIG.md`, configurator draft/models/views/update code, tests, and schema behavior. diff --git a/src/config/document.rs b/src/config/document.rs index d228092d..a9590ed8 100644 --- a/src/config/document.rs +++ b/src/config/document.rs @@ -29,6 +29,10 @@ use merge::{ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConfigDiagnosticKind { UnknownSetting, + /// A setting this build no longer models, authored by an older build that + /// did. Loading ignores the value and leaves it in the file: nothing the + /// user wrote is deleted on their behalf. + RetiredSetting, /// A shortcut two actions both claim. Loading drops it from one of them /// for the session; the file is left exactly as authored. KeybindingConflict, @@ -64,6 +68,11 @@ impl fmt::Display for ConfigDiagnostic { ConfigDiagnosticKind::UnknownSetting => { write!(formatter, "unrecognized setting `{}`", self.path) } + ConfigDiagnosticKind::RetiredSetting => write!( + formatter, + "retired setting `{}` is preserved but no longer used", + self.path + ), ConfigDiagnosticKind::KeybindingConflict => match &self.detail { Some(detail) => write!(formatter, "{detail}"), None => write!(formatter, "conflicting keybinding in `{}`", self.path), @@ -806,7 +815,11 @@ fn parse_typed_config(input: &str) -> Result { let mut diagnostics: Vec = ignored .into_iter() .map(|path| ConfigDiagnostic { - kind: ConfigDiagnosticKind::UnknownSetting, + kind: if is_retired_path(&path) { + ConfigDiagnosticKind::RetiredSetting + } else { + ConfigDiagnosticKind::UnknownSetting + }, path, detail: None, }) @@ -890,6 +903,36 @@ fn collect_flattened_unknown_paths( Ok(()) } +/// Settings a previous build modeled and this one does not, matched by exact +/// path so a typo next to a retired key still reads as a typo. +const RETIRED_SETTING_PATHS: &[&str] = &[ + "ui.toolbar.collapsed_sections", + "ui.toolbar.items.order.actions", + "ui.toolbar.items.order.boards", + "ui.toolbar.items.order.pages", + "ui.toolbar.items.order.presets", + "ui.toolbar.items.order.sessions", + "ui.toolbar.items.order.side_sections", + "ui.toolbar.items.order.tool_options", + "ui.toolbar.mode_overrides.advanced.show_settings_section", + // `full` is the historical alias for the `regular` mode table; a file may + // spell either one. + "ui.toolbar.mode_overrides.full.show_settings_section", + "ui.toolbar.mode_overrides.regular.show_settings_section", + "ui.toolbar.mode_overrides.simple.show_settings_section", + "ui.toolbar.show_settings_section", + "ui.toolbar.side_active_pane", + "ui.toolbar.side_layout", + "ui.toolbar.side_minimized", + "ui.toolbar.side_offset", + "ui.toolbar.side_offset_x", + "ui.toolbar.side_pinned", +]; + +fn is_retired_path(path: &str) -> bool { + RETIRED_SETTING_PATHS.contains(&path) +} + fn is_known_feature_gated_path(_path: &str) -> bool { #[cfg(not(feature = "tablet-input"))] if _path == "tablet" || _path.starts_with("tablet.") { diff --git a/src/config/mod.rs b/src/config/mod.rs index 503ea844..76833915 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -75,10 +75,10 @@ pub use types::{ ToolbarItemCategory, ToolbarItemDefinition, ToolbarItemId, ToolbarItemOrderConfig, ToolbarItemOrderGroup, ToolbarItemSurface, ToolbarItemsConfig, ToolbarLayoutMode, ToolbarModeOverride, ToolbarModeOverrides, ToolbarRebindModifier, ToolbarSectionFlag, - ToolbarSectionVisibility, ToolbarSideLayout, TopDisplayMode, TrayConfig, TrayIconStyle, - UiConfig, UpdatesConfig, ZoomChipDisplay, default_quick_color_for_index, - fold_legacy_section_flags, resolve_section_visibility, section_flag_for_item, - set_section_visibility, toolbar_item_definitions, toolbar_item_ids, toolbar_item_order_group, + ToolbarSectionVisibility, TopDisplayMode, TrayConfig, TrayIconStyle, UiConfig, UpdatesConfig, + ZoomChipDisplay, default_quick_color_for_index, fold_legacy_section_flags, + resolve_section_visibility, section_flag_for_item, set_section_visibility, + toolbar_item_definitions, toolbar_item_ids, toolbar_item_order_group, validate_pdf_label_template, }; #[cfg(feature = "tablet-input")] diff --git a/src/config/tests/document.rs b/src/config/tests/document.rs index 959e8e2b..4cb559fc 100644 --- a/src/config/tests/document.rs +++ b/src/config/tests/document.rs @@ -41,6 +41,14 @@ fn diagnostic_paths(document: &ConfigDocument) -> Vec<&str> { .collect() } +fn diagnostic_kinds(document: &ConfigDocument) -> Vec<(ConfigDiagnosticKind, &str)> { + document + .diagnostics() + .iter() + .map(|diagnostic| (diagnostic.kind(), diagnostic.path())) + .collect() +} + /// A save writes the caller's one change. Values that only differ because /// loading clamped them keep their authored text: persisting them would let an /// unrelated save rewrite settings the user never touched (#293). @@ -1978,3 +1986,354 @@ future_font_weight = 600 ); } } + +/// A v0.9.23 panel-era toolbar table must load top-only, report every retired +/// inventory path as `RetiredSetting`, keep a similarly named typo as +/// `UnknownSetting`, and survive an unrelated save without rewriting the +/// retired values, comments, numeric spelling, or retained item IDs. +#[test] +fn retired_toolbar_settings_are_preserved_and_diagnosed() { + let temp = TempConfig::new("retired-toolbar"); + temp.write( + r#"# Keep this panel-era header. + +[ui.toolbar] +# retired side seeds +side_layout = "panel" +side_pinned = true +side_minimized = false +side_active_pane = "canvas" +collapsed_sections = ["colors", "session"] +# non-canonical numeric spelling must survive +side_offset = 12 +side_offset_x = 3.50 +show_settings_section = false +top_pinned = true +# typo next to a retired key must stay UnknownSetting +side_layot = "panel" + +[ui.toolbar.mode_overrides.simple] +show_settings_section = false + +[ui.toolbar.mode_overrides.regular] +show_settings_section = true + +[ui.toolbar.mode_overrides.advanced] +show_settings_section = false + +[ui.toolbar.items] +hidden = [ + "side.session.info", + "side.pages.delete", + "side.actions.freeze", + "side.boards.delete", + "side.future.unknown", +] +shown = [ + "side.group.presets", + "side.settings.about", + "side.actions.zoom-in", + "side.boards.picker", +] + +[ui.toolbar.items.order] +side_sections = [ + "side.group.pages", + "side.group.boards", +] +actions = [ + "side.actions.zoom-in", + "side.actions.freeze", +] +pages = [ + "side.pages.previous", +] +boards = [ + "side.boards.picker", + "side.boards.delete", +] +presets = [ + "side.group.presets", +] +tool_options = [ + "side.tool-options.thickness", +] +sessions = [ + "side.session.info", +] +top_tools = [ + "top.tool.pen", +] +top_controls = [ + "top.utility.clear-canvas", + "top.utility.text", +] + +[ui.toolbar.future_extension] +enabled = true +"#, + ); + + let document = ConfigDocument::load_from_path(&temp.path).expect("load retired toolbar"); + let kinds = diagnostic_kinds(&document); + let retired: Vec<&str> = kinds + .iter() + .filter_map(|(kind, path)| (*kind == ConfigDiagnosticKind::RetiredSetting).then_some(*path)) + .collect(); + let unknown: Vec<&str> = kinds + .iter() + .filter_map(|(kind, path)| (*kind == ConfigDiagnosticKind::UnknownSetting).then_some(*path)) + .collect(); + + for path in [ + "ui.toolbar.side_layout", + "ui.toolbar.side_pinned", + "ui.toolbar.side_minimized", + "ui.toolbar.side_active_pane", + "ui.toolbar.collapsed_sections", + "ui.toolbar.side_offset", + "ui.toolbar.side_offset_x", + "ui.toolbar.show_settings_section", + "ui.toolbar.mode_overrides.simple.show_settings_section", + "ui.toolbar.mode_overrides.regular.show_settings_section", + "ui.toolbar.mode_overrides.advanced.show_settings_section", + "ui.toolbar.items.order.side_sections", + "ui.toolbar.items.order.actions", + "ui.toolbar.items.order.pages", + "ui.toolbar.items.order.boards", + "ui.toolbar.items.order.presets", + "ui.toolbar.items.order.tool_options", + "ui.toolbar.items.order.sessions", + ] { + assert!( + retired.contains(&path), + "missing RetiredSetting for {path}: {retired:?}" + ); + assert!( + !unknown.contains(&path), + "{path} must not be UnknownSetting: {unknown:?}" + ); + } + + assert!( + unknown.contains(&"ui.toolbar.side_layot"), + "typo must stay UnknownSetting: {unknown:?}" + ); + assert!( + unknown.iter().any(|path| path.contains("future_extension")), + "extension data must stay UnknownSetting: {unknown:?}" + ); + + assert!(document.config().ui.toolbar.top_pinned); + let assert_active_item_contract = |items: &crate::config::ResolvedToolbarItems| { + use crate::config::toolbar_item_ids as ids; + + for id in [ + ids::SIDE_SESSION_INFO, + ids::SIDE_PAGES_DELETE, + ids::SIDE_ACTIONS_FREEZE, + ids::SIDE_BOARDS_DELETE, + ] { + assert!( + items.hidden.contains(&id), + "missing retained hidden id {id}" + ); + } + for id in [ + ids::SIDE_GROUP_PRESETS, + ids::SIDE_SETTINGS_ABOUT, + ids::SIDE_ACTIONS_ZOOM_IN, + ids::SIDE_BOARDS_PICKER, + ] { + assert!(items.shown.contains(&id), "missing retained shown id {id}"); + } + + assert_eq!( + items + .order + .ordered_ids(crate::config::ToolbarItemOrderGroup::TopTools) + .first(), + Some(&ids::TOP_TOOL_PEN), + "active top_tools order must remain effective" + ); + assert_eq!( + &items + .order + .ordered_ids(crate::config::ToolbarItemOrderGroup::TopControls)[..2], + &[ids::TOP_UTILITY_CLEAR_CANVAS, ids::TOP_UTILITY_TEXT], + "active top_controls order must remain effective" + ); + }; + assert_active_item_contract(&document.config().ui.toolbar.items.resolved()); + + let mut updated = document.config().clone(); + updated.drawing.default_thickness = 7.0; + let outcome = document + .save_with_backup(updated) + .expect("unrelated save with retired toolbar"); + let saved = fs::read_to_string(&temp.path).expect("read saved retired toolbar"); + + for preserved in [ + "# Keep this panel-era header.", + "side_layout = \"panel\"", + "side_pinned = true", + "side_minimized = false", + "side_active_pane = \"canvas\"", + "collapsed_sections = [\"colors\", \"session\"]", + "side_offset = 12", + "side_offset_x = 3.50", + "show_settings_section = false", + "side_sections = [", + "actions = [", + "pages = [", + "boards = [", + "presets = [", + "tool_options = [", + "sessions = [", + "top_tools = [", + "top_controls = [", + "\"side.session.info\"", + "\"side.pages.delete\"", + "\"side.actions.freeze\"", + "\"side.boards.delete\"", + "\"side.actions.zoom-in\"", + "\"side.boards.picker\"", + "\"side.future.unknown\"", + "\"side.group.presets\"", + "\"side.settings.about\"", + "\"side.tool-options.thickness\"", + "side_layot = \"panel\"", + "[ui.toolbar.future_extension]", + "enabled = true", + "default_thickness = 7.0", + ] { + assert!( + saved.contains(preserved), + "missing preserved text after unrelated save: {preserved}\n{saved}" + ); + } + + let reloaded_kinds = diagnostic_kinds(outcome.document()); + let reloaded_retired: Vec<&str> = reloaded_kinds + .iter() + .filter_map(|(kind, path)| (*kind == ConfigDiagnosticKind::RetiredSetting).then_some(*path)) + .collect(); + let reloaded_unknown: Vec<&str> = reloaded_kinds + .iter() + .filter_map(|(kind, path)| (*kind == ConfigDiagnosticKind::UnknownSetting).then_some(*path)) + .collect(); + for path in [ + "ui.toolbar.side_layout", + "ui.toolbar.side_pinned", + "ui.toolbar.side_minimized", + "ui.toolbar.side_active_pane", + "ui.toolbar.collapsed_sections", + "ui.toolbar.side_offset", + "ui.toolbar.side_offset_x", + "ui.toolbar.show_settings_section", + "ui.toolbar.mode_overrides.simple.show_settings_section", + "ui.toolbar.mode_overrides.regular.show_settings_section", + "ui.toolbar.mode_overrides.advanced.show_settings_section", + "ui.toolbar.items.order.side_sections", + "ui.toolbar.items.order.actions", + "ui.toolbar.items.order.pages", + "ui.toolbar.items.order.boards", + "ui.toolbar.items.order.presets", + "ui.toolbar.items.order.tool_options", + "ui.toolbar.items.order.sessions", + ] { + assert!( + reloaded_retired.contains(&path), + "post-save document lost RetiredSetting for {path}: {reloaded_retired:?}" + ); + } + assert!( + reloaded_unknown.contains(&"ui.toolbar.side_layot"), + "post-save typo must stay UnknownSetting: {reloaded_unknown:?}" + ); + assert!( + reloaded_unknown + .iter() + .any(|path| path.contains("future_extension")), + "post-save extension data must stay UnknownSetting: {reloaded_unknown:?}" + ); + assert_active_item_contract(&outcome.document().config().ui.toolbar.items.resolved()); +} + +/// The historical `full` alias for the regular mode table must keep its +/// authored spelling on an unrelated save, and the retired nested field must +/// classify under the path serde_ignored actually emits. +#[test] +fn retired_full_alias_show_settings_section_is_diagnosed_and_spelling_preserved() { + let temp = TempConfig::new("retired-full-alias"); + temp.write( + r#"# alias fixture +[ui.toolbar.mode_overrides.full] +# regular-mode alias spelling +show_settings_section = false +show_presets = true +"#, + ); + + let document = ConfigDocument::load_from_path(&temp.path).expect("load full alias fixture"); + let kinds = diagnostic_kinds(&document); + let retired_paths: Vec<&str> = kinds + .iter() + .filter_map(|(kind, path)| (*kind == ConfigDiagnosticKind::RetiredSetting).then_some(*path)) + .collect(); + assert!( + retired_paths + .iter() + .any(|path| path.contains("show_settings_section")), + "expected a RetiredSetting for show_settings_section, got {retired_paths:?}" + ); + let observed = retired_paths + .iter() + .copied() + .find(|path| path.contains("show_settings_section")) + .expect("retired path"); + assert_eq!( + observed, "ui.toolbar.mode_overrides.full.show_settings_section", + "observed full-alias retired path: {observed}" + ); + + let mut updated = document.config().clone(); + updated.drawing.default_thickness = 5.0; + document + .save_with_backup(updated) + .expect("unrelated save of full alias fixture"); + let saved = fs::read_to_string(&temp.path).expect("read full alias save"); + assert!( + saved.contains("[ui.toolbar.mode_overrides.full]"), + "untouched full alias spelling must survive:\n{saved}" + ); + assert!( + !saved.contains("[ui.toolbar.mode_overrides.regular]"), + "unrelated save must not canonicalize the full alias:\n{saved}" + ); + assert!(saved.contains("show_settings_section = false")); + assert!(saved.contains("show_presets = true")); + assert!(saved.contains("default_thickness = 5.0")); +} + +/// Default/new configs must not emit retired toolbar keys. +#[test] +fn default_config_omits_retired_toolbar_keys() { + let serialized = toml::to_string_pretty(&Config::default()).expect("serialize default config"); + for retired in [ + "side_layout", + "side_pinned", + "side_minimized", + "side_active_pane", + "collapsed_sections", + "side_offset", + "side_offset_x", + "show_settings_section", + "side_sections", + ] { + assert!( + !serialized.contains(retired), + "default config must omit retired key {retired}:\n{serialized}" + ); + } +} diff --git a/src/config/tests/file_io.rs b/src/config/tests/file_io.rs index c1c4b14a..8f21f08b 100644 --- a/src/config/tests/file_io.rs +++ b/src/config/tests/file_io.rs @@ -12,13 +12,13 @@ fn save_with_backup_creates_timestamped_file() { let config_dir = config_root.join(PRIMARY_CONFIG_DIR); fs::create_dir_all(&config_dir).unwrap(); let config_file = config_dir.join("config.toml"); - let original = "# Keep this backup source comment.\n[ui.toolbar]\nside_pinned = true\n"; + let original = "# Keep this backup source comment.\n[ui.toolbar]\ntop_pinned = true\n"; fs::write(&config_file, original).unwrap(); let mut config = Config::load() .expect("load config before backup save") .config; - config.ui.toolbar.side_pinned = false; + config.ui.toolbar.top_pinned = false; let backup_path = save_through_document_with_backup(config).expect("backup should be created"); @@ -35,7 +35,7 @@ fn save_with_backup_creates_timestamped_file() { let new_contents = fs::read_to_string(&config_file).unwrap(); assert!(new_contents.contains("# Keep this backup source comment.")); - assert!(new_contents.contains("side_pinned = false")); + assert!(new_contents.contains("top_pinned = false")); assert!(!new_contents.contains("[drawing]")); }); } @@ -51,7 +51,7 @@ fn a_toolbar_preference_save_preserves_comments_and_unrelated_toml_formatting() r#"# Keep this user comment. [ui.toolbar] -side_pinned = true +top_pinned = true [boards] default_board = "transparent" @@ -71,12 +71,12 @@ default_pen_color = { rgb = [0.0, 0.0, 0.0] } .unwrap(); let mut config = Config::load().expect("load sparse config").config; - config.ui.toolbar.side_pinned = false; + config.ui.toolbar.top_pinned = false; save_through_document(config); let saved = fs::read_to_string(&config_file).unwrap(); assert!(saved.contains("# Keep this user comment.")); - assert!(saved.contains("side_pinned = false")); + assert!(saved.contains("top_pinned = false")); assert!(saved.contains("background = { rgb = [0.992, 0.992, 0.992] }")); assert!(saved.contains("default_pen_color = { rgb = [0.0, 0.0, 0.0] }")); assert!(!saved.contains("[drawing]")); @@ -164,18 +164,18 @@ fn a_save_keeps_an_out_of_range_value_as_authored() { let config_file = config_dir.join("config.toml"); fs::write( &config_file, - "[performance]\nbuffer_count = 99\n\n[ui.toolbar]\nside_pinned = true\n", + "[performance]\nbuffer_count = 99\n\n[ui.toolbar]\ntop_pinned = true\n", ) .unwrap(); let mut config = Config::load().expect("load clamped config").config; assert_eq!(config.performance.buffer_count, 4); - config.ui.toolbar.side_pinned = false; + config.ui.toolbar.top_pinned = false; save_through_document(config); let saved = fs::read_to_string(&config_file).unwrap(); assert!(saved.contains("buffer_count = 99")); - assert!(saved.contains("side_pinned = false")); + assert!(saved.contains("top_pinned = false")); }); } @@ -484,13 +484,13 @@ fn save_with_backup_preserves_symlinked_config_target_and_backup_contents() { let target = managed_dir.join("config.toml"); let config_link = config_dir.join("config.toml"); - let original = "# Keep this symlinked comment.\n[ui.toolbar]\nside_pinned = true\n"; + let original = "# Keep this symlinked comment.\n[ui.toolbar]\ntop_pinned = true\n"; fs::write(&target, original).unwrap(); fs::set_permissions(&target, fs::Permissions::from_mode(0o600)).unwrap(); symlink(&target, &config_link).unwrap(); let mut config = Config::load().expect("load symlinked config").config; - config.ui.toolbar.side_pinned = false; + config.ui.toolbar.top_pinned = false; let backup_path = save_through_document_with_backup(config) .expect("backup should be created for symlinked config"); @@ -516,7 +516,7 @@ fn save_with_backup_preserves_symlinked_config_target_and_backup_contents() { let target_contents = fs::read_to_string(&target).unwrap(); assert!(target_contents.contains("# Keep this symlinked comment.")); - assert!(target_contents.contains("side_pinned = false")); + assert!(target_contents.contains("top_pinned = false")); assert!(!target_contents.contains("[drawing]")); assert_eq!( fs::metadata(&target).unwrap().permissions().mode() & 0o777, diff --git a/src/config/tests/load.rs b/src/config/tests/load.rs index 0af5338f..7de71940 100644 --- a/src/config/tests/load.rs +++ b/src/config/tests/load.rs @@ -108,37 +108,6 @@ fn toolbar_top_display_mode_defaults_to_full_and_round_trips() { assert_eq!(TopDisplayMode::Micro.persisted(), TopDisplayMode::Micro); } -#[test] -fn toolbar_side_layout_defaults_to_pill_and_round_trips() { - use crate::config::ToolbarSideLayout; - - // Pill became the default once the Session/Settings panes were - // re-hosted as top-strip overflow popovers (M4-B3); the classic panel - // remains a deprecated escape hatch. - let default_config: Config = toml::from_str("").expect("empty config should use defaults"); - assert_eq!( - default_config.ui.toolbar.side_layout, - ToolbarSideLayout::Pill - ); - - for (value, expected) in [ - ("pill", ToolbarSideLayout::Pill), - ("panel", ToolbarSideLayout::Panel), - ] { - let config: Config = toml::from_str(&format!("[ui.toolbar]\nside_layout = '{value}'\n")) - .expect("supported side layout should parse"); - assert_eq!(config.ui.toolbar.side_layout, expected); - - let saved = toml::to_string(&config).expect("config should serialize"); - let reloaded: Config = toml::from_str(&saved).expect("saved config should reload"); - assert_eq!(reloaded.ui.toolbar.side_layout, expected); - } - - let error = toml::from_str::("[ui.toolbar]\nside_layout = 'drawer'\n") - .expect_err("unknown side layout should fail"); - assert!(error.to_string().contains("unknown variant")); -} - #[test] fn ui_theme_rejects_unknown_values() { let error = toml::from_str::("[ui]\ntheme = 'sepia'\n") diff --git a/src/config/types/mod.rs b/src/config/types/mod.rs index 9f4ad26a..9d823a42 100644 --- a/src/config/types/mod.rs +++ b/src/config/types/mod.rs @@ -67,8 +67,8 @@ pub use toolbar::{ ResolvedToolbarItems, ToolbarBackendKind, ToolbarConfig, ToolbarGroupId, ToolbarItemCategory, ToolbarItemDefinition, ToolbarItemId, ToolbarItemOrderConfig, ToolbarItemOrderGroup, ToolbarItemSurface, ToolbarItemsConfig, ToolbarLayoutMode, ToolbarModeOverride, - ToolbarModeOverrides, ToolbarRebindModifier, ToolbarSectionDefaults, ToolbarSideLayout, - TopDisplayMode, ZoomChipDisplay, toolbar_item_definitions, toolbar_item_order_group, + ToolbarModeOverrides, ToolbarRebindModifier, ToolbarSectionDefaults, TopDisplayMode, + ZoomChipDisplay, toolbar_item_definitions, toolbar_item_order_group, }; pub(crate) use toolbar::{ ToolbarItemVisibilitySetting, factory_individual_toolbar_item_visibility_settings, diff --git a/src/config/types/presenter_mode.rs b/src/config/types/presenter_mode.rs index 5a32a438..dbfe55b2 100644 --- a/src/config/types/presenter_mode.rs +++ b/src/config/types/presenter_mode.rs @@ -14,7 +14,7 @@ pub struct PresenterModeConfig { /// What `hide_toolbars` does to the top strip: "hidden" removes it /// entirely (default, today's behavior); "micro" collapses it to the - /// 44px micro chip instead. Side toolbars always hide. + /// 44px micro chip instead. #[serde(default)] pub toolbar_mode: PresenterToolbarMode, @@ -64,10 +64,10 @@ impl Default for PresenterModeConfig { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "kebab-case")] pub enum PresenterToolbarMode { - /// Hide the top strip along with the side toolbars. + /// Hide the top strip. #[default] Hidden, - /// Collapse the top strip to the micro chip; side toolbars still hide. + /// Collapse the top strip to the micro chip. Micro, } diff --git a/src/config/types/toolbar/config.rs b/src/config/types/toolbar/config.rs index cae9c87a..5ede8adc 100644 --- a/src/config/types/toolbar/config.rs +++ b/src/config/types/toolbar/config.rs @@ -2,13 +2,13 @@ use serde::{Deserialize, Serialize}; use super::{ ToolbarBackendKind, ToolbarItemsConfig, ToolbarLayoutMode, ToolbarModeOverrides, - ToolbarRebindModifier, ToolbarSideLayout, TopDisplayMode, ZoomChipDisplay, + ToolbarRebindModifier, TopDisplayMode, ZoomChipDisplay, }; /// Toolbar visibility and pinning configuration. /// -/// Controls which toolbar panels are visible on startup and whether they -/// remain pinned (saved to config) when closed. +/// Controls whether the top toolbar is visible on startup and whether it +/// remains pinned (saved to config) when closed. #[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolbarConfig { @@ -32,10 +32,6 @@ pub struct ToolbarConfig { #[serde(default = "default_toolbar_top_pinned")] pub top_pinned: bool, - /// Show the side toolbar (colors, settings) on startup - #[serde(default = "default_toolbar_side_pinned")] - pub side_pinned: bool, - /// Start the top toolbar minimized to its edge restore tab #[serde(default)] pub top_minimized: bool, @@ -46,28 +42,6 @@ pub struct ToolbarConfig { #[serde(default)] pub top_display_mode: TopDisplayMode, - /// Start the side toolbar minimized to its edge restore tab - #[serde(default)] - pub side_minimized: bool, - - /// Where the side-palette functions live ("pill", "panel"). The default - /// "pill" retires the side palette: drawing properties live in the - /// contextual style pill, canvas management in the status HUD/board - /// picker, and Session/Settings in popovers on the top strip's - /// overflow menu. "panel" is the legacy escape hatch restoring the - /// classic side palette (deprecated; removal planned one release after - /// the pill default). - #[serde(default)] - pub side_layout: ToolbarSideLayout, - - /// Side-palette pane restored at startup ("draw", "canvas", "session", "settings") - #[serde(default = "default_side_active_pane")] - pub side_active_pane: String, - - /// Side-palette sections collapsed to their header row - #[serde(default)] - pub collapsed_sections: Vec, - /// Use icons instead of text labels in toolbars #[serde(default = "default_toolbar_use_icons")] pub use_icons: bool, @@ -103,15 +77,15 @@ pub struct ToolbarConfig { #[serde(default = "default_show_zoom_chip")] pub show_zoom_chip: bool, - /// Show the Pages section in the side toolbar + /// Show the Pages section #[serde(default = "default_show_pages_section")] pub show_pages_section: bool, - /// Show the Boards section in the side toolbar + /// Show the Boards section #[serde(default = "default_show_boards_section")] pub show_boards_section: bool, - /// Show the presets section in the side toolbar + /// Show the presets section #[serde(default = "default_show_presets")] pub show_presets: bool, @@ -123,15 +97,11 @@ pub struct ToolbarConfig { #[serde(default = "default_show_text_controls")] pub show_text_controls: bool, - /// Deprecated compatibility mirror. Settings navigation is always reachable. - #[serde(default = "default_show_settings_section")] - pub show_settings_section: bool, - /// Show delay sliders in Step Undo/Redo section #[serde(default = "default_show_delay_sliders")] pub show_delay_sliders: bool, - /// Show the marker opacity slider section in the side toolbar + /// Show the marker opacity slider section #[serde(default = "default_show_marker_opacity_section")] pub show_marker_opacity_section: bool, @@ -155,14 +125,6 @@ pub struct ToolbarConfig { #[serde(default)] pub top_offset_y: f64, - /// Saved vertical offset for the side toolbar (layer-shell/inline) - #[serde(default)] - pub side_offset: f64, - - /// Saved horizontal offset for the side toolbar (layer-shell/inline) - #[serde(default)] - pub side_offset_x: f64, - /// Force inline toolbars even when layer-shell is available (debug/compatibility). #[serde(default)] pub force_inline: bool, @@ -180,15 +142,10 @@ impl Default for ToolbarConfig { mode_overrides: ToolbarModeOverrides::default(), items: ToolbarItemsConfig::default(), top_pinned: default_toolbar_top_pinned(), - side_pinned: default_toolbar_side_pinned(), top_minimized: false, top_display_mode: TopDisplayMode::default(), zoom_chip_display: ZoomChipDisplay::default(), show_zoom_chip: default_show_zoom_chip(), - side_minimized: false, - side_layout: ToolbarSideLayout::default(), - side_active_pane: default_side_active_pane(), - collapsed_sections: Vec::new(), use_icons: default_toolbar_use_icons(), scale: default_toolbar_scale(), show_more_colors: default_show_more_colors(), @@ -200,7 +157,6 @@ impl Default for ToolbarConfig { show_presets: default_show_presets(), show_step_section: default_show_step_section(), show_text_controls: default_show_text_controls(), - show_settings_section: default_show_settings_section(), show_delay_sliders: default_show_delay_sliders(), show_marker_opacity_section: default_show_marker_opacity_section(), context_aware_ui: default_context_aware_ui(), @@ -208,8 +164,6 @@ impl Default for ToolbarConfig { show_tool_preview: default_show_tool_preview(), top_offset: 0.0, top_offset_y: 0.0, - side_offset: 0.0, - side_offset_x: 0.0, force_inline: false, rebind_modifier: ToolbarRebindModifier::default(), } @@ -220,14 +174,6 @@ fn default_toolbar_top_pinned() -> bool { true } -fn default_toolbar_side_pinned() -> bool { - true -} - -fn default_side_active_pane() -> String { - "draw".to_string() -} - fn default_toolbar_use_icons() -> bool { true } @@ -280,10 +226,6 @@ fn default_show_text_controls() -> bool { true } -fn default_show_settings_section() -> bool { - true -} - fn default_show_delay_sliders() -> bool { false } diff --git a/src/config/types/toolbar/display.rs b/src/config/types/toolbar/display.rs index fd802273..cc7bd0b4 100644 --- a/src/config/types/toolbar/display.rs +++ b/src/config/types/toolbar/display.rs @@ -49,32 +49,3 @@ pub enum ZoomChipDisplay { /// chip appears as soon as zoom engages. WhileZoomed, } - -/// Where the side-palette functions live. -/// -/// `Pill` (the default) is the supported layout: the standalone side palette is -/// fully retired and every pane has a concrete new home in the top toolbar. The -/// Draw pane's drawing properties live in the top strip's contextual style pill -/// (colors included); canvas management lives in the "Canvas…" overflow popover, -/// the bottom-right zoom chip, and the status-bar board picker (boards & pages); -/// presets live in the top-strip presets island; and the Session/Settings panes -/// live in popovers opened from the top strip's overflow menu. `Panel` is the -/// deprecated legacy escape hatch that restores the classic four-pane side -/// palette (`side_active_pane`, `collapsed_sections`, `side_pinned`, -/// `side_minimized` all apply); it is deprecated and planned for removal one -/// release after the pill default lands. Panel-mode users see a once-per-session -/// notice pointing at these new homes. -#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "kebab-case")] -pub enum ToolbarSideLayout { - /// No side palette surface; its functions live in the style pill, the - /// "Canvas…" overflow popover, the bottom-right zoom chip, the status-bar - /// board picker, the top-strip presets island, and the Session/Settings - /// overflow popovers (default; the supported layout). - #[default] - Pill, - /// The classic side palette (legacy escape hatch, deprecated; removal - /// planned one release after the pill default). - Panel, -} diff --git a/src/config/types/toolbar/ids.rs b/src/config/types/toolbar/ids.rs index 8f518f75..c3128336 100644 --- a/src/config/types/toolbar/ids.rs +++ b/src/config/types/toolbar/ids.rs @@ -44,26 +44,11 @@ pub const TOP_UTILITY_ICON_MODE_ICONS: ToolbarItemId = pub const TOP_UTILITY_ICON_MODE_TEXT: ToolbarItemId = ToolbarItemId::from_known("top.utility.icon-mode-text"); -pub const SIDE_GROUP_COLORS: ToolbarItemId = ToolbarItemId::from_known("side.group.colors"); -pub const SIDE_GROUP_THICKNESS: ToolbarItemId = ToolbarItemId::from_known("side.group.thickness"); -pub const SIDE_GROUP_ERASER_MODE: ToolbarItemId = - ToolbarItemId::from_known("side.group.eraser-mode"); -pub const SIDE_GROUP_POLYGON_SIDES: ToolbarItemId = - ToolbarItemId::from_known("side.group.polygon-sides"); -pub const SIDE_GROUP_ARROW_LABELS: ToolbarItemId = - ToolbarItemId::from_known("side.group.arrow-labels"); -pub const SIDE_GROUP_STEP_MARKERS: ToolbarItemId = - ToolbarItemId::from_known("side.group.step-markers"); pub const SIDE_GROUP_STEP_UNDO: ToolbarItemId = ToolbarItemId::from_known("side.group.step-undo"); -pub const SIDE_GROUP_MARKER_OPACITY: ToolbarItemId = - ToolbarItemId::from_known("side.group.marker-opacity"); -pub const SIDE_GROUP_TEXT_SIZE: ToolbarItemId = ToolbarItemId::from_known("side.group.text-size"); -pub const SIDE_GROUP_FONT: ToolbarItemId = ToolbarItemId::from_known("side.group.font"); pub const SIDE_GROUP_ACTIONS: ToolbarItemId = ToolbarItemId::from_known("side.group.actions"); pub const SIDE_GROUP_PAGES: ToolbarItemId = ToolbarItemId::from_known("side.group.pages"); pub const SIDE_GROUP_BOARDS: ToolbarItemId = ToolbarItemId::from_known("side.group.boards"); pub const SIDE_GROUP_PRESETS: ToolbarItemId = ToolbarItemId::from_known("side.group.presets"); -pub const SIDE_GROUP_SETTINGS: ToolbarItemId = ToolbarItemId::from_known("side.group.settings"); pub const TOP_GROUP_QUICK_COLORS: ToolbarItemId = ToolbarItemId::from_known("top.group.quick-colors"); pub const TOP_UTILITY_UNDO: ToolbarItemId = ToolbarItemId::from_known("top.utility.undo"); @@ -75,12 +60,7 @@ pub const SIDE_GROUP_ZOOM_ACTIONS: ToolbarItemId = ToolbarItemId::from_known("side.group.zoom-actions"); pub const SIDE_GROUP_TEXT_CONTROLS: ToolbarItemId = ToolbarItemId::from_known("side.group.text-controls"); -pub const SIDE_GROUP_SESSION: ToolbarItemId = ToolbarItemId::from_known("side.group.session"); -pub const SIDE_ACTIONS_UNDO: ToolbarItemId = ToolbarItemId::from_known("side.actions.undo"); -pub const SIDE_ACTIONS_REDO: ToolbarItemId = ToolbarItemId::from_known("side.actions.redo"); -pub const SIDE_ACTIONS_CLEAR_CANVAS: ToolbarItemId = - ToolbarItemId::from_known("side.actions.clear-canvas"); pub const SIDE_ACTIONS_ZOOM_IN: ToolbarItemId = ToolbarItemId::from_known("side.actions.zoom-in"); pub const SIDE_ACTIONS_ZOOM_OUT: ToolbarItemId = ToolbarItemId::from_known("side.actions.zoom-out"); pub const SIDE_ACTIONS_RESET_ZOOM: ToolbarItemId = @@ -107,7 +87,6 @@ pub const SIDE_BOARDS_NEXT: ToolbarItemId = ToolbarItemId::from_known("side.boar pub const SIDE_BOARDS_NEW: ToolbarItemId = ToolbarItemId::from_known("side.boards.new"); pub const SIDE_BOARDS_DUPLICATE: ToolbarItemId = ToolbarItemId::from_known("side.boards.duplicate"); pub const SIDE_BOARDS_DELETE: ToolbarItemId = ToolbarItemId::from_known("side.boards.delete"); -pub const SIDE_BOARDS_RENAME: ToolbarItemId = ToolbarItemId::from_known("side.boards.rename"); pub const SIDE_SETTINGS_CONTEXT_AWARE_UI: ToolbarItemId = ToolbarItemId::from_known("side.settings.context-aware-ui"); @@ -146,22 +125,3 @@ pub const SIDE_SESSION_SAVE_AS: ToolbarItemId = ToolbarItemId::from_known("side. pub const SIDE_SESSION_INFO: ToolbarItemId = ToolbarItemId::from_known("side.session.info"); pub const SIDE_SESSION_CLEAR: ToolbarItemId = ToolbarItemId::from_known("side.session.clear"); pub const SIDE_SESSION_MANAGER: ToolbarItemId = ToolbarItemId::from_known("side.session.manager"); - -pub const SIDE_TOOL_OPTIONS_COLOR: ToolbarItemId = - ToolbarItemId::from_known("side.tool-options.color"); -pub const SIDE_TOOL_OPTIONS_THICKNESS: ToolbarItemId = - ToolbarItemId::from_known("side.tool-options.thickness"); -pub const SIDE_TOOL_OPTIONS_MARKER_OPACITY: ToolbarItemId = - ToolbarItemId::from_known("side.tool-options.marker-opacity"); -pub const SIDE_TOOL_OPTIONS_ERASER_MODE: ToolbarItemId = - ToolbarItemId::from_known("side.tool-options.eraser-mode"); -pub const SIDE_TOOL_OPTIONS_FONT_SIZE: ToolbarItemId = - ToolbarItemId::from_known("side.tool-options.font-size"); -pub const SIDE_TOOL_OPTIONS_FONT_FAMILY: ToolbarItemId = - ToolbarItemId::from_known("side.tool-options.font-family"); -pub const SIDE_TOOL_OPTIONS_POLYGON_SIDES: ToolbarItemId = - ToolbarItemId::from_known("side.tool-options.polygon-sides"); -pub const SIDE_TOOL_OPTIONS_ARROW_LABELS: ToolbarItemId = - ToolbarItemId::from_known("side.tool-options.arrow-labels"); -pub const SIDE_TOOL_OPTIONS_STEP_MARKER_RESET: ToolbarItemId = - ToolbarItemId::from_known("side.tool-options.step-marker-reset"); diff --git a/src/config/types/toolbar/items.rs b/src/config/types/toolbar/items.rs index c4712396..66f969e7 100644 --- a/src/config/types/toolbar/items.rs +++ b/src/config/types/toolbar/items.rs @@ -81,22 +81,6 @@ const DEFAULT_TOP_CONTROLS_ORDER: &[ToolbarItemId] = &[ ids::TOP_UTILITY_HIGHLIGHT, ]; -const DEFAULT_SIDE_SECTIONS_ORDER: &[ToolbarItemId] = &[ - ids::SIDE_GROUP_COLORS, - ids::SIDE_GROUP_PRESETS, - ids::SIDE_GROUP_THICKNESS, - ids::SIDE_GROUP_ARROW_LABELS, - ids::SIDE_GROUP_STEP_MARKERS, - ids::SIDE_GROUP_MARKER_OPACITY, - ids::SIDE_GROUP_TEXT_SIZE, - ids::SIDE_GROUP_ACTIONS, - ids::SIDE_GROUP_BOARDS, - ids::SIDE_GROUP_PAGES, - ids::SIDE_GROUP_STEP_UNDO, - ids::SIDE_GROUP_SESSION, - ids::SIDE_GROUP_SETTINGS, -]; - impl Default for ToolbarItemsConfig { fn default() -> Self { Self { @@ -312,9 +296,7 @@ pub(crate) fn factory_individual_toolbar_item_visibility_settings() /// Canonical visibility-customization predicate shared by the settings UI, /// runtime-state seed builder, and factory reset implementation. pub(crate) fn toolbar_item_visibility_override_allowed(definition: &ToolbarItemDefinition) -> bool { - definition.group != Some(ToolbarGroupId::Settings) - && definition.id != ids::SIDE_GROUP_SETTINGS - && definition.id != ids::TOP_CHROME_OVERFLOW + definition.group != Some(ToolbarGroupId::Settings) && definition.id != ids::TOP_CHROME_OVERFLOW } /// Individual toolbar items affected by "Restore built-in visibility". @@ -393,7 +375,8 @@ impl ToolbarItemDefinition { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ToolbarItemSurface { Top, - Side, + /// Historical `side.*` item ids that customize top popover sections. + Popover, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -407,21 +390,16 @@ pub enum ToolbarItemCategory { Board, Setting, Session, - ToolOption, } +/// Logical groups for retained toolbar item definitions. +/// +/// Panel-era draw groups (`colors`, `thickness`, `eraser-mode`, …) were retired +/// with the side palette; only groups that still label live top/popover items +/// remain. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ToolbarGroupId { - Colors, - Thickness, - EraserMode, - PolygonSides, - ArrowLabels, - StepMarkers, StepUndo, - MarkerOpacity, - TextSize, - Font, Actions, Pages, Boards, @@ -433,16 +411,7 @@ pub enum ToolbarGroupId { impl ToolbarGroupId { pub const fn as_str(self) -> &'static str { match self { - Self::Colors => "colors", - Self::Thickness => "thickness", - Self::EraserMode => "eraser-mode", - Self::PolygonSides => "polygon-sides", - Self::ArrowLabels => "arrow-labels", - Self::StepMarkers => "step-markers", Self::StepUndo => "step-undo", - Self::MarkerOpacity => "marker-opacity", - Self::TextSize => "text-size", - Self::Font => "font", Self::Actions => "actions", Self::Pages => "pages", Self::Boards => "boards", @@ -451,27 +420,6 @@ impl ToolbarGroupId { Self::Session => "session", } } - - pub const fn toolbar_item_id(self) -> ToolbarItemId { - match self { - Self::Colors => ids::SIDE_GROUP_COLORS, - Self::Thickness => ids::SIDE_GROUP_THICKNESS, - Self::EraserMode => ids::SIDE_GROUP_ERASER_MODE, - Self::PolygonSides => ids::SIDE_GROUP_POLYGON_SIDES, - Self::ArrowLabels => ids::SIDE_GROUP_ARROW_LABELS, - Self::StepMarkers => ids::SIDE_GROUP_STEP_MARKERS, - Self::StepUndo => ids::SIDE_GROUP_STEP_UNDO, - Self::MarkerOpacity => ids::SIDE_GROUP_MARKER_OPACITY, - Self::TextSize => ids::SIDE_GROUP_TEXT_SIZE, - Self::Font => ids::SIDE_GROUP_FONT, - Self::Actions => ids::SIDE_GROUP_ACTIONS, - Self::Pages => ids::SIDE_GROUP_PAGES, - Self::Boards => ids::SIDE_GROUP_BOARDS, - Self::Presets => ids::SIDE_GROUP_PRESETS, - Self::Settings => ids::SIDE_GROUP_SETTINGS, - Self::Session => ids::SIDE_GROUP_SESSION, - } - } } impl fmt::Display for ToolbarGroupId { @@ -485,16 +433,7 @@ impl FromStr for ToolbarGroupId { fn from_str(value: &str) -> Result { match value.trim() { - "colors" => Ok(Self::Colors), - "thickness" => Ok(Self::Thickness), - "eraser-mode" => Ok(Self::EraserMode), - "polygon-sides" => Ok(Self::PolygonSides), - "arrow-labels" => Ok(Self::ArrowLabels), - "step-markers" => Ok(Self::StepMarkers), "step-undo" => Ok(Self::StepUndo), - "marker-opacity" => Ok(Self::MarkerOpacity), - "text-size" => Ok(Self::TextSize), - "font" => Ok(Self::Font), "actions" => Ok(Self::Actions), "pages" => Ok(Self::Pages), "boards" => Ok(Self::Boards), diff --git a/src/config/types/toolbar/items/definitions.rs b/src/config/types/toolbar/items/definitions.rs index 8f9b0657..73336a31 100644 --- a/src/config/types/toolbar/items/definitions.rs +++ b/src/config/types/toolbar/items/definitions.rs @@ -112,531 +112,363 @@ const TOOLBAR_ITEM_DEFINITIONS: &[ToolbarItemDefinition] = &[ Utility, None, ), - item( - ids::SIDE_GROUP_COLORS, - "Colors", - Side, - Group, - Some(ToolbarGroupId::Colors), - ), - item( - ids::SIDE_GROUP_THICKNESS, - "Thickness", - Side, - Group, - Some(ToolbarGroupId::Thickness), - ), - item( - ids::SIDE_GROUP_ERASER_MODE, - "Eraser mode", - Side, - Group, - Some(ToolbarGroupId::EraserMode), - ), - item( - ids::SIDE_GROUP_POLYGON_SIDES, - "Polygon sides", - Side, - Group, - Some(ToolbarGroupId::PolygonSides), - ), - item( - ids::SIDE_GROUP_ARROW_LABELS, - "Arrow labels", - Side, - Group, - Some(ToolbarGroupId::ArrowLabels), - ), - item( - ids::SIDE_GROUP_STEP_MARKERS, - "Step markers", - Side, - Group, - Some(ToolbarGroupId::StepMarkers), - ), item( ids::SIDE_GROUP_STEP_UNDO, "Step Undo/Redo", - Side, + Popover, Group, Some(ToolbarGroupId::StepUndo), ), - item( - ids::SIDE_GROUP_MARKER_OPACITY, - "Marker opacity", - Side, - Group, - Some(ToolbarGroupId::MarkerOpacity), - ), - item( - ids::SIDE_GROUP_TEXT_SIZE, - "Text size", - Side, - Group, - Some(ToolbarGroupId::TextSize), - ), - item( - ids::SIDE_GROUP_FONT, - "Font", - Side, - Group, - Some(ToolbarGroupId::Font), - ), item( ids::SIDE_GROUP_ACTIONS, "Actions", - Side, + Popover, Group, Some(ToolbarGroupId::Actions), ), item( ids::SIDE_GROUP_PAGES, "Pages", - Side, + Popover, Group, Some(ToolbarGroupId::Pages), ), item( ids::SIDE_GROUP_BOARDS, "Boards", - Side, + Popover, Group, Some(ToolbarGroupId::Boards), ), item( ids::SIDE_GROUP_PRESETS, "Presets", - Side, + Popover, Group, Some(ToolbarGroupId::Presets), ), - item( - ids::SIDE_GROUP_SETTINGS, - "Settings", - Side, - Group, - Some(ToolbarGroupId::Settings), - ), item( ids::SIDE_GROUP_ACTIONS_ADVANCED, "Advanced actions", - Side, + Popover, Group, None, ), item( ids::SIDE_GROUP_ZOOM_ACTIONS, "Zoom actions", - Side, + Popover, Group, None, ), item( ids::SIDE_GROUP_TEXT_CONTROLS, "Text controls (persistent)", - Side, + Popover, Group, None, ), - item( - ids::SIDE_GROUP_SESSION, - "Session", - Side, - Group, - Some(ToolbarGroupId::Session), - ), - item( - ids::SIDE_ACTIONS_UNDO, - "Undo", - Side, - Action, - Some(ToolbarGroupId::Actions), - ), - item( - ids::SIDE_ACTIONS_REDO, - "Redo", - Side, - Action, - Some(ToolbarGroupId::Actions), - ), - item( - ids::SIDE_ACTIONS_CLEAR_CANVAS, - "Clear canvas", - Side, - Action, - Some(ToolbarGroupId::Actions), - ), item( ids::SIDE_ACTIONS_ZOOM_IN, "Zoom in", - Side, + Popover, Action, Some(ToolbarGroupId::Actions), ), item( ids::SIDE_ACTIONS_ZOOM_OUT, "Zoom out", - Side, + Popover, Action, Some(ToolbarGroupId::Actions), ), item( ids::SIDE_ACTIONS_RESET_ZOOM, "Reset zoom", - Side, + Popover, Action, Some(ToolbarGroupId::Actions), ), item( ids::SIDE_ACTIONS_TOGGLE_ZOOM_LOCK, "Toggle zoom lock", - Side, + Popover, Action, Some(ToolbarGroupId::Actions), ), item( ids::SIDE_ACTIONS_UNDO_ALL, "Undo all", - Side, + Popover, Action, Some(ToolbarGroupId::Actions), ), item( ids::SIDE_ACTIONS_REDO_ALL, "Redo all", - Side, + Popover, Action, Some(ToolbarGroupId::Actions), ), item( ids::SIDE_ACTIONS_UNDO_ALL_DELAYED, "Delayed undo all", - Side, + Popover, Action, Some(ToolbarGroupId::Actions), ), item( ids::SIDE_ACTIONS_REDO_ALL_DELAYED, "Delayed redo all", - Side, + Popover, Action, Some(ToolbarGroupId::Actions), ), item( ids::SIDE_ACTIONS_FREEZE, "Freeze", - Side, + Popover, Action, Some(ToolbarGroupId::Actions), ), item( ids::SIDE_PAGES_PREVIOUS, "Previous page", - Side, + Popover, Page, Some(ToolbarGroupId::Pages), ), item( ids::SIDE_PAGES_NEXT, "Next page", - Side, + Popover, Page, Some(ToolbarGroupId::Pages), ), item( ids::SIDE_PAGES_NEW, "New page", - Side, + Popover, Page, Some(ToolbarGroupId::Pages), ), item( ids::SIDE_PAGES_DUPLICATE, "Duplicate page", - Side, + Popover, Page, Some(ToolbarGroupId::Pages), ), item( ids::SIDE_PAGES_DELETE, "Delete page", - Side, + Popover, Page, Some(ToolbarGroupId::Pages), ), item( ids::SIDE_BOARDS_PICKER, "Board picker", - Side, + Popover, Board, Some(ToolbarGroupId::Boards), ), item( ids::SIDE_BOARDS_PREVIOUS, "Previous board", - Side, + Popover, Board, Some(ToolbarGroupId::Boards), ), item( ids::SIDE_BOARDS_NEXT, "Next board", - Side, + Popover, Board, Some(ToolbarGroupId::Boards), ), item( ids::SIDE_BOARDS_NEW, "New board", - Side, + Popover, Board, Some(ToolbarGroupId::Boards), ), item( ids::SIDE_BOARDS_DUPLICATE, "Duplicate board", - Side, + Popover, Board, Some(ToolbarGroupId::Boards), ), item( ids::SIDE_BOARDS_DELETE, "Delete board", - Side, - Board, - Some(ToolbarGroupId::Boards), - ), - item( - ids::SIDE_BOARDS_RENAME, - "Rename board", - Side, + Popover, Board, Some(ToolbarGroupId::Boards), ), item( ids::SIDE_SETTINGS_CONTEXT_AWARE_UI, "Adapt to tool", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_TEXT_CONTROLS, "Text controls", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_STATUS_BAR, "Status bar", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_STATUS_BOARD_BADGE, "Status board badge", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_STATUS_PAGE_BADGE, "Status page badge", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_FLOATING_BADGE_ALWAYS, "Floating board/page badge", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_PRESET_TOASTS, "Preset toasts", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_PRESETS, "Presets toggle", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_ACTIONS, "Actions toggle", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_ZOOM_ACTIONS, "Zoom actions toggle", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_ADVANCED_ACTIONS, "Advanced actions toggle", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_BOARDS, "Boards toggle", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_PAGES, "Pages toggle", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_STEP_CONTROLS, "Multi-step undo/redo toggle", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_COMMAND_PALETTE, "Open command palette", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_CONFIGURATOR, "Open configurator", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_CONFIG_FILE, "Open config file", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SETTINGS_ABOUT, "Open About window", - Side, + Popover, Setting, Some(ToolbarGroupId::Settings), ), item( ids::SIDE_SESSION_OPEN, "Open session", - Side, + Popover, Session, Some(ToolbarGroupId::Session), ), item( ids::SIDE_SESSION_SAVE_AS, "Save session as", - Side, + Popover, Session, Some(ToolbarGroupId::Session), ), item( ids::SIDE_SESSION_INFO, "Session info", - Side, + Popover, Session, Some(ToolbarGroupId::Session), ), item( ids::SIDE_SESSION_CLEAR, "Clear session", - Side, + Popover, Session, Some(ToolbarGroupId::Session), ), item( ids::SIDE_SESSION_MANAGER, "Session manager", - Side, + Popover, Session, Some(ToolbarGroupId::Session), ), - item( - ids::SIDE_TOOL_OPTIONS_COLOR, - "Color", - Side, - ToolOption, - Some(ToolbarGroupId::Colors), - ), - item( - ids::SIDE_TOOL_OPTIONS_THICKNESS, - "Thickness", - Side, - ToolOption, - Some(ToolbarGroupId::Thickness), - ), - item( - ids::SIDE_TOOL_OPTIONS_MARKER_OPACITY, - "Marker opacity", - Side, - ToolOption, - Some(ToolbarGroupId::MarkerOpacity), - ), - item( - ids::SIDE_TOOL_OPTIONS_ERASER_MODE, - "Eraser mode", - Side, - ToolOption, - Some(ToolbarGroupId::EraserMode), - ), - item( - ids::SIDE_TOOL_OPTIONS_FONT_SIZE, - "Font size", - Side, - ToolOption, - Some(ToolbarGroupId::TextSize), - ), - item( - ids::SIDE_TOOL_OPTIONS_FONT_FAMILY, - "Font family", - Side, - ToolOption, - Some(ToolbarGroupId::Font), - ), - item( - ids::SIDE_TOOL_OPTIONS_POLYGON_SIDES, - "Polygon sides", - Side, - ToolOption, - Some(ToolbarGroupId::PolygonSides), - ), - item( - ids::SIDE_TOOL_OPTIONS_ARROW_LABELS, - "Arrow labels", - Side, - ToolOption, - Some(ToolbarGroupId::ArrowLabels), - ), - item( - ids::SIDE_TOOL_OPTIONS_STEP_MARKER_RESET, - "Reset step marker", - Side, - ToolOption, - Some(ToolbarGroupId::StepMarkers), - ), ]; const fn item( @@ -649,7 +481,5 @@ const fn item( ToolbarItemDefinition::new(id, label, surface, category, group) } -use ToolbarItemCategory::{ - Action, Board, Chrome, Group, Page, Session, Setting, Tool, ToolOption, Utility, -}; -use ToolbarItemSurface::{Side, Top}; +use ToolbarItemCategory::{Action, Board, Chrome, Group, Page, Session, Setting, Tool, Utility}; +use ToolbarItemSurface::{Popover, Top}; diff --git a/src/config/types/toolbar/items/order.rs b/src/config/types/toolbar/items/order.rs index 55c92523..f4cd673b 100644 --- a/src/config/types/toolbar/items/order.rs +++ b/src/config/types/toolbar/items/order.rs @@ -7,20 +7,6 @@ pub struct ToolbarItemOrderConfig { pub top_tools: Vec, #[serde(default)] pub top_controls: Vec, - #[serde(default)] - pub side_sections: Vec, - #[serde(default)] - pub actions: Vec, - #[serde(default)] - pub pages: Vec, - #[serde(default)] - pub boards: Vec, - #[serde(default)] - pub presets: Vec, - #[serde(default)] - pub tool_options: Vec, - #[serde(default)] - pub sessions: Vec, } impl ToolbarItemOrderConfig { @@ -31,19 +17,6 @@ impl ToolbarItemOrderConfig { ToolbarItemOrderGroup::TopControls, &self.top_controls, ), - side_sections: resolve_order_group( - ToolbarItemOrderGroup::SideSections, - &self.side_sections, - ), - actions: resolve_order_group(ToolbarItemOrderGroup::Actions, &self.actions), - pages: resolve_order_group(ToolbarItemOrderGroup::Pages, &self.pages), - boards: resolve_order_group(ToolbarItemOrderGroup::Boards, &self.boards), - presets: resolve_order_group(ToolbarItemOrderGroup::Presets, &self.presets), - tool_options: resolve_order_group( - ToolbarItemOrderGroup::ToolOptions, - &self.tool_options, - ), - sessions: resolve_order_group(ToolbarItemOrderGroup::Sessions, &self.sessions), } } @@ -51,13 +24,6 @@ impl ToolbarItemOrderConfig { match group { ToolbarItemOrderGroup::TopTools => &mut self.top_tools, ToolbarItemOrderGroup::TopControls => &mut self.top_controls, - ToolbarItemOrderGroup::SideSections => &mut self.side_sections, - ToolbarItemOrderGroup::Actions => &mut self.actions, - ToolbarItemOrderGroup::Pages => &mut self.pages, - ToolbarItemOrderGroup::Boards => &mut self.boards, - ToolbarItemOrderGroup::Presets => &mut self.presets, - ToolbarItemOrderGroup::ToolOptions => &mut self.tool_options, - ToolbarItemOrderGroup::Sessions => &mut self.sessions, } } @@ -65,13 +31,6 @@ impl ToolbarItemOrderConfig { match group { ToolbarItemOrderGroup::TopTools => &self.top_tools, ToolbarItemOrderGroup::TopControls => &self.top_controls, - ToolbarItemOrderGroup::SideSections => &self.side_sections, - ToolbarItemOrderGroup::Actions => &self.actions, - ToolbarItemOrderGroup::Pages => &self.pages, - ToolbarItemOrderGroup::Boards => &self.boards, - ToolbarItemOrderGroup::Presets => &self.presets, - ToolbarItemOrderGroup::ToolOptions => &self.tool_options, - ToolbarItemOrderGroup::Sessions => &self.sessions, } } @@ -107,13 +66,6 @@ impl ToolbarItemOrderConfig { pub struct ResolvedToolbarOrder { top_tools: ResolvedToolbarOrderGroup, top_controls: ResolvedToolbarOrderGroup, - side_sections: ResolvedToolbarOrderGroup, - actions: ResolvedToolbarOrderGroup, - pages: ResolvedToolbarOrderGroup, - boards: ResolvedToolbarOrderGroup, - presets: ResolvedToolbarOrderGroup, - tool_options: ResolvedToolbarOrderGroup, - sessions: ResolvedToolbarOrderGroup, } impl ResolvedToolbarOrder { @@ -131,13 +83,6 @@ impl ResolvedToolbarOrder { match group { ToolbarItemOrderGroup::TopTools => &self.top_tools, ToolbarItemOrderGroup::TopControls => &self.top_controls, - ToolbarItemOrderGroup::SideSections => &self.side_sections, - ToolbarItemOrderGroup::Actions => &self.actions, - ToolbarItemOrderGroup::Pages => &self.pages, - ToolbarItemOrderGroup::Boards => &self.boards, - ToolbarItemOrderGroup::Presets => &self.presets, - ToolbarItemOrderGroup::ToolOptions => &self.tool_options, - ToolbarItemOrderGroup::Sessions => &self.sessions, } } } @@ -148,31 +93,21 @@ struct ResolvedToolbarOrderGroup { unknown: Vec, } +/// Live reorder groups for the unified top toolbar. +/// +/// Panel-era order lists (`actions`, `pages`, `boards`, `presets`, +/// `tool_options`, `sessions`, `side_sections`) are retired config/runtime +/// keys: authored `config.toml` values stay in the raw document as +/// `RetiredSetting`, while `runtime-ui.toml` recognized-map entries are pruned +/// on rewrite like other unknown order IDs. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ToolbarItemOrderGroup { TopTools, TopControls, - SideSections, - Actions, - Pages, - Boards, - Presets, - ToolOptions, - Sessions, } impl ToolbarItemOrderGroup { - pub(crate) const ALL: [Self; 9] = [ - Self::TopTools, - Self::TopControls, - Self::SideSections, - Self::Actions, - Self::Pages, - Self::Boards, - Self::Presets, - Self::ToolOptions, - Self::Sessions, - ]; + pub(crate) const ALL: [Self; 2] = [Self::TopTools, Self::TopControls]; } pub fn toolbar_item_order_group( @@ -187,15 +122,6 @@ pub fn toolbar_item_order_group( { Some(ToolbarItemOrderGroup::TopControls) } - (_, ToolbarItemCategory::Group, Some(group)) if side_section_orderable(group) => { - Some(ToolbarItemOrderGroup::SideSections) - } - (_, ToolbarItemCategory::Action, _) => Some(ToolbarItemOrderGroup::Actions), - (_, ToolbarItemCategory::Page, _) => Some(ToolbarItemOrderGroup::Pages), - (_, ToolbarItemCategory::Board, _) => Some(ToolbarItemOrderGroup::Boards), - (_, ToolbarItemCategory::ToolOption, _) => Some(ToolbarItemOrderGroup::ToolOptions), - (_, ToolbarItemCategory::Session, _) => Some(ToolbarItemOrderGroup::Sessions), - (_, _, Some(ToolbarGroupId::Presets)) => Some(ToolbarItemOrderGroup::Presets), _ => None, } } @@ -204,25 +130,6 @@ fn top_control_orderable(id: ToolbarItemId) -> bool { DEFAULT_TOP_CONTROLS_ORDER.contains(&id) } -fn side_section_orderable(group: ToolbarGroupId) -> bool { - matches!( - group, - ToolbarGroupId::Colors - | ToolbarGroupId::Thickness - | ToolbarGroupId::ArrowLabels - | ToolbarGroupId::StepMarkers - | ToolbarGroupId::MarkerOpacity - | ToolbarGroupId::TextSize - | ToolbarGroupId::Actions - | ToolbarGroupId::Pages - | ToolbarGroupId::Boards - | ToolbarGroupId::Presets - | ToolbarGroupId::StepUndo - | ToolbarGroupId::Session - | ToolbarGroupId::Settings - ) -} - pub fn toolbar_item_id_in_order_group(id: ToolbarItemId, group: ToolbarItemOrderGroup) -> bool { toolbar_item_definitions() .iter() @@ -263,21 +170,10 @@ fn resolve_order_group(group: ToolbarItemOrderGroup, raw: &[String]) -> Resolved } fn default_order_for_group(group: ToolbarItemOrderGroup) -> Vec { - let default_visual_order = match group { - ToolbarItemOrderGroup::TopTools => Some(DEFAULT_TOP_TOOLS_ORDER), - ToolbarItemOrderGroup::TopControls => Some(DEFAULT_TOP_CONTROLS_ORDER), - ToolbarItemOrderGroup::SideSections => Some(DEFAULT_SIDE_SECTIONS_ORDER), - _ => None, - }; - if let Some(order) = default_visual_order { - return order.to_vec(); + match group { + ToolbarItemOrderGroup::TopTools => DEFAULT_TOP_TOOLS_ORDER.to_vec(), + ToolbarItemOrderGroup::TopControls => DEFAULT_TOP_CONTROLS_ORDER.to_vec(), } - - toolbar_item_definitions() - .iter() - .filter(|definition| toolbar_item_order_group(definition) == Some(group)) - .map(|definition| definition.id) - .collect() } fn append_preserved_order_strings( diff --git a/src/config/types/toolbar/items/tests.rs b/src/config/types/toolbar/items/tests.rs index 19d91046..144b8ac5 100644 --- a/src/config/types/toolbar/items/tests.rs +++ b/src/config/types/toolbar/items/tests.rs @@ -65,7 +65,7 @@ fn resettable_visibility_ids_are_exactly_customizable_individual_items() { assert!(actual.contains(&ids::TOP_TOOL_PEN)); assert!(actual.contains(&ids::TOP_UTILITY_SCREENSHOT)); assert!(!actual.contains(&ids::TOP_CHROME_OVERFLOW)); - assert!(!actual.contains(&ids::SIDE_GROUP_SETTINGS)); + assert!(!actual.contains(&ids::SIDE_SETTINGS_ABOUT)); } #[test] @@ -130,12 +130,6 @@ fn default_order_matches_visual_toolbar_defaults() { .ordered_ids(ToolbarItemOrderGroup::TopControls), DEFAULT_TOP_CONTROLS_ORDER ); - assert_eq!( - resolved - .order - .ordered_ids(ToolbarItemOrderGroup::SideSections), - DEFAULT_SIDE_SECTIONS_ORDER - ); } #[test] @@ -193,39 +187,52 @@ fn top_control_order_excludes_visibility_only_utilities() { } #[test] -fn side_section_order_uses_runtime_representable_blocks() { - let config = ToolbarItemsConfig { - hidden: Vec::new(), - shown: Vec::new(), - order: ToolbarItemOrderConfig { - side_sections: vec![ - ids::SIDE_GROUP_FONT.as_str().to_string(), - ids::SIDE_GROUP_THICKNESS.as_str().to_string(), - ids::SIDE_GROUP_POLYGON_SIDES.as_str().to_string(), - ], - ..ToolbarItemOrderConfig::default() - }, - }; - - let resolved = config.resolved(); - let ordered = resolved - .order - .ordered_ids(ToolbarItemOrderGroup::SideSections); - assert_eq!(ordered[0], ids::SIDE_GROUP_THICKNESS); - assert!(!ordered.contains(&ids::SIDE_GROUP_FONT)); - assert!(!ordered.contains(&ids::SIDE_GROUP_POLYGON_SIDES)); -} - -#[test] -fn toolbar_group_ids_include_step_markers_and_step_undo() { - assert_eq!( - "step-markers".parse::(), - Ok(ToolbarGroupId::StepMarkers) - ); +fn toolbar_group_ids_cover_live_popover_groups_only() { assert_eq!( "step-undo".parse::(), Ok(ToolbarGroupId::StepUndo) ); + assert_eq!( + "actions".parse::(), + Ok(ToolbarGroupId::Actions) + ); + assert!("step-markers".parse::().is_err()); + assert!("colors".parse::().is_err()); +} + +#[test] +fn every_retired_panel_item_id_is_unknown_to_the_active_model() { + for id in [ + "side.group.colors", + "side.group.thickness", + "side.group.eraser-mode", + "side.group.polygon-sides", + "side.group.arrow-labels", + "side.group.step-markers", + "side.group.marker-opacity", + "side.group.text-size", + "side.group.font", + "side.group.settings", + "side.group.session", + "side.actions.undo", + "side.actions.redo", + "side.actions.clear-canvas", + "side.boards.rename", + "side.tool-options.color", + "side.tool-options.thickness", + "side.tool-options.marker-opacity", + "side.tool-options.eraser-mode", + "side.tool-options.font-size", + "side.tool-options.font-family", + "side.tool-options.polygon-sides", + "side.tool-options.arrow-labels", + "side.tool-options.step-marker-reset", + ] { + assert!( + id.parse::().is_err(), + "retired panel id must not enter the active model: {id}" + ); + } } #[test] diff --git a/src/config/types/toolbar/mod.rs b/src/config/types/toolbar/mod.rs index 214e224e..b65c6133 100644 --- a/src/config/types/toolbar/mod.rs +++ b/src/config/types/toolbar/mod.rs @@ -10,7 +10,7 @@ mod visibility; pub use backend::ToolbarBackendKind; pub use config::ToolbarConfig; -pub use display::{ToolbarSideLayout, TopDisplayMode, ZoomChipDisplay}; +pub use display::{TopDisplayMode, ZoomChipDisplay}; pub use items::{ ResolvedToolbarItems, ToolbarGroupId, ToolbarItemCategory, ToolbarItemDefinition, ToolbarItemId, ToolbarItemOrderConfig, ToolbarItemOrderGroup, ToolbarItemSurface, diff --git a/src/config/types/toolbar/overrides.rs b/src/config/types/toolbar/overrides.rs index 9bcf768a..77b6fc10 100644 --- a/src/config/types/toolbar/overrides.rs +++ b/src/config/types/toolbar/overrides.rs @@ -18,15 +18,15 @@ pub struct ToolbarModeOverride { #[serde(default)] pub show_zoom_actions: Option, - /// Show the Pages section in the side toolbar + /// Show the Pages section #[serde(default)] pub show_pages_section: Option, - /// Show the Boards section in the side toolbar + /// Show the Boards section #[serde(default)] pub show_boards_section: Option, - /// Show the presets section in the side toolbar + /// Show the presets section #[serde(default)] pub show_presets: Option, @@ -37,10 +37,6 @@ pub struct ToolbarModeOverride { /// Keep text controls visible even when text is not active #[serde(default)] pub show_text_controls: Option, - - /// Deprecated compatibility field. Settings navigation is always reachable. - #[serde(default)] - pub show_settings_section: Option, } /// Mode-specific overrides for toolbar layout presets. diff --git a/src/config/types/toolbar/visibility.rs b/src/config/types/toolbar/visibility.rs index 9835daa7..cd235cf5 100644 --- a/src/config/types/toolbar/visibility.rs +++ b/src/config/types/toolbar/visibility.rs @@ -2,7 +2,7 @@ //! //! Effective visibility = explicit user overrides (`items.shown` / //! `items.hidden`) layered over the layout-mode baseline -//! (`section_defaults()` adjusted by `mode_overrides`). The nine legacy +//! (`section_defaults()` adjusted by `mode_overrides`). The eight legacy //! `show_*` booleans become derived mirrors of this resolver: they are //! still read everywhere and still written to config so configs written by //! this version keep working in older ones, but the overrides are the @@ -89,7 +89,7 @@ pub fn section_flag_for_item(id: ToolbarItemId) -> Option { .find(|flag| flag.item_id() == id) } -/// Effective values for the nine legacy section booleans. +/// Effective values for the eight legacy section booleans. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ToolbarSectionVisibility { pub show_actions_section: bool, @@ -100,7 +100,6 @@ pub struct ToolbarSectionVisibility { pub show_presets: bool, pub show_step_section: bool, pub show_text_controls: bool, - pub show_settings_section: bool, } impl ToolbarSectionVisibility { @@ -174,9 +173,6 @@ pub fn resolve_section_visibility( show_presets: value(ToolbarSectionFlag::Presets), show_step_section: value(ToolbarSectionFlag::StepSection), show_text_controls: value(ToolbarSectionFlag::TextControls), - // Settings is navigation and the only route back to customization. - // The serialized legacy key remains readable, but no longer hides it. - show_settings_section: true, } } @@ -223,6 +219,23 @@ pub fn fold_legacy_section_flags( mod tests { use super::*; + #[test] + fn every_live_section_flag_uses_its_retained_item_id() { + assert_eq!( + ToolbarSectionFlag::ALL.map(|flag| flag.item_id()), + [ + ids::SIDE_GROUP_ACTIONS, + ids::SIDE_GROUP_ACTIONS_ADVANCED, + ids::SIDE_GROUP_ZOOM_ACTIONS, + ids::SIDE_GROUP_PAGES, + ids::SIDE_GROUP_BOARDS, + ids::SIDE_GROUP_PRESETS, + ids::SIDE_GROUP_STEP_UNDO, + ids::SIDE_GROUP_TEXT_CONTROLS, + ] + ); + } + #[test] fn baseline_follows_mode_defaults_and_overrides() { let overrides = ToolbarModeOverrides::default(); @@ -273,7 +286,6 @@ mod tests { show_presets: true, show_step_section: false, show_text_controls: true, - show_settings_section: true, }; let changed = @@ -323,17 +335,4 @@ mod tests { vec!["future.mystery-id".to_string()] ); } - - #[test] - fn settings_visibility_key_is_compatibility_only() { - let mut items = ToolbarItemsConfig::default(); - items.hidden.push(ids::SIDE_GROUP_SETTINGS.to_string()); - - let visibility = resolve_section_visibility( - ToolbarLayoutMode::Simple, - &ToolbarModeOverrides::default(), - &items.resolved(), - ); - assert!(visibility.show_settings_section); - } } diff --git a/src/input/state/actions/action_ui.rs b/src/input/state/actions/action_ui.rs index 85ec6b06..6c6135df 100644 --- a/src/input/state/actions/action_ui.rs +++ b/src/input/state/actions/action_ui.rs @@ -149,21 +149,17 @@ impl InputState { // (focus mode, presenter mode) bypass this path on purpose // and stay run-only. let previous_top_pinned = self.toolbar_top_pinned; - let previous_side_pinned = self.toolbar_side_pinned; self.toolbar_top_pinned = now_visible; - self.toolbar_side_pinned = now_visible; - // Persist only when a pin actually moves. A show from a - // cycle-hidden strip under the pill layout (and a hide - // with both surfaces already unpinned) changes no pin: - // the write would be byte-identical, and its only - // observable effect would be a rollback that re-derives - // pin-true visibility for a screen that was effectively - // hidden before the press. The unfold itself stays - // runtime-only, exactly like F2's hidden rung. - if previous_top_pinned != now_visible || previous_side_pinned != now_visible { + // Persist only when the pin actually moves. A show from a + // cycle-hidden strip changes no pin: the write would be + // byte-identical, and its only observable effect would be + // a rollback that re-derives pin-true visibility for a + // screen that was effectively hidden before the press. The + // unfold itself stays runtime-only, exactly like F2's + // hidden rung. + if previous_top_pinned != now_visible { self.queue_toolbar_persistence(PendingToolbarPersistence::Visibility { previous_top_pinned, - previous_side_pinned, }); } self.pending_onboarding_usage.used_toolbar_toggle = true; @@ -192,9 +188,7 @@ impl InputState { crate::config::TopDisplayMode::Micro => Toast::info("Toolbar: micro"), crate::config::TopDisplayMode::Hidden => { // The hidden rung teaches its own way back: another - // cycle press always lands on Full (unlike - // ToggleToolbar, which would hide a still-visible - // side palette instead of restoring the strip). + // cycle press always lands on Full. let label = match self.action_binding_primary_label(Action::CycleToolbarDisplay) { Some(binding) => format!("Show ({binding})"), diff --git a/src/input/state/core/base/state/init.rs b/src/input/state/core/base/state/init.rs index 2615a278..a8acc45a 100644 --- a/src/input/state/core/base/state/init.rs +++ b/src/input/state/core/base/state/init.rs @@ -164,11 +164,9 @@ impl InputState { light_mode_restore: None, toolbar_visible: true, toolbar_top_visible: true, - toolbar_side_visible: true, fill_enabled, polygon_sides: REGULAR_POLYGON_DEFAULT_SIDES, toolbar_top_pinned: true, - toolbar_side_pinned: true, toolbar_use_icons: true, // Default to icon mode toolbar_scale: 1.0, toolbar_layout_mode: crate::config::ToolbarLayoutMode::Regular, @@ -186,12 +184,7 @@ impl InputState { toolbar_top_display_mode: crate::config::TopDisplayMode::Full, last_draw_activity: std::time::Instant::now(), precision_entry: None, - toolbar_side_minimized: false, - toolbar_side_layout: crate::config::ToolbarSideLayout::Pill, toolbar_rebind_modifier: crate::config::ToolbarRebindModifier::default(), - toolbar_picker_hsv: None, - toolbar_side_pane: crate::ui::toolbar::SidePane::Draw, - toolbar_side_scroll: [0.0; 4], toolbar_customize_items_open: false, toolbar_customize_items_group: None, toolbar_status_bar_contents_open: false, @@ -313,8 +306,6 @@ impl InputState { show_step_section: false, show_text_controls: true, context_aware_ui: true, - show_settings_section: true, - toolbar_collapsed_side_sections: std::collections::BTreeSet::new(), preset_slot_count: PRESET_SLOTS_MAX, presets: vec![None; PRESET_SLOTS_MAX], active_preset_slot: None, diff --git a/src/input/state/core/base/state/structs.rs b/src/input/state/core/base/state/structs.rs index 4089bd28..387a49ed 100644 --- a/src/input/state/core/base/state/structs.rs +++ b/src/input/state/core/base/state/structs.rs @@ -42,9 +42,8 @@ use crate::input::{ }; use crate::render_profiles::RenderProfileSet; use crate::session::SessionOptions; -use crate::ui::toolbar::ToolbarSideSection; use crate::util::Rect; -use std::collections::{BTreeSet, HashMap, VecDeque}; +use std::collections::{HashMap, VecDeque}; use std::path::PathBuf; use std::time::Instant; @@ -54,7 +53,6 @@ pub(crate) struct PresenterRestore { pub(crate) show_tool_preview: Option, pub(crate) toolbar_visible: Option, pub(crate) toolbar_top_visible: Option, - pub(crate) toolbar_side_visible: Option, /// Top-strip form/minimize state before presenter mapped the strip to /// the micro chip (`[presenter_mode] toolbar_mode = "micro"`). pub(crate) toolbar_top_display_mode: Option, @@ -71,7 +69,6 @@ pub(crate) struct FocusModeRestore { pub(crate) show_status_bar: bool, pub(crate) toolbar_visible: bool, pub(crate) toolbar_top_visible: bool, - pub(crate) toolbar_side_visible: bool, pub(crate) toolbar_top_display_mode: crate::config::TopDisplayMode, pub(crate) show_floating_badge: bool, pub(crate) show_zoom_chip: bool, @@ -83,7 +80,6 @@ pub(crate) struct LightModeRestore { pub(crate) show_tool_preview: bool, pub(crate) toolbar_visible: bool, pub(crate) toolbar_top_visible: bool, - pub(crate) toolbar_side_visible: bool, pub(crate) click_highlight_enabled: bool, pub(crate) tool_override: Option, } @@ -274,20 +270,16 @@ pub struct InputState { pub light_mode_drawing: bool, /// Previous UI state to restore after light mode exits pub(crate) light_mode_restore: Option, - /// Whether both toolbars are visible (combined flag, prefer top/side specific) + /// Whether the toolbar is visible (combined flag, prefer `toolbar_top_visible`) pub toolbar_visible: bool, /// Whether the top toolbar panel is visible pub toolbar_top_visible: bool, - /// Whether the side toolbar panel is visible - pub toolbar_side_visible: bool, /// Whether fill is enabled for fill-capable shapes (rect, ellipse) pub fill_enabled: bool, /// Current side count for regular polygon drawing. pub polygon_sides: u8, /// Whether the top toolbar is pinned (saved to config, opens at startup) pub toolbar_top_pinned: bool, - /// Whether the side toolbar is pinned (saved to config, opens at startup) - pub toolbar_side_pinned: bool, /// Whether to use icons instead of text labels in toolbars pub toolbar_use_icons: bool, /// Scale factor for toolbar UI (icons + layout) @@ -329,26 +321,10 @@ pub struct InputState { pub(crate) last_draw_activity: Instant, /// Precise numeric entry popup opened from a pill numeral, when open. pub(crate) precision_entry: Option, - /// Whether the side palette is minimized to its edge restore tab. - pub toolbar_side_minimized: bool, - /// Where the side-palette functions live. Under the default `Pill` - /// layout the side surface never appears; the deprecated `Panel` - /// escape hatch keeps the classic side palette. Startup init applies - /// the config value; this struct field deliberately defaults to - /// `Panel` so side-palette tests exercise the panel without setup. - pub toolbar_side_layout: crate::config::ToolbarSideLayout, /// Modifier chord that turns a toolbar click into shortcut rebinding. /// Used to generate onboarding copy (the tour's rebind hint) without /// hardcoding key strings. Startup init applies the config value. pub toolbar_rebind_modifier: crate::config::ToolbarRebindModifier, - /// Last HSV triple committed from the side palette's color picker; - /// preserves hue/saturation across gray colors where RGB loses them. - pub toolbar_picker_hsv: Option<(f64, f64, f64)>, - /// Whether the toolbar drawer is open - /// Active toolbar drawer tab - pub toolbar_side_pane: crate::ui::toolbar::SidePane, - /// Runtime scroll offsets per side-palette pane (Draw/Canvas/Session/Settings). - pub toolbar_side_scroll: [f64; 4], /// Whether the Settings drawer is showing the toolbar item customization sub-panel pub toolbar_customize_items_open: bool, /// Selected toolbar item customization group in the Settings drawer sub-panel @@ -507,7 +483,7 @@ pub struct InputState { pub custom_section_enabled: bool, /// Whether to show the delay sliders in Actions section pub show_delay_sliders: bool, - /// Whether to show the marker opacity slider in the side toolbar + /// Whether to keep the marker opacity control available in the style pill pub show_marker_opacity_section: bool, /// Whether to show preset action toast notifications pub show_preset_toasts: bool, @@ -638,10 +614,6 @@ pub struct InputState { pub show_text_controls: bool, /// Whether to enable context-aware UI that shows/hides controls based on active tool pub context_aware_ui: bool, - /// Whether to show the Settings section - pub show_settings_section: bool, - /// Side drawer sections whose body content is collapsed for this runtime. - pub toolbar_collapsed_side_sections: BTreeSet, /// Number of preset slots to display pub preset_slot_count: usize, /// Preset slots for quick tool switching diff --git a/src/input/state/core/base/types.rs b/src/input/state/core/base/types.rs index 99d8214f..b4b7b3ff 100644 --- a/src/input/state/core/base/types.rs +++ b/src/input/state/core/base/types.rs @@ -470,13 +470,12 @@ pub enum PendingToolbarPersistence { DisplayMode { previous: crate::config::TopDisplayMode, }, - /// Both pin flags driven by the keyboard visibility toggle (F9). The - /// toggle already applied, so the payload carries the pre-change pins for - /// the runtime-UI preview's rollback; the pin buttons persist via their - /// exact event-policy targets instead. + /// The pin flag driven by the keyboard visibility toggle (F9). The + /// toggle already applied, so the payload carries the pre-change pin for + /// the runtime-UI preview's rollback; the pin button persists via its + /// exact event-policy target instead. Visibility { previous_top_pinned: bool, - previous_side_pinned: bool, }, /// One chrome preference flipped by a direct action -- a keybinding, a /// command-palette entry, a menu command. Each kind is its own variant so diff --git a/src/input/state/core/tool_controls/toolbar.rs b/src/input/state/core/tool_controls/toolbar.rs index eb324ad5..10723639 100644 --- a/src/input/state/core/tool_controls/toolbar.rs +++ b/src/input/state/core/tool_controls/toolbar.rs @@ -1,7 +1,7 @@ use super::super::base::InputState; use crate::config::{ RadialMenuMouseBinding, ToolbarItemId, ToolbarItemOrderGroup, ToolbarItemVisibilitySetting, - ToolbarSideLayout, TopDisplayMode, factory_individual_toolbar_item_visibility_settings, + TopDisplayMode, factory_individual_toolbar_item_visibility_settings, }; use crate::domain::Action; use crate::input::state::{Toast, ToastPriority}; @@ -10,19 +10,16 @@ use crate::input::state::{Toast, ToastPriority}; pub(crate) const CLEAR_UNDO_TOAST_MS: u64 = 2000; impl InputState { - /// Sets toolbar visibility flag (controls both top and side). Returns true if toggled. + /// Sets the toolbar visibility flag. Returns true if toggled. pub fn set_toolbar_visible(&mut self, visible: bool) -> bool { - // Showing must also count a cycle-hidden (F2) top strip as a - // change: under the default pill side layout every raw flag can be - // true while no surface is visible, and the raw-flag comparison - // alone would swallow the restore (F9, the onboarding toast's Show - // action, and the status-bar hint chip all dispatch ToggleToolbar - // into this setter). + // Showing must also count a cycle-hidden (F2) top strip as a change: + // the raw flag can be true while no surface is visible, and the + // raw-flag comparison alone would swallow the restore (F9, the + // onboarding toast's Show action, and the status-bar hint chip all + // dispatch ToggleToolbar into this setter). let unhide_top = visible && self.toolbar_top_display_mode == TopDisplayMode::Hidden; - let any_change = unhide_top - || self.toolbar_visible != visible - || self.toolbar_top_visible != visible - || self.toolbar_side_visible != visible; + let any_change = + unhide_top || self.toolbar_visible != visible || self.toolbar_top_visible != visible; if !any_change { return false; @@ -30,7 +27,6 @@ impl InputState { self.toolbar_visible = visible; self.toolbar_top_visible = visible; - self.toolbar_side_visible = visible; // Showing toolbars always brings the top strip back: a cycle-hidden // strip (F2) reverts to its full form when F9 shows the bars again. if visible && self.toolbar_top_display_mode == TopDisplayMode::Hidden { @@ -41,7 +37,7 @@ impl InputState { true } - /// Re-derive the three live visibility flags from the pin flags — the + /// Re-derive the live visibility flags from the pin flag — the /// same rule startup applies — and refresh the status HUD layout, which /// follows toolbar visibility (see `set_toolbar_visible`). Used when a /// rolled-back visibility toggle hands the pre-toggle pins back: @@ -49,7 +45,6 @@ impl InputState { /// the screen to match what a restart would show. pub(crate) fn derive_toolbar_visibility_from_pins(&mut self) { let top = self.toolbar_top_pinned; - let side = self.toolbar_side_pinned; // A rollback can resolve long after the toggle (a failed write // barrier holds it), by which time a transient chrome owner — // focus mode, presenter mode with `hide_toolbars`, or light mode — @@ -66,8 +61,7 @@ impl InputState { // snapshot exists and there is no restore-order ambiguity. if let Some(restore) = self.focus_mode_restore.as_mut() { restore.toolbar_top_visible = top; - restore.toolbar_side_visible = side; - restore.toolbar_visible = top || side; + restore.toolbar_visible = top; return; } // Presenter tracks toolbar visibility only when `hide_toolbars` @@ -77,19 +71,16 @@ impl InputState { && restore.toolbar_visible.is_some() { restore.toolbar_top_visible = Some(top); - restore.toolbar_side_visible = Some(side); - restore.toolbar_visible = Some(top || side); + restore.toolbar_visible = Some(top); return; } if let Some(restore) = self.light_mode_restore.as_mut() { restore.toolbar_top_visible = top; - restore.toolbar_side_visible = side; - restore.toolbar_visible = top || side; + restore.toolbar_visible = top; return; } self.toolbar_top_visible = top; - self.toolbar_side_visible = side; - self.toolbar_visible = self.toolbar_top_visible || self.toolbar_side_visible; + self.toolbar_visible = top; self.refresh_status_hud_layout(); } @@ -156,52 +147,24 @@ impl InputState { .unwrap_or(self.toolbar_top_display_mode); let restores_top_toolbar = restore.toolbar_top_visible == Some(true) && restored_top_mode != TopDisplayMode::Hidden; - let restores_side_toolbar = restore.toolbar_side_visible == Some(true) - && self.toolbar_side_layout == ToolbarSideLayout::Panel; - restores_status_bar || restores_top_toolbar || restores_side_toolbar + restores_status_bar || restores_top_toolbar } - /// Returns whether any toolbar surface is effectively visible: the top - /// strip (not cycle-hidden) or the side palette (not retired by - /// `side_layout = "pill"`). Raw visibility flags that cannot produce a - /// surface do not count, so the F9 toggle always has a visible effect - /// on its first press. + /// Returns whether the toolbar is effectively visible: the top strip and + /// not cycle-hidden. A raw visibility flag that cannot produce a surface + /// does not count, so the F9 toggle always has a visible effect on its + /// first press. pub fn toolbar_visible(&self) -> bool { - self.toolbar_top_visible() || self.toolbar_side_visible() + self.toolbar_top_visible() } /// Returns whether the top toolbar surface is visible. The cycle - /// action's Hidden display mode hides the top strip without touching - /// the side toolbar. + /// action's Hidden display mode hides the top strip. pub fn toolbar_top_visible(&self) -> bool { self.toolbar_top_visible && self.toolbar_top_display_mode != TopDisplayMode::Hidden } - /// Returns whether the side toolbar is visible. Under the default - /// `side_layout = "pill"` the side palette is retired: its surface never - /// appears (layer-shell, inline fallback, or GTK), regardless of the - /// visibility toggles. The deprecated `"panel"` escape hatch keeps the - /// classic behavior. - /// - /// Invariant: the `InputState` struct-field default matches the config - /// default (`Pill`), so tests exercise the shipping layout unless they - /// opt into the deprecated `Panel` escape hatch explicitly; startup - /// still applies the configured value via - /// [`Self::init_toolbar_side_layout_from_config`]. - pub fn toolbar_side_visible(&self) -> bool { - self.toolbar_side_visible - && self.toolbar_side_layout == crate::config::ToolbarSideLayout::Panel - } - - /// Restore the persisted side layout (called at startup). - pub fn init_toolbar_side_layout_from_config( - &mut self, - layout: crate::config::ToolbarSideLayout, - ) { - self.toolbar_side_layout = layout; - } - /// Store the configured toolbar shortcut-rebind modifier (called at /// startup) so onboarding copy can name the chord without hardcoding keys. pub fn init_toolbar_rebind_modifier_from_config( @@ -219,7 +182,6 @@ impl InputState { mode_overrides: crate::config::ToolbarModeOverrides, items: crate::config::ToolbarItemsConfig, top_pinned: bool, - side_pinned: bool, use_icons: bool, scale: f64, show_more_colors: bool, @@ -232,17 +194,14 @@ impl InputState { show_step_section: bool, show_text_controls: bool, context_aware_ui: bool, - show_settings_section: bool, show_delay_sliders: bool, show_marker_opacity_section: bool, show_preset_toasts: bool, show_tool_preview: bool, ) { self.toolbar_top_pinned = top_pinned; - self.toolbar_side_pinned = side_pinned; self.toolbar_top_visible = top_pinned; - self.toolbar_side_visible = side_pinned; - self.toolbar_visible = top_pinned || side_pinned; + self.toolbar_visible = top_pinned; self.toolbar_use_icons = use_icons; self.toolbar_scale = scale; self.toolbar_layout_mode = layout_mode; @@ -259,7 +218,6 @@ impl InputState { self.show_step_section = show_step_section; self.show_text_controls = show_text_controls; self.context_aware_ui = context_aware_ui; - self.show_settings_section = show_settings_section; self.show_delay_sliders = show_delay_sliders; self.show_marker_opacity_section = show_marker_opacity_section; self.show_preset_toasts = show_preset_toasts; @@ -276,7 +234,6 @@ impl InputState { show_presets: self.show_presets, show_step_section: self.show_step_section, show_text_controls: self.show_text_controls, - show_settings_section: self.show_settings_section, }; legacy.apply_mode_override(self.toolbar_mode_overrides.for_mode(layout_mode)); if crate::config::fold_legacy_section_flags( @@ -307,14 +264,12 @@ impl InputState { self.show_presets = visibility.show_presets; self.show_step_section = visibility.show_step_section; self.show_text_controls = visibility.show_text_controls; - self.show_settings_section = visibility.show_settings_section; } - /// Restore the persisted minimize state of both bars (called at - /// startup). Minimized bars come back as their edge restore tabs. - pub fn init_toolbar_minimized_from_config(&mut self, top: bool, side: bool) { + /// Restore the persisted minimize state of the top strip (called at + /// startup). A minimized strip comes back as its edge restore tab. + pub fn init_toolbar_minimized_from_config(&mut self, top: bool) { self.toolbar_top_minimized = top; - self.toolbar_side_minimized = side; } /// Restore the persisted top-strip display form (called at startup). @@ -373,7 +328,7 @@ impl InputState { fn show_top_strip_surface(&mut self) { self.toolbar_top_visible = true; - self.toolbar_visible = self.toolbar_top_visible || self.toolbar_side_visible; + self.toolbar_visible = true; } /// Advance the top strip through Full → Micro → Hidden → Full and @@ -388,22 +343,6 @@ impl InputState { next } - /// Restore the persisted side-palette pane and collapsed sections - /// (called at startup). Unknown ids are ignored; they are preserved in - /// the config file itself for forward compatibility. - pub fn init_toolbar_side_panes_from_config( - &mut self, - active_pane_id: &str, - collapsed_section_ids: &[String], - ) { - self.toolbar_side_pane = - crate::ui::toolbar::SidePane::from_config_id(active_pane_id).unwrap_or_default(); - self.toolbar_collapsed_side_sections = collapsed_section_ids - .iter() - .filter_map(|id| crate::ui::toolbar::ToolbarSideSection::from_config_id(id)) - .collect(); - } - pub fn set_toolbar_item_hidden(&mut self, id: ToolbarItemId, hidden: bool) -> bool { let before = self.toolbar_items.clone(); self.toolbar_items.set_hidden(id, hidden); @@ -585,7 +524,6 @@ impl InputState { mod tests { use crate::config::{ToolbarItemsConfig, toolbar_item_ids as ids}; use crate::input::state::test_support::make_test_input_state; - use crate::ui::toolbar::{SidePane, ToolbarSideSection}; #[test] fn init_folds_legacy_section_booleans_into_explicit_overrides() { @@ -598,7 +536,6 @@ mod tests { crate::config::ToolbarItemsConfig::default(), true, true, - true, 1.0, false, true, // actions @@ -610,7 +547,6 @@ mod tests { false, // step true, // text controls true, // context aware ui - true, // settings section false, false, true, @@ -630,63 +566,6 @@ mod tests { assert!(!state.show_zoom_actions); } - #[test] - fn pill_side_layout_retires_the_side_surface_and_panel_restores_it() { - let mut state = make_test_input_state(); - // The struct-field default matches the config default (Pill), so - // tests exercise the shipping layout unless they opt into the - // deprecated Panel escape hatch explicitly. - assert_eq!( - state.toolbar_side_layout, - crate::config::ToolbarSideLayout::Pill - ); - assert_eq!( - crate::config::ToolbarSideLayout::default(), - crate::config::ToolbarSideLayout::Pill - ); - - // The default Pill layout retires the side surface: it never - // reports visible, even through the plain visibility toggles. - assert!(!state.toolbar_side_visible()); - state.set_toolbar_visible(true); - assert!(!state.toolbar_side_visible()); - // The top strip is unaffected by the side retirement. - assert!(state.toolbar_top_visible()); - - // The deprecated escape hatch restores the classic behavior. - state.init_toolbar_side_layout_from_config(crate::config::ToolbarSideLayout::Panel); - assert!(state.toolbar_side_visible()); - } - - #[test] - fn side_pane_config_restore_ignores_unknown_ids() { - let mut state = make_test_input_state(); - state.init_toolbar_side_panes_from_config( - "session", - &[ - "colors".to_string(), - "unknown-id".to_string(), - "step-undo".to_string(), - ], - ); - assert_eq!(state.toolbar_side_pane, SidePane::Session); - assert!( - state - .toolbar_collapsed_side_sections - .contains(&ToolbarSideSection::Colors) - ); - assert!( - state - .toolbar_collapsed_side_sections - .contains(&ToolbarSideSection::StepUndo) - ); - assert_eq!(state.toolbar_collapsed_side_sections.len(), 2); - - state.init_toolbar_side_panes_from_config("bogus", &[]); - assert_eq!(state.toolbar_side_pane, SidePane::Draw); - assert!(state.toolbar_collapsed_side_sections.is_empty()); - } - #[test] fn factory_visibility_reset_changes_only_the_centralized_eligible_set() { let mut state = make_test_input_state(); diff --git a/src/input/state/core/tour.rs b/src/input/state/core/tour.rs index e2bb0fae..e74153c5 100644 --- a/src/input/state/core/tour.rs +++ b/src/input/state/core/tour.rs @@ -272,11 +272,9 @@ impl InputState { self.tour_active = false; if !self.presenter_mode || !self.presenter_mode_config.hide_toolbars { let top_visible = self.toolbar_top_pinned; - let side_visible = self.toolbar_side_pinned; - if !self.toolbar_visible() && (top_visible || side_visible) { - self.toolbar_top_visible = top_visible; - self.toolbar_side_visible = side_visible; - self.toolbar_visible = top_visible || side_visible; + if !self.toolbar_visible() && top_visible { + self.toolbar_top_visible = true; + self.toolbar_visible = true; } } self.dirty_tracker.mark_full(); diff --git a/src/input/state/core/utility/focus_mode.rs b/src/input/state/core/utility/focus_mode.rs index f226b55c..de18245a 100644 --- a/src/input/state/core/utility/focus_mode.rs +++ b/src/input/state/core/utility/focus_mode.rs @@ -116,7 +116,6 @@ impl InputState { self.show_status_bar = restore.show_status_bar; self.toolbar_visible = restore.toolbar_visible; self.toolbar_top_visible = restore.toolbar_top_visible; - self.toolbar_side_visible = restore.toolbar_side_visible; self.toolbar_top_display_mode = restore.toolbar_top_display_mode; self.show_floating_badge = restore.show_floating_badge; self.show_zoom_chip = restore.show_zoom_chip; @@ -148,7 +147,6 @@ impl InputState { show_status_bar: self.show_status_bar, toolbar_visible: self.toolbar_visible, toolbar_top_visible: self.toolbar_top_visible, - toolbar_side_visible: self.toolbar_side_visible, toolbar_top_display_mode: self.toolbar_top_display_mode, show_floating_badge: self.show_floating_badge, show_zoom_chip: self.show_zoom_chip, @@ -157,7 +155,6 @@ impl InputState { // comes back as micro on restore. self.toolbar_visible = false; self.toolbar_top_visible = false; - self.toolbar_side_visible = false; self.show_status_bar = false; self.show_floating_badge = false; self.show_zoom_chip = false; diff --git a/src/input/state/core/utility/light_mode.rs b/src/input/state/core/utility/light_mode.rs index aa77dcbd..95f5f5c2 100644 --- a/src/input/state/core/utility/light_mode.rs +++ b/src/input/state/core/utility/light_mode.rs @@ -117,7 +117,6 @@ impl InputState { self.show_tool_preview = restore.show_tool_preview; self.toolbar_visible = restore.toolbar_visible; self.toolbar_top_visible = restore.toolbar_top_visible; - self.toolbar_side_visible = restore.toolbar_side_visible; self.set_tool_override(restore.tool_override); if self.click_highlight_enabled() != restore.click_highlight_enabled { self.toggle_click_highlight(); @@ -156,7 +155,6 @@ impl InputState { show_tool_preview: self.show_tool_preview, toolbar_visible: self.toolbar_visible, toolbar_top_visible: self.toolbar_top_visible, - toolbar_side_visible: self.toolbar_side_visible, click_highlight_enabled: self.click_highlight_enabled(), tool_override: self.tool_override(), }); @@ -165,7 +163,6 @@ impl InputState { self.show_tool_preview = false; self.toolbar_visible = false; self.toolbar_top_visible = false; - self.toolbar_side_visible = false; self.set_tool_override(Some(Tool::Pen)); if self.click_highlight_forced_in_light_mode() && !self.click_highlight_enabled() { self.toggle_click_highlight(); diff --git a/src/input/state/core/utility/pending.rs b/src/input/state/core/utility/pending.rs index dcd533fd..126295d5 100644 --- a/src/input/state/core/utility/pending.rs +++ b/src/input/state/core/utility/pending.rs @@ -56,11 +56,7 @@ impl InputState { } PendingToolbarPersistence::Visibility { previous_top_pinned, - previous_side_pinned, - } => { - previous_top_pinned != self.toolbar_top_pinned - || previous_side_pinned != self.toolbar_side_pinned - } + } => previous_top_pinned != self.toolbar_top_pinned, PendingToolbarPersistence::StatusBar { previous } => previous != self.show_status_bar, PendingToolbarPersistence::FloatingBadge { previous } => { previous != self.show_floating_badge diff --git a/src/input/state/core/utility/presenter_mode.rs b/src/input/state/core/utility/presenter_mode.rs index 08174ccf..83d0bf07 100644 --- a/src/input/state/core/utility/presenter_mode.rs +++ b/src/input/state/core/utility/presenter_mode.rs @@ -30,9 +30,6 @@ impl InputState { if let Some(value) = restore.toolbar_top_visible { self.toolbar_top_visible = value; } - if let Some(value) = restore.toolbar_side_visible { - self.toolbar_side_visible = value; - } if let Some(value) = restore.toolbar_top_display_mode { self.toolbar_top_display_mode = value; } @@ -79,7 +76,6 @@ impl InputState { show_tool_preview: None, toolbar_visible: None, toolbar_top_visible: None, - toolbar_side_visible: None, toolbar_top_display_mode: None, toolbar_top_minimized: None, click_highlight_enabled: None, @@ -103,19 +99,15 @@ impl InputState { if config.hide_toolbars { restore.toolbar_visible = Some(self.toolbar_visible); restore.toolbar_top_visible = Some(self.toolbar_top_visible); - restore.toolbar_side_visible = Some(self.toolbar_side_visible); match config.toolbar_mode { crate::config::PresenterToolbarMode::Hidden => { self.toolbar_visible = false; self.toolbar_top_visible = false; - self.toolbar_side_visible = false; } crate::config::PresenterToolbarMode::Micro => { - // The top strip stays up as the micro chip; side (and - // bottom) toolbars keep the hidden behavior. + // The top strip stays up as the micro chip. restore.toolbar_top_display_mode = Some(self.toolbar_top_display_mode); restore.toolbar_top_minimized = Some(self.toolbar_top_minimized); - self.toolbar_side_visible = false; self.set_top_display_mode(crate::config::TopDisplayMode::Micro); } } diff --git a/src/input/state/tests/basics.rs b/src/input/state/tests/basics.rs index 16b295cd..70cfb4fe 100644 --- a/src/input/state/tests/basics.rs +++ b/src/input/state/tests/basics.rs @@ -225,54 +225,6 @@ fn test_adjust_font_size_multiple_adjustments() { assert_eq!(state.current_font_size, 34.0); } -#[test] -fn toolbar_toggle_handles_partial_visibility() { - let mut state = create_test_input_state(); - // Partial visibility is a side-palette scenario: opt into the - // deprecated Panel escape hatch (the struct default is Pill). - state.init_toolbar_side_layout_from_config(crate::config::ToolbarSideLayout::Panel); - // Simulate config: top pinned, side not pinned - state.init_toolbar_from_config( - crate::config::ToolbarLayoutMode::Regular, - crate::config::ToolbarModeOverrides::default(), - crate::config::ToolbarItemsConfig::default(), - true, // top_pinned - false, // side_pinned - true, // use_icons - 1.0, // scale - false, // show_more_colors - true, // show_actions_section - false, // show_actions_advanced - true, // show_zoom_actions - true, // show_pages_section - true, // show_boards_section - true, // show_presets - false, // show_step_section - false, // show_text_controls - true, // context_aware_ui - true, // show_settings_section - false, // show_delay_sliders - false, // show_marker_opacity_section - true, // show_preset_toasts - false, // show_tool_preview - ); - assert!(state.toolbar_top_visible()); - assert!(!state.toolbar_side_visible()); - assert!(state.toolbar_visible()); - - // Toggle off - let _ = state.set_toolbar_visible(!state.toolbar_visible()); - assert!(!state.toolbar_visible()); - assert!(!state.toolbar_top_visible()); - assert!(!state.toolbar_side_visible()); - - // Toggle on - let _ = state.set_toolbar_visible(!state.toolbar_visible()); - assert!(state.toolbar_visible()); - assert!(state.toolbar_top_visible()); - assert!(state.toolbar_side_visible()); -} - /// Each chrome action queues its own durable entry carrying the value it /// replaced, so persistence does not depend on which handler dispatched it -- /// a keybinding, a command-palette click, or a menu command all reach the diff --git a/src/input/state/tests/light_mode.rs b/src/input/state/tests/light_mode.rs index f1c97b77..2d7409c5 100644 --- a/src/input/state/tests/light_mode.rs +++ b/src/input/state/tests/light_mode.rs @@ -22,7 +22,6 @@ fn light_mode_enters_passthrough_and_hides_heavy_ui() { state.show_status_bar = true; state.toolbar_visible = true; state.toolbar_top_visible = true; - state.toolbar_side_visible = true; state.show_tool_preview = true; state.set_tool_override(Some(Tool::Arrow)); @@ -34,7 +33,6 @@ fn light_mode_enters_passthrough_and_hides_heavy_ui() { assert!(!state.show_status_bar); assert!(!state.toolbar_visible); assert!(!state.toolbar_top_visible); - assert!(!state.toolbar_side_visible); assert!(!state.show_tool_preview); assert_eq!(state.tool_override(), Some(Tool::Pen)); } @@ -73,7 +71,6 @@ fn light_mode_restores_previous_ui_and_tool_on_exit() { state.show_status_bar = true; state.toolbar_visible = true; state.toolbar_top_visible = false; - state.toolbar_side_visible = true; state.show_tool_preview = true; state.set_tool_override(Some(Tool::Marker)); @@ -86,7 +83,6 @@ fn light_mode_restores_previous_ui_and_tool_on_exit() { assert!(state.show_status_bar); assert!(state.toolbar_visible); assert!(!state.toolbar_top_visible); - assert!(state.toolbar_side_visible); assert!(state.show_tool_preview); assert_eq!(state.tool_override(), Some(Tool::Marker)); } diff --git a/src/input/state/tests/modal.rs b/src/input/state/tests/modal.rs index f35faa2c..45b9d6b7 100644 --- a/src/input/state/tests/modal.rs +++ b/src/input/state/tests/modal.rs @@ -85,10 +85,8 @@ fn every_opener_ends_the_tour() { fn an_opener_that_ends_the_tour_restores_pinned_chrome() { let mut state = create_test_input_state(); state.toolbar_top_pinned = true; - state.toolbar_side_pinned = true; state.start_tour(); state.toolbar_top_visible = false; - state.toolbar_side_visible = false; state.toolbar_visible = false; state.toggle_command_palette(); diff --git a/src/input/state/tests/presenter_mode.rs b/src/input/state/tests/presenter_mode.rs index 0872995a..9030ff91 100644 --- a/src/input/state/tests/presenter_mode.rs +++ b/src/input/state/tests/presenter_mode.rs @@ -127,7 +127,6 @@ fn presenter_mode_restores_status_bar_toolbars_and_tool_override_on_exit() { state.show_status_bar = true; state.toolbar_visible = true; state.toolbar_top_visible = true; - state.toolbar_side_visible = true; state.set_tool_override(Some(Tool::Arrow)); state.toggle_presenter_mode(); @@ -140,7 +139,6 @@ fn presenter_mode_restores_status_bar_toolbars_and_tool_override_on_exit() { assert!(state.show_status_bar); assert!(state.toolbar_visible); assert!(state.toolbar_top_visible); - assert!(state.toolbar_side_visible); assert_eq!(state.tool_override(), Some(Tool::Arrow)); } @@ -149,14 +147,10 @@ fn presenter_micro_mapping_shows_the_chip_and_restores_on_exit() { use crate::config::{PresenterToolbarMode, TopDisplayMode}; let mut state = create_test_input_state(); - // Side-palette assertions below need the deprecated Panel escape hatch - // (the struct default is Pill, which retires the side surface). - state.init_toolbar_side_layout_from_config(crate::config::ToolbarSideLayout::Panel); state.presenter_mode_config.hide_toolbars = true; state.presenter_mode_config.toolbar_mode = PresenterToolbarMode::Micro; state.toolbar_visible = true; state.toolbar_top_visible = true; - state.toolbar_side_visible = true; state.toolbar_top_minimized = true; state.toggle_presenter_mode(); @@ -170,8 +164,6 @@ fn presenter_micro_mapping_shows_the_chip_and_restores_on_exit() { !state.toolbar_top_minimized, "the chip replaces the restore tab" ); - assert!(!state.toolbar_side_visible(), "side toolbars still hide"); - state.toggle_presenter_mode(); assert!(!state.presenter_mode); assert_eq!(state.toolbar_top_display_mode, TopDisplayMode::Full); @@ -179,7 +171,6 @@ fn presenter_micro_mapping_shows_the_chip_and_restores_on_exit() { state.toolbar_top_minimized, "minimize state restored on exit" ); - assert!(state.toolbar_side_visible()); } #[test] @@ -192,7 +183,6 @@ fn presenter_hidden_mapping_keeps_todays_behavior() { state.toggle_presenter_mode(); assert!(!state.toolbar_top_visible()); - assert!(!state.toolbar_side_visible()); assert_eq!(state.toolbar_top_display_mode, TopDisplayMode::Full); } diff --git a/src/input/state/tests/status_hud.rs b/src/input/state/tests/status_hud.rs index 22f01e39..aa2fb45d 100644 --- a/src/input/state/tests/status_hud.rs +++ b/src/input/state/tests/status_hud.rs @@ -291,16 +291,15 @@ fn status_hud_click_toolbar_hint_returns_toggle_toolbar_action() { assert!(!input.is_radial_menu_open()); } -/// End-to-end recovery in the shipping default state: pill side layout -/// (side palette retired) with the top strip F2-cycled to Hidden leaves -/// every raw visibility flag true while no surface is visible. The hint +/// End-to-end recovery in the shipping top-only state: F2-cycling the top +/// strip to Hidden leaves every raw visibility flag true while no surface is +/// visible. The hint /// chip must appear there, and dispatching its returned action must /// actually restore the toolbar — the advertised recovery cannot be a /// no-op. #[test] -fn status_hud_toolbar_hint_recovers_cycle_hidden_strip_under_pill_layout() { +fn status_hud_toolbar_hint_recovers_cycle_hidden_top_strip() { let mut input = create_test_input_state(); - input.init_toolbar_side_layout_from_config(crate::config::ToolbarSideLayout::Pill); input.handle_action(Action::CycleToolbarDisplay); // micro input.handle_action(Action::CycleToolbarDisplay); // hidden assert!(!input.toolbar_visible()); diff --git a/src/input/state/tests/toolbar_display.rs b/src/input/state/tests/toolbar_display.rs index 283402cb..fe5fabe4 100644 --- a/src/input/state/tests/toolbar_display.rs +++ b/src/input/state/tests/toolbar_display.rs @@ -32,10 +32,9 @@ fn refresh_status_hud_layout(state: &mut InputState) { #[test] fn cycle_action_walks_full_micro_hidden_full_with_toasts() { let mut state = create_test_input_state(); - // The "cycle only affects the top strip" assertion below observes the - // side palette: opt into the deprecated Panel escape hatch (the struct - // default is Pill, which retires the side surface). - state.init_toolbar_side_layout_from_config(crate::config::ToolbarSideLayout::Panel); + // Keep the status bar up so the hidden rung shows its routine toast + // instead of the all-chrome-hidden recovery warning. + refresh_status_hud_layout(&mut state); assert_eq!(state.top_display_state(), TopDisplayMode::Full); state.handle_action(Action::CycleToolbarDisplay); @@ -59,10 +58,6 @@ fn cycle_action_walks_full_micro_hidden_full_with_toasts() { state.handle_action(Action::CycleToolbarDisplay); assert_eq!(state.top_display_state(), TopDisplayMode::Hidden); assert!(!state.toolbar_top_visible()); - assert!( - state.toolbar_side_visible(), - "the cycle only affects the top strip" - ); assert_eq!( state.ui_toast.as_ref().map(|toast| toast.message.as_str()), Some("Toolbar: hidden") @@ -97,35 +92,15 @@ fn entering_micro_unminimizes_and_closes_top_menus() { #[test] fn toggle_toolbar_show_restores_a_cycle_hidden_top_strip() { let mut state = create_test_input_state(); - // Two-press semantics are Panel-specific (the still-visible side - // palette makes the first press a hide); the single-press Pill variant - // is covered below. - state.init_toolbar_side_layout_from_config(crate::config::ToolbarSideLayout::Panel); - state.handle_action(Action::CycleToolbarDisplay); // micro - state.handle_action(Action::CycleToolbarDisplay); // hidden - assert!(!state.toolbar_top_visible()); - - state.handle_action(Action::ToggleToolbar); // hides side too - state.handle_action(Action::ToggleToolbar); // shows everything - assert!(state.toolbar_top_visible()); - assert_eq!(state.top_display_state(), TopDisplayMode::Full); -} - -#[test] -fn toggle_toolbar_restores_a_cycle_hidden_strip_under_pill_layout() { - let mut state = create_test_input_state(); - // The shipping default: the side palette is retired by the pill layout, - // so a cycle-hidden top strip leaves NO visible toolbar surface while - // every raw visibility flag stays true. The raw-flag early return in - // set_toolbar_visible used to swallow the restore in exactly this - // state, leaving F9 (and everything else dispatching ToggleToolbar) - // dead. - state.init_toolbar_side_layout_from_config(crate::config::ToolbarSideLayout::Pill); + // A cycle-hidden top strip leaves no visible toolbar surface while every + // raw visibility flag stays true. The raw-flag early return in + // set_toolbar_visible used to swallow the restore in exactly this state, + // leaving F9 (and everything else dispatching ToggleToolbar) dead. state.handle_action(Action::CycleToolbarDisplay); // micro state.handle_action(Action::CycleToolbarDisplay); // hidden assert!( !state.toolbar_visible(), - "cycle-hidden under pill must leave no visible surface" + "a cycle-hidden strip must leave no visible surface" ); // A single ToggleToolbar press must bring the strip back. @@ -135,69 +110,62 @@ fn toggle_toolbar_restores_a_cycle_hidden_strip_under_pill_layout() { } #[test] -fn toggle_toolbar_drives_both_pins_and_queues_their_persistence() { +fn toggle_toolbar_drives_the_top_pin_and_queues_its_persistence() { let mut state = create_test_input_state(); assert!(state.toolbar_visible()); - assert!(state.toolbar_top_pinned && state.toolbar_side_pinned); + assert!(state.toolbar_top_pinned); - // F9 hide: the durable form of the toggle unpins both surfaces, and the - // pending action carries the pre-change pins for the preview's rollback. + // F9 hide: the durable form of the toggle unpins the strip, and the + // pending action carries the pre-change pin for the preview's rollback. state.handle_action(Action::ToggleToolbar); assert!(!state.toolbar_visible()); assert!(!state.toolbar_top_pinned); - assert!(!state.toolbar_side_pinned); assert_eq!( state.take_pending_toolbar_persistence(), vec![PendingToolbarPersistence::Visibility { previous_top_pinned: true, - previous_side_pinned: true, }], "the keyboard toggle persists like the toolbar-event paths" ); - // F9 show: both pins come back on, with the hidden state as rollback. + // F9 show: the pin comes back on, with the hidden state as rollback. state.handle_action(Action::ToggleToolbar); assert!(state.toolbar_visible()); assert!(state.toolbar_top_pinned); - assert!(state.toolbar_side_pinned); assert_eq!( state.take_pending_toolbar_persistence(), vec![PendingToolbarPersistence::Visibility { previous_top_pinned: false, - previous_side_pinned: false, }] ); } -/// F9 is WYSIWYG, not a pin round-trip: an asymmetric pin start (top pinned, -/// side not) ends symmetric after off + on, because the second press must -/// restore exactly the all-visible state the user was looking at. +/// F9 is WYSIWYG, not a pin round-trip: the second press must restore exactly +/// the visible state the user was looking at. #[test] -fn toggle_toolbar_resolves_asymmetric_pins_to_what_is_on_screen() { +fn toggle_toolbar_resolves_pins_to_what_is_on_screen() { let mut state = create_test_input_state(); - state.toolbar_side_pinned = false; assert!(state.toolbar_visible()); state.handle_action(Action::ToggleToolbar); // off - assert!(!state.toolbar_top_pinned && !state.toolbar_side_pinned); + assert!(!state.toolbar_top_pinned); assert_eq!( state.take_pending_toolbar_persistence(), vec![PendingToolbarPersistence::Visibility { previous_top_pinned: true, - previous_side_pinned: false, }] ); state.handle_action(Action::ToggleToolbar); // on assert!(state.toolbar_visible()); assert!( - state.toolbar_top_pinned && state.toolbar_side_pinned, - "showing pins both surfaces so the next start matches the screen" + state.toolbar_top_pinned, + "showing pins the strip so the next start matches the screen" ); } -/// Under the shipping pill layout a cycle-hidden strip leaves no visible -/// surface while both pins stay true, so F9 there is a SHOW whose pin values +/// A cycle-hidden strip leaves no visible surface while the pin stays true, +/// so F9 there is a SHOW whose pin value /// do not change. Nothing may be queued for persistence: the write would be /// byte-identical, and its rollback would re-derive pin-true visibility for /// a screen that was effectively hidden before the press. The unfold stays @@ -205,17 +173,16 @@ fn toggle_toolbar_resolves_asymmetric_pins_to_what_is_on_screen() { #[test] fn cycle_hidden_show_with_unchanged_pins_queues_no_persistence() { let mut state = create_test_input_state(); - state.init_toolbar_side_layout_from_config(crate::config::ToolbarSideLayout::Pill); state.handle_action(Action::CycleToolbarDisplay); // micro state.handle_action(Action::CycleToolbarDisplay); // hidden assert!(!state.toolbar_visible()); - assert!(state.toolbar_top_pinned && state.toolbar_side_pinned); + assert!(state.toolbar_top_pinned); state.take_pending_toolbar_persistence(); // drain the cycle's display-mode write state.handle_action(Action::ToggleToolbar); // show: unfolds Hidden → Full assert!(state.toolbar_visible()); assert_eq!(state.top_display_state(), TopDisplayMode::Full); - assert!(state.toolbar_top_pinned && state.toolbar_side_pinned); + assert!(state.toolbar_top_pinned); assert!( !state.has_pending_toolbar_persistence(), "a toggle that moves no pin queues nothing (the raw queue, not the \ @@ -223,20 +190,19 @@ fn cycle_hidden_show_with_unchanged_pins_queues_no_persistence() { ); } -/// The hide-side twin: both surfaces already unpinned via the pin buttons -/// while still visible, then F9. The hide moves no pin — the false pins -/// already persist the hidden restart state — so the toggle needs no +/// The hide twin: the strip is already unpinned via the pin button while +/// still visible, then F9. The hide moves no pin — the false pin already +/// persists the hidden restart state — so the toggle needs no /// additional persistence and queues nothing. #[test] -fn hide_with_already_unpinned_surfaces_queues_no_persistence() { +fn hide_with_an_already_unpinned_strip_queues_no_persistence() { let mut state = create_test_input_state(); state.toolbar_top_pinned = false; - state.toolbar_side_pinned = false; assert!(state.toolbar_visible()); state.handle_action(Action::ToggleToolbar); // hide assert!(!state.toolbar_visible()); - assert!(!state.toolbar_top_pinned && !state.toolbar_side_pinned); + assert!(!state.toolbar_top_pinned); assert!( !state.has_pending_toolbar_persistence(), "a toggle that moves no pin queues nothing (the raw queue, not the \ @@ -252,7 +218,7 @@ fn presenter_swallowed_toggle_leaves_pins_and_persistence_untouched() { assert!(!state.toolbar_visible()); state.handle_action(Action::ToggleToolbar); - assert!(state.toolbar_top_pinned && state.toolbar_side_pinned); + assert!(state.toolbar_top_pinned); assert!( !state.has_pending_toolbar_persistence(), "a swallowed toggle must queue nothing (the raw queue, not the \ @@ -276,7 +242,7 @@ fn focus_and_presenter_transitions_never_queue_pin_persistence() { ] { state.handle_action(action); assert!( - state.toolbar_top_pinned && state.toolbar_side_pinned, + state.toolbar_top_pinned, "{action:?} must not touch the pin overrides" ); assert!( @@ -298,7 +264,7 @@ fn focus_and_presenter_transitions_never_queue_pin_persistence() { #[test] fn a_toggle_and_a_cycle_in_one_batch_both_keep_their_persistence() { let mut state = create_test_input_state(); - assert!(state.toolbar_top_pinned && state.toolbar_side_pinned); + assert!(state.toolbar_top_pinned); state.handle_action(Action::CycleToolbarDisplay); // micro state.take_pending_toolbar_persistence(); // drain the setup cycle's write @@ -310,7 +276,6 @@ fn a_toggle_and_a_cycle_in_one_batch_both_keep_their_persistence() { vec![ PendingToolbarPersistence::Visibility { previous_top_pinned: true, - previous_side_pinned: true, }, PendingToolbarPersistence::DisplayMode { previous: TopDisplayMode::Micro, @@ -338,7 +303,6 @@ fn visibility_persistence_survives_a_capture_request() { state.take_pending_toolbar_persistence(), vec![PendingToolbarPersistence::Visibility { previous_top_pinned: true, - previous_side_pinned: true, }], "the toggle must survive the capture" ); @@ -355,7 +319,6 @@ fn visibility_persistence_survives_a_capture_request() { state.take_pending_toolbar_persistence(), vec![PendingToolbarPersistence::Visibility { previous_top_pinned: false, - previous_side_pinned: false, }], "the toggle must survive the capture" ); @@ -368,11 +331,11 @@ fn visibility_persistence_survives_a_capture_request() { #[test] fn a_toggle_burst_coalesces_to_the_original_rollback_baseline() { let mut state = create_test_input_state(); - assert!(state.toolbar_top_pinned && state.toolbar_side_pinned); + assert!(state.toolbar_top_pinned); state.handle_action(Action::ToggleToolbar); // hide state.handle_action(Action::ToggleToolbar); // show: pins back where they started - assert!(state.toolbar_top_pinned && state.toolbar_side_pinned); + assert!(state.toolbar_top_pinned); assert!( state.has_pending_toolbar_persistence(), @@ -399,7 +362,6 @@ fn an_exit_request_does_not_clear_queued_toolbar_persistence() { state.take_pending_toolbar_persistence(), vec![PendingToolbarPersistence::Visibility { previous_top_pinned: true, - previous_side_pinned: true, }], "the toggle must still be waiting for the teardown drain" ); @@ -428,9 +390,7 @@ fn hidden_cycle_toast_offers_a_show_action() { assert_eq!(toast.message, "Toolbar: hidden"); let action = toast.action.as_ref().expect("show action chip"); assert_eq!(action.label, "Show (F2)"); - // Another cycle press from Hidden always lands on Full — unlike - // ToggleToolbar, which under the Panel escape hatch would hide a - // still-visible side palette instead. + // Another cycle press from Hidden always lands on Full. assert_eq!(action.action, Action::CycleToolbarDisplay); } @@ -438,7 +398,7 @@ fn hidden_cycle_toast_offers_a_show_action() { fn hiding_the_last_chrome_surface_warns_with_recovery_bindings() { let mut state = create_test_input_state(); refresh_status_hud_layout(&mut state); - // Pill default: F9 alone hides every toolbar surface. The status bar is + // F9 alone hides every toolbar surface. The status bar is // still up, so its hint chip covers recovery — no warning yet. state.handle_action(Action::ToggleToolbar); assert!( diff --git a/src/input/tool/profile.rs b/src/input/tool/profile.rs index 5b19bc81..f64b6478 100644 --- a/src/input/tool/profile.rs +++ b/src/input/tool/profile.rs @@ -46,7 +46,7 @@ pub(crate) enum ToolSizeSource { EraserSize, } -/// Side-toolbar control family exposed by a tool. +/// Contextual style-pill control family exposed by a tool. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ToolControlGroup { None, diff --git a/src/label_format.rs b/src/label_format.rs index 1dc0bfee..5b25703c 100644 --- a/src/label_format.rs +++ b/src/label_format.rs @@ -27,8 +27,8 @@ pub(crate) fn format_binding_label(label: &str, binding: Option<&str>) -> String } /// Quick-color swatch tooltip: the slot label with its shortcut, plus the -/// secondary-click recolor hint. Shared by the style pill and both frontends' -/// side palettes so the gesture is advertised identically everywhere. +/// secondary-click recolor hint. Shared by both top-toolbar frontends so the +/// gesture is advertised identically everywhere. #[allow(dead_code)] pub(crate) fn format_quick_color_tooltip(label: &str, binding: Option<&str>) -> String { format!("{} — {RECOLOR_HINT}", format_binding_label(label, binding)) diff --git a/src/runtime_ui_state/store/tests.rs b/src/runtime_ui_state/store/tests.rs index 1573a495..4bd49df8 100644 --- a/src/runtime_ui_state/store/tests.rs +++ b/src/runtime_ui_state/store/tests.rs @@ -17,7 +17,7 @@ fn seeds() -> ValidatedInteractionSeeds { .unwrap(); seeds .insert( - InteractionSeedTarget::SidePinned, + InteractionSeedTarget::TopMinimized, InteractionSeedValue::Bool(false), ) .unwrap(); @@ -431,11 +431,11 @@ future_boards = true .controller; let permit = controller .begin_mutation(RuntimeUiMutationScope::one( - InteractionSeedTarget::SidePinned, + InteractionSeedTarget::TopMinimized, )) .unwrap(); let values = RuntimeUiMutationValues::one( - InteractionSeedTarget::SidePinned, + InteractionSeedTarget::TopMinimized, InteractionSeedValue::Bool(true), ) .unwrap(); @@ -959,7 +959,7 @@ future_entry = "must not return" .unwrap(); changed_seeds .insert( - InteractionSeedTarget::SidePinned, + InteractionSeedTarget::TopMinimized, InteractionSeedValue::Bool(false), ) .unwrap(); diff --git a/src/runtime_ui_state/tests.rs b/src/runtime_ui_state/tests.rs index 106882e3..4ee8e905 100644 --- a/src/runtime_ui_state/tests.rs +++ b/src/runtime_ui_state/tests.rs @@ -49,7 +49,7 @@ fn invalid_observation(label: &str) -> RuntimeStateSourceObservation { } } -fn test_seeds(top_pinned: bool, side_pinned: bool) -> ValidatedInteractionSeeds { +fn test_seeds(top_pinned: bool, top_minimized: bool) -> ValidatedInteractionSeeds { let mut seeds = ValidatedInteractionSeeds::new(); seeds .insert( @@ -59,8 +59,8 @@ fn test_seeds(top_pinned: bool, side_pinned: bool) -> ValidatedInteractionSeeds .unwrap(); seeds .insert( - InteractionSeedTarget::SidePinned, - InteractionSeedValue::Bool(side_pinned), + InteractionSeedTarget::TopMinimized, + InteractionSeedValue::Bool(top_minimized), ) .unwrap(); seeds @@ -85,12 +85,6 @@ fn test_seeds(top_pinned: bool, side_pinned: bool) -> ValidatedInteractionSeeds ) .unwrap(); seeds - .insert( - InteractionSeedTarget::SidePosition, - InteractionSeedValue::Position(ToolbarPositionSeed::new(30.0, 40.0).unwrap()), - ) - .unwrap(); - seeds } fn controller() -> RuntimeUiStateController { diff --git a/src/runtime_ui_state/tests/permits.rs b/src/runtime_ui_state/tests/permits.rs index c828333b..f4922181 100644 --- a/src/runtime_ui_state/tests/permits.rs +++ b/src/runtime_ui_state/tests/permits.rs @@ -8,7 +8,7 @@ fn coalesced_a_to_b_to_a_reload_invalidates_pre_barrier_permit() { InteractionSeedTarget::TopPinned, )) .unwrap(); - commit_bool(&mut controller, InteractionSeedTarget::SidePinned, true); + commit_bool(&mut controller, InteractionSeedTarget::TopMinimized, true); let (_, incident) = fail_current_replace(&mut controller, "temporary"); assert!(matches!( controller.update_seeds(test_seeds(true, false)), @@ -205,7 +205,7 @@ fn atomic_batch_rejects_when_one_guard_changes() { let permit = controller .begin_mutation(RuntimeUiMutationScope::batch([ InteractionSeedTarget::TopPinned, - InteractionSeedTarget::SidePinned, + InteractionSeedTarget::TopMinimized, ])) .unwrap(); controller.update_seeds(test_seeds(false, true)); @@ -218,7 +218,7 @@ fn atomic_batch_rejects_when_one_guard_changes() { InteractionSeedValue::Bool(true), ), ( - InteractionSeedTarget::SidePinned, + InteractionSeedTarget::TopMinimized, InteractionSeedValue::Bool(false), ), ]) @@ -258,7 +258,7 @@ fn removed_and_reused_board_target_rejects_old_permit() { } #[test] -fn position_permit_is_rejected_only_by_relevant_seed_reload() { +fn position_permit_survives_an_unrelated_seed_reload() { let mut controller = controller(); let top = controller .begin_mutation(RuntimeUiMutationScope::one( @@ -277,38 +277,4 @@ fn position_permit_is_rejected_only_by_relevant_seed_reload() { ), CommitResult::Accepted { .. } )); - - // A side drag also owns the reconciled top offset, so a reload of either - // position seed invalidates its permit. - let side = controller - .begin_mutation(RuntimeUiMutationScope::batch([ - InteractionSeedTarget::TopPosition, - InteractionSeedTarget::SidePosition, - ])) - .unwrap(); - let mut changed = test_seeds(false, true); - changed - .insert( - InteractionSeedTarget::SidePosition, - InteractionSeedValue::Position(ToolbarPositionSeed::new(31.0, 41.0).unwrap()), - ) - .unwrap(); - controller.update_seeds(changed); - assert!(matches!( - controller.commit( - side, - RuntimeUiMutationValues::batch([ - ( - InteractionSeedTarget::TopPosition, - InteractionSeedValue::Position(ToolbarPositionSeed::new(12.0, 22.0).unwrap()), - ), - ( - InteractionSeedTarget::SidePosition, - InteractionSeedValue::Position(ToolbarPositionSeed::new(32.0, 42.0).unwrap()), - ), - ]) - .unwrap(), - ), - CommitResult::RejectedSeedChanged { .. } - )); } diff --git a/src/runtime_ui_state/tests/pipeline.rs b/src/runtime_ui_state/tests/pipeline.rs index 45a8dae1..8a144c2b 100644 --- a/src/runtime_ui_state/tests/pipeline.rs +++ b/src/runtime_ui_state/tests/pipeline.rs @@ -175,7 +175,7 @@ fn rapid_non_coalesced_writes_chain_acknowledged_revisions() { let mut controller = controller(); let first = commit_bool(&mut controller, InteractionSeedTarget::TopPinned, true); let request_a = controller.take_source_mutation().expect("first request"); - let second = commit_bool(&mut controller, InteractionSeedTarget::SidePinned, true); + let second = commit_bool(&mut controller, InteractionSeedTarget::TopMinimized, true); assert!(controller.take_source_mutation().is_none()); let revision_a = present_revision("r1"); @@ -201,8 +201,8 @@ fn staged_replacements_coalesce_but_flush_waits_for_the_latest_snapshot() { let mut controller = controller(); let first = commit_bool(&mut controller, InteractionSeedTarget::TopPinned, true); let request_a = controller.take_source_mutation().unwrap(); - let second = commit_bool(&mut controller, InteractionSeedTarget::SidePinned, true); - let third = commit_bool(&mut controller, InteractionSeedTarget::SidePinned, false); + let second = commit_bool(&mut controller, InteractionSeedTarget::TopMinimized, true); + let third = commit_bool(&mut controller, InteractionSeedTarget::TopMinimized, false); let flush = controller.request_flush(third).unwrap(); assert!(controller.flush_outcome(flush).is_none()); @@ -254,7 +254,7 @@ fn completed_receipts_and_flushes_can_be_consumed_without_losing_flush_history() ); assert!(controller.flush_outcome(durable_flush).is_none()); - let second = commit_bool(&mut controller, InteractionSeedTarget::SidePinned, true); + let second = commit_bool(&mut controller, InteractionSeedTarget::TopMinimized, true); let second_request = controller.take_source_mutation().unwrap(); let barrier = match controller.submit_source_mutation(SourceMutationResult::SourceChangedBeforeMutation { @@ -297,7 +297,7 @@ fn flush_is_rejected_while_external_authority_reconciliation_is_active() { &persisted_request, present_revision("persisted-before-conflict"), ); - commit_bool(&mut controller, InteractionSeedTarget::SidePinned, true); + commit_bool(&mut controller, InteractionSeedTarget::TopMinimized, true); let conflicting_request = controller.take_source_mutation().unwrap(); let barrier = match controller.submit_source_mutation(SourceMutationResult::SourceChangedBeforeMutation { diff --git a/src/runtime_ui_state/tests/recovery_cleanup.rs b/src/runtime_ui_state/tests/recovery_cleanup.rs index 107d11b5..a97bc44e 100644 --- a/src/runtime_ui_state/tests/recovery_cleanup.rs +++ b/src/runtime_ui_state/tests/recovery_cleanup.rs @@ -76,7 +76,7 @@ fn recovery_prunes_override_when_board_id_is_removed_and_readded_with_same_seed( fn recovery_captures_replacements_fenced_by_a_flush_before_closing_barrier() { let mut controller = controller(); commit_bool(&mut controller, InteractionSeedTarget::TopPinned, true); - let second = commit_bool(&mut controller, InteractionSeedTarget::SidePinned, true); + let second = commit_bool(&mut controller, InteractionSeedTarget::TopMinimized, true); let flush = controller.request_flush(second).unwrap(); let (_, incident) = fail_current_replace(&mut controller, "first write failed"); assert!(matches!( @@ -137,7 +137,7 @@ fn recovery_captures_replacements_fenced_by_a_flush_before_closing_barrier() { fn failed_reset_prerequisite_captures_replacements_fenced_by_a_flush() { let mut controller = controller(); commit_bool(&mut controller, InteractionSeedTarget::TopPinned, true); - let second = commit_bool(&mut controller, InteractionSeedTarget::SidePinned, true); + let second = commit_bool(&mut controller, InteractionSeedTarget::TopMinimized, true); let flush = controller.request_flush(second).unwrap(); let reset_through = match controller.request_supported_reset() { RequestResetResult::Started { through, .. } => through, @@ -270,7 +270,7 @@ fn reload_cleanup_is_written_even_when_failed_reset_has_no_retry_snapshot() { fn reload_during_cleanup_acknowledgement_recomputes_and_writes_again() { let mut original = controller(); commit_bool(&mut original, InteractionSeedTarget::TopPinned, true); - commit_bool(&mut original, InteractionSeedTarget::SidePinned, true); + commit_bool(&mut original, InteractionSeedTarget::TopMinimized, true); let first_persisted = original.take_source_mutation().unwrap(); apply_request(&mut original, &first_persisted, present_revision("r0.5")); let persisted = original.take_source_mutation().unwrap(); diff --git a/src/runtime_ui_state/tests/recovery_conflicts.rs b/src/runtime_ui_state/tests/recovery_conflicts.rs index cff42d0e..cb7bf847 100644 --- a/src/runtime_ui_state/tests/recovery_conflicts.rs +++ b/src/runtime_ui_state/tests/recovery_conflicts.rs @@ -5,7 +5,7 @@ fn external_conflict_discards_old_snapshots_and_chains_from_installed_authority( let mut controller = controller(); let first = commit_bool(&mut controller, InteractionSeedTarget::TopPinned, true); let request = controller.take_source_mutation().unwrap(); - let second = commit_bool(&mut controller, InteractionSeedTarget::SidePinned, true); + let second = commit_bool(&mut controller, InteractionSeedTarget::TopMinimized, true); let external = present_revision("external"); let result = controller.submit_source_mutation(SourceMutationResult::SourceChangedBeforeMutation { @@ -484,7 +484,7 @@ fn shutdown_during_external_cleanup_closes_after_a_late_reload() { commit_bool(&mut wire_source, InteractionSeedTarget::TopPinned, true); let first = wire_source.take_source_mutation().unwrap(); apply_request(&mut wire_source, &first, present_revision("wire-1")); - commit_bool(&mut wire_source, InteractionSeedTarget::SidePinned, true); + commit_bool(&mut wire_source, InteractionSeedTarget::TopMinimized, true); let second = wire_source.take_source_mutation().unwrap(); let SourceMutationKind::Replace(external_wire) = second.kind else { unreachable!(); @@ -557,7 +557,7 @@ fn invalid_external_authority_discards_pre_change_preview_rollback() { rollback, ) .unwrap(); - commit_bool(&mut controller, InteractionSeedTarget::SidePinned, true); + commit_bool(&mut controller, InteractionSeedTarget::TopMinimized, true); let write = controller.take_source_mutation().unwrap(); let invalid = invalid_observation("malformed-external"); let barrier = diff --git a/src/runtime_ui_state/tests/reset_preview.rs b/src/runtime_ui_state/tests/reset_preview.rs index 2a755b85..063c81de 100644 --- a/src/runtime_ui_state/tests/reset_preview.rs +++ b/src/runtime_ui_state/tests/reset_preview.rs @@ -47,7 +47,7 @@ fn supported_reset_waits_for_in_flight_write_and_publishes_epoch_on_ack() { .unwrap(); let first = commit_bool(&mut controller, InteractionSeedTarget::TopPinned, true); let request_a = controller.take_source_mutation().unwrap(); - let held = commit_bool(&mut controller, InteractionSeedTarget::SidePinned, true); + let held = commit_bool(&mut controller, InteractionSeedTarget::TopMinimized, true); let old_epoch = controller.authority_epoch(); let (barrier, reset_through, publish_epoch) = match controller.request_supported_reset() { RequestResetResult::Started { @@ -341,7 +341,7 @@ fn unsupported_reset_conflict_retains_live_only_authority_for_unsupported_source ); let untouched = controller .begin_runtime_preview( - RuntimeUiMutationScope::one(InteractionSeedTarget::SidePinned), + RuntimeUiMutationScope::one(InteractionSeedTarget::TopMinimized), PreviewRollbackSnapshot::default(), ) .unwrap(); @@ -394,7 +394,7 @@ fn unsupported_reset_conflict_retains_live_only_authority_for_unsupported_source untouched, RuntimePreviewFinishIntent::Commit( RuntimeUiMutationValues::one( - InteractionSeedTarget::SidePinned, + InteractionSeedTarget::TopMinimized, InteractionSeedValue::Bool(true), ) .unwrap(), @@ -583,7 +583,7 @@ fn retained_authority_failure_resolves_previews_before_a_later_staged_reload() { ); let mut side_rollback = PreviewRollbackSnapshot::default(); side_rollback.values.insert( - InteractionSeedTarget::SidePinned, + InteractionSeedTarget::TopMinimized, InteractionSeedValue::Bool(false), ); let top = controller @@ -594,7 +594,7 @@ fn retained_authority_failure_resolves_previews_before_a_later_staged_reload() { .unwrap(); let side = controller .begin_runtime_preview( - RuntimeUiMutationScope::one(InteractionSeedTarget::SidePinned), + RuntimeUiMutationScope::one(InteractionSeedTarget::TopMinimized), side_rollback, ) .unwrap(); diff --git a/src/runtime_ui_state/types.rs b/src/runtime_ui_state/types.rs index f4a2335d..0b79d1e9 100644 --- a/src/runtime_ui_state/types.rs +++ b/src/runtime_ui_state/types.rs @@ -2,7 +2,6 @@ use crate::config::{ ToolbarItemId, ToolbarItemOrderGroup, ToolbarItemVisibilitySetting as ItemVisibilitySetting, TopDisplayMode, }; -use crate::ui::toolbar::{SidePane, ToolbarSideSection}; macro_rules! id_type { ($name:ident) => { @@ -107,16 +106,11 @@ impl PersistedTopDisplayMode { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub(crate) enum InteractionSeedTarget { TopPinned, - SidePinned, TopMinimized, - SideMinimized, - SidePane, - CollapsedSection(ToolbarSideSection), ItemVisibility(ToolbarItemId), ItemOrder(ToolbarItemOrderGroup), BoardPin(String), TopPosition, - SidePosition, TopDisplayMode, /// Whether status-bar segments respond to clicks. StatusBarInteractive, @@ -157,7 +151,6 @@ pub(crate) enum InteractionSeedTarget { #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum InteractionSeedValue { Bool(bool), - SidePane(SidePane), Visibility(ItemVisibilitySetting), ItemOrder(Vec), Position(ToolbarPositionSeed), @@ -172,10 +165,7 @@ impl InteractionSeedValue { (target, self), ( Target::TopPinned - | Target::SidePinned | Target::TopMinimized - | Target::SideMinimized - | Target::CollapsedSection(_) | Target::BoardPin(_) | Target::StatusBarInteractive | Target::StatusBarItem(_) @@ -196,16 +186,11 @@ impl InteractionSeedValue { | Target::FloatingBadge | Target::ZoomChip, Self::Bool(_), - ) | (Target::SidePane, Self::SidePane(_)) - | ( - Target::ItemVisibility(_) | Target::SectionVisibility(_), - Self::Visibility(_) - ) - | (Target::ItemOrder(_), Self::ItemOrder(_)) - | ( - Target::TopPosition | Target::SidePosition, - Self::Position(_) - ) + ) | ( + Target::ItemVisibility(_) | Target::SectionVisibility(_), + Self::Visibility(_) + ) | (Target::ItemOrder(_), Self::ItemOrder(_)) + | (Target::TopPosition, Self::Position(_)) | (Target::TopDisplayMode, Self::TopDisplayMode(_)) | (Target::ToolbarLayoutMode, Self::LayoutMode(_)) ) diff --git a/src/runtime_ui_state/wire/tests.rs b/src/runtime_ui_state/wire/tests.rs index 7ee211e8..ccdd4154 100644 --- a/src/runtime_ui_state/wire/tests.rs +++ b/src/runtime_ui_state/wire/tests.rs @@ -1,6 +1,8 @@ use super::*; use crate::runtime_ui_state::{ - InteractionSeedTarget, InteractionSeedValue, PersistedTopDisplayMode, ToolbarPositionSeed, + InteractionSeedTarget, InteractionSeedValue, PersistedTopDisplayMode, RuntimeStatePathIdentity, + RuntimeStateSourceRevision, RuntimeUiStateController, ToolbarPositionSeed, + ValidatedInteractionSeeds, }; #[test] @@ -98,7 +100,7 @@ fn unknown_ids_and_unknown_order_items_are_pruned() { let source = br#" version = 1 -[toolbar.collapsed_sections.future-section] +[toolbar.sections.future-section] seed = false value = true @@ -149,14 +151,6 @@ fn malformed_file_without_version_is_invalid() { fn every_v1_override_shape_round_trips() { let source = br#"version = 1 -[toolbar.side_pane] -seed = "draw" -value = "canvas" - -[toolbar.collapsed_sections.colors] -seed = false -value = true - [toolbar.item_visibility."top.tool.pen"] seed = "default" value = "hidden" @@ -169,10 +163,6 @@ value = ["top.tool.marker", "top.tool.pen"] seed = { x = 0.0, y = 0.0 } value = { x = -12.5, y = 48.0 } -[toolbar.side_position] -seed = { x = 1.0, y = 2.0 } -value = { x = 3.25, y = -4.5 } - [toolbar.top_display_mode] seed = "full" value = "micro" @@ -184,7 +174,7 @@ value = true let decoded = decode_runtime_ui_file(source); assert_eq!(decoded.status, RuntimeUiFileStatus::Supported); let wire = decoded.supported_wire.unwrap(); - assert_eq!(wire.model.iter().count(), 8); + assert_eq!(wire.model.iter().count(), 5); let encoded = encode_runtime_ui_file(&wire).unwrap(); let reparsed = decode_runtime_ui_file(&encoded); assert_eq!(reparsed.supported_wire, Some(wire)); @@ -198,10 +188,6 @@ fn position_and_display_mode_overrides_decode_to_their_typed_values() { seed = { x = 0.0, y = 0.0 } value = { x = -12.5, y = 48.0 } -[toolbar.side_position] -seed = { x = 1.0, y = 2.0 } -value = { x = 3.25, y = -4.5 } - [toolbar.top_display_mode] seed = "full" value = "micro" @@ -217,14 +203,6 @@ value = "micro" ToolbarPositionSeed::new(-12.5, 48.0).unwrap() )) ); - assert_eq!( - wire.model - .get(&InteractionSeedTarget::SidePosition) - .map(|entry| &entry.seed), - Some(&InteractionSeedValue::Position( - ToolbarPositionSeed::new(1.0, 2.0).unwrap() - )) - ); assert_eq!( wire.model .get(&InteractionSeedTarget::TopDisplayMode) @@ -303,7 +281,7 @@ value = { x = 1.0 } "# .as_slice(), br#"version = 1 -[toolbar.side_position] +[toolbar.top_position] seed = { x = 0.0, y = 0.0 } value = { x = 1.0, y = 2.0, z = 3.0 } "# @@ -351,14 +329,141 @@ fn duplicate_normalized_recognized_ids_are_invalid() { let decoded = decode_runtime_ui_file( br#"version = 1 +[toolbar.item_visibility."top.tool.pen"] +seed = "default" +value = "hidden" + +[toolbar.item_visibility." top.tool.pen "] +seed = "hidden" +value = "default" +"#, + ); + assert_eq!(decoded.status, RuntimeUiFileStatus::Invalid); +} + +/// The retired side palette left five raw shapes behind in existing files. +/// This build no longer models them, so they must survive a rewrite verbatim +/// and contribute nothing to the live model. +#[test] +fn retired_side_entries_are_inert_passthrough() { + let source = br#"version = 1 + +[toolbar.top_pinned] +seed = false +value = true + +[toolbar.side_pinned] +seed = false +value = true + +[toolbar.side_minimized] +seed = false +value = true + +[toolbar.side_pane] +seed = "draw" +value = "canvas" + +[toolbar.side_position] +seed = { x = 1.0, y = 2.0 } +value = { x = 3.25, y = -4.5 } + [toolbar.collapsed_sections.colors] seed = false value = true -[toolbar.collapsed_sections."COLORS"] -seed = true -value = false -"#, +[toolbar.item_order.side_sections] +seed = ["side.group.colors"] +value = ["side.group.pages"] + +[toolbar.item_order.tool_options] +seed = ["side.tool-options.thickness"] +value = ["side.tool-options.fill"] + +[toolbar.item_order.actions] +seed = ["side.actions.undo"] +value = ["side.actions.redo"] +"#; + let decoded = decode_runtime_ui_file(source); + assert_eq!(decoded.status, RuntimeUiFileStatus::Supported); + let wire = decoded.supported_wire.expect("supported wire"); + assert_eq!( + wire.model.iter().count(), + 1, + "only top_pinned is modeled; the side entries stay raw" ); - assert_eq!(decoded.status, RuntimeUiFileStatus::Invalid); + + let mut seeds = ValidatedInteractionSeeds::new(); + seeds + .insert( + InteractionSeedTarget::TopPinned, + InteractionSeedValue::Bool(false), + ) + .expect("valid top-pinned seed"); + let controller = RuntimeUiStateController::new_with_authority( + seeds, + RuntimeStateSourceRevision::present( + RuntimeStatePathIdentity::direct("/tmp/retired-side-runtime-ui.toml"), + source.to_vec(), + ), + RuntimeUiFileStatus::Supported, + wire.clone(), + ); + assert_eq!( + controller + .live_state() + .get(&InteractionSeedTarget::TopPinned), + Some(&InteractionSeedValue::Bool(true)), + "the supported model is applied" + ); + assert_eq!( + controller.model().iter().count(), + 1, + "retired side values do not become live entries during application" + ); + + let encoded = encode_runtime_ui_file(&wire).expect("encode"); + let text = String::from_utf8(encoded.clone()).unwrap(); + let parsed: toml::Value = toml::from_str(&text).expect("encoded TOML"); + let toolbar = parsed + .get("toolbar") + .and_then(toml::Value::as_table) + .expect("toolbar table"); + for retired in [ + "side_pinned", + "side_minimized", + "side_pane", + "side_position", + "collapsed_sections", + ] { + assert!( + toolbar.contains_key(retired), + "retired toolbar key `{retired}` must be preserved" + ); + } + assert_eq!( + toolbar + .get("side_position") + .and_then(toml::Value::as_table) + .and_then(|position| position.get("value")) + .and_then(toml::Value::as_table) + .and_then(|value| value.get("x")) + .and_then(toml::Value::as_float), + Some(3.25), + "raw side_position values survive" + ); + // `item_order` is a recognized map, so retired group ids are pruned by + // the same unknown-ID contract that prunes unknown item ids. Authored + // config.toml uses RetiredSetting preservation instead. + let item_order = toolbar.get("item_order").and_then(toml::Value::as_table); + for pruned in ["side_sections", "tool_options", "actions"] { + assert!( + item_order.is_none_or(|order| !order.contains_key(pruned)), + "retired order group `{pruned}` is pruned, not preserved" + ); + } + + let reparsed = decode_runtime_ui_file(&encoded); + assert_eq!(reparsed.status, RuntimeUiFileStatus::Supported); + assert_eq!(reparsed.supported_wire, Some(wire)); } diff --git a/src/runtime_ui_state/wire/v1.rs b/src/runtime_ui_state/wire/v1.rs index 1e4ad069..d00147ed 100644 --- a/src/runtime_ui_state/wire/v1.rs +++ b/src/runtime_ui_state/wire/v1.rs @@ -10,22 +10,20 @@ use crate::runtime_ui_state::{ InteractionSeedTarget, InteractionSeedValue, ItemVisibilitySetting, PersistedTopDisplayMode, RuntimeOverride, RuntimeUiModel, RuntimeUiWireState, ToolbarPositionSeed, WirePassthrough, }; -use crate::ui::toolbar::{SidePane, ToolbarSideSection}; - /// Recognized `[toolbar]` overrides that carry a single scalar or table value. /// -/// `top_position`, `side_position`, and `top_display_mode` were added after -/// V1 shipped. They stay in V1 because an older build decodes them as unknown -/// keys and preserves them verbatim through `WirePassthrough`, whereas a -/// version bump would make that build treat the whole file as read-only. -const TOOLBAR_SCALARS: [(&str, InteractionSeedTarget); 26] = [ +/// `top_position` and `top_display_mode` were added after V1 shipped. They stay +/// in V1 because an older build decodes them as unknown keys and preserves them +/// verbatim through `WirePassthrough`, whereas a version bump would make that +/// build treat the whole file as read-only. +/// +/// 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] = [ ("top_pinned", InteractionSeedTarget::TopPinned), - ("side_pinned", InteractionSeedTarget::SidePinned), ("top_minimized", InteractionSeedTarget::TopMinimized), - ("side_minimized", InteractionSeedTarget::SideMinimized), - ("side_pane", InteractionSeedTarget::SidePane), ("top_position", InteractionSeedTarget::TopPosition), - ("side_position", InteractionSeedTarget::SidePosition), ("top_display_mode", InteractionSeedTarget::TopDisplayMode), ("layout_mode", InteractionSeedTarget::ToolbarLayoutMode), ("click_highlight", InteractionSeedTarget::ClickHighlight), @@ -94,12 +92,6 @@ fn decode_toolbar( decode_override(value, target, &mut wire.model, &mut wire.passthrough)?; } } - decode_id_map( - toolbar.remove("collapsed_sections"), - |id| ToolbarSideSection::from_config_id(id).map(InteractionSeedTarget::CollapsedSection), - &mut wire.model, - &mut wire.passthrough, - )?; decode_id_map( toolbar.remove("item_visibility"), |id| { @@ -212,10 +204,7 @@ fn decode_value( use InteractionSeedTarget as Target; match target { Target::TopPinned - | Target::SidePinned | Target::TopMinimized - | Target::SideMinimized - | Target::CollapsedSection(_) | Target::BoardPin(_) | Target::StatusBarInteractive | Target::StatusBarItem(_) @@ -238,11 +227,6 @@ fn decode_value( .as_bool() .map(InteractionSeedValue::Bool) .ok_or_else(|| RuntimeUiWireError::new("boolean override has a non-boolean value")), - Target::SidePane => value - .as_str() - .and_then(SidePane::from_config_id) - .map(InteractionSeedValue::SidePane) - .ok_or_else(|| RuntimeUiWireError::new("side pane override has an unknown value")), Target::ItemVisibility(_) | Target::SectionVisibility(_) => match value.as_str() { Some("default") => Ok(InteractionSeedValue::Visibility( ItemVisibilitySetting::Default, @@ -258,7 +242,7 @@ fn decode_value( )), }, Target::ItemOrder(group) => decode_order(*group, value), - Target::TopPosition | Target::SidePosition => decode_position(value), + Target::TopPosition => decode_position(value), Target::ToolbarLayoutMode => value .as_str() .and_then(layout_mode_from_wire_id) @@ -339,7 +323,6 @@ pub(super) fn encode(wire: &RuntimeUiWireState) -> Result Result { insert_recognized(&mut toolbar, "top_pinned", entry) } - InteractionSeedTarget::SidePinned => { - insert_recognized(&mut toolbar, "side_pinned", entry) - } InteractionSeedTarget::TopMinimized => { insert_recognized(&mut toolbar, "top_minimized", entry) } - InteractionSeedTarget::SideMinimized => { - insert_recognized(&mut toolbar, "side_minimized", entry) - } - InteractionSeedTarget::SidePane => insert_recognized(&mut toolbar, "side_pane", entry), - InteractionSeedTarget::CollapsedSection(section) => { - insert_recognized(&mut collapsed, section.config_id(), entry) - } InteractionSeedTarget::ItemVisibility(item) => { insert_recognized(&mut visibility, item.as_str(), entry) } @@ -379,9 +352,6 @@ pub(super) fn encode(wire: &RuntimeUiWireState) -> Result { insert_recognized(&mut toolbar, "top_position", entry) } - InteractionSeedTarget::SidePosition => { - insert_recognized(&mut toolbar, "side_position", entry) - } InteractionSeedTarget::ToolbarLayoutMode => { insert_recognized(&mut toolbar, "layout_mode", entry) } @@ -443,7 +413,6 @@ pub(super) fn encode(wire: &RuntimeUiWireState) -> Result insert_recognized(&mut toolbar, "zoom_chip", entry), } } - insert_recognized(&mut toolbar, "collapsed_sections", Value::Table(collapsed)); insert_recognized(&mut toolbar, "item_visibility", Value::Table(visibility)); insert_recognized(&mut toolbar, "item_order", Value::Table(order)); insert_recognized( @@ -493,10 +462,7 @@ fn encode_value( match (target, value) { ( InteractionSeedTarget::TopPinned - | InteractionSeedTarget::SidePinned | InteractionSeedTarget::TopMinimized - | InteractionSeedTarget::SideMinimized - | InteractionSeedTarget::CollapsedSection(_) | InteractionSeedTarget::BoardPin(_) | InteractionSeedTarget::StatusBarInteractive | InteractionSeedTarget::StatusBarItem(_) @@ -518,9 +484,6 @@ fn encode_value( | InteractionSeedTarget::ZoomChip, InteractionSeedValue::Bool(value), ) => Ok(Value::Boolean(*value)), - (InteractionSeedTarget::SidePane, InteractionSeedValue::SidePane(value)) => { - Ok(Value::String(value.config_id().to_string())) - } ( InteractionSeedTarget::ItemVisibility(_) | InteractionSeedTarget::SectionVisibility(_), InteractionSeedValue::Visibility(value), @@ -548,10 +511,7 @@ fn encode_value( .collect(), )) } - ( - InteractionSeedTarget::TopPosition | InteractionSeedTarget::SidePosition, - InteractionSeedValue::Position(position), - ) => { + (InteractionSeedTarget::TopPosition, InteractionSeedValue::Position(position)) => { let mut table = Table::new(); table.insert("x".to_string(), Value::Float(position.x.get())); table.insert("y".to_string(), Value::Float(position.y.get())); @@ -618,13 +578,6 @@ fn order_group_wire_id(group: ToolbarItemOrderGroup) -> &'static str { match group { ToolbarItemOrderGroup::TopTools => "top_tools", ToolbarItemOrderGroup::TopControls => "top_controls", - ToolbarItemOrderGroup::SideSections => "side_sections", - ToolbarItemOrderGroup::Actions => "actions", - ToolbarItemOrderGroup::Pages => "pages", - ToolbarItemOrderGroup::Boards => "boards", - ToolbarItemOrderGroup::Presets => "presets", - ToolbarItemOrderGroup::ToolOptions => "tool_options", - ToolbarItemOrderGroup::Sessions => "sessions", } } @@ -632,13 +585,9 @@ fn order_group_from_wire_id(value: &str) -> Option { match value { "top_tools" => Some(ToolbarItemOrderGroup::TopTools), "top_controls" => Some(ToolbarItemOrderGroup::TopControls), - "side_sections" => Some(ToolbarItemOrderGroup::SideSections), - "actions" => Some(ToolbarItemOrderGroup::Actions), - "pages" => Some(ToolbarItemOrderGroup::Pages), - "boards" => Some(ToolbarItemOrderGroup::Boards), - "presets" => Some(ToolbarItemOrderGroup::Presets), - "tool_options" => Some(ToolbarItemOrderGroup::ToolOptions), - "sessions" => Some(ToolbarItemOrderGroup::Sessions), + // Retired panel-era order groups are unrecognized. The recognized-map + // decoder drops them on rewrite; authored config.toml values use the + // separate RetiredSetting document path instead. _ => None, } } diff --git a/src/toolbar_gtk/bridge.rs b/src/toolbar_gtk/bridge.rs index 5b6e6616..2fb05b2d 100644 --- a/src/toolbar_gtk/bridge.rs +++ b/src/toolbar_gtk/bridge.rs @@ -314,10 +314,6 @@ impl GtkToolbarFeedback { phase: GtkToolbarDragPhase::Move, .. } => Some(GtkToolbarKind::Top), - Self::SetSideOffset { - phase: GtkToolbarDragPhase::Move, - .. - } => Some(GtkToolbarKind::Side), _ => None, } } @@ -329,7 +325,6 @@ impl GtkToolbarFeedback { fn drag_kind(&self) -> Option { match self { Self::SetTopOffset { .. } => Some(GtkToolbarKind::Top), - Self::SetSideOffset { .. } => Some(GtkToolbarKind::Side), Self::Event { .. } | Self::TopHover { .. } | Self::CaptureSuppressionReady { .. } @@ -549,13 +544,6 @@ mod tests { seq, phase, }, - GtkToolbarKind::Side => GtkToolbarFeedback::SetSideOffset { - x: 0.0, - y: seq as f64, - surface_size: SURFACE, - seq, - phase, - }, } } @@ -714,11 +702,11 @@ mod tests { #[test] fn capture_suppression_ack_reclaims_a_move_and_remains_an_ordered_boundary() { let (_wake, health, publisher) = channel(); - let start = drag(GtkToolbarKind::Side, GtkToolbarDragPhase::Start, 1); + let start = drag(GtkToolbarKind::Top, GtkToolbarDragPhase::Start, 1); publisher.publish(start.clone()).unwrap(); for seq in 2..=GTK_FEEDBACK_CAPACITY as u64 { publisher - .publish(drag(GtkToolbarKind::Side, GtkToolbarDragPhase::Move, seq)) + .publish(drag(GtkToolbarKind::Top, GtkToolbarDragPhase::Move, seq)) .unwrap(); } @@ -733,7 +721,7 @@ mod tests { } #[test] - fn reclaimed_move_is_appended_after_event_boundary_and_other_kind() { + fn reclaimed_move_is_appended_after_the_event_boundary() { let (_wake, health, publisher) = channel(); for seq in 0..(GTK_FEEDBACK_CAPACITY - 2) as u64 { publisher @@ -741,20 +729,15 @@ mod tests { .unwrap(); } publisher.publish(event()).unwrap(); - publisher - .publish(drag(GtkToolbarKind::Side, GtkToolbarDragPhase::Move, 1)) - .unwrap(); + let kept_move = drag(GtkToolbarKind::Top, GtkToolbarDragPhase::Move, 1); + publisher.publish(kept_move.clone()).unwrap(); let latest_top = drag(GtkToolbarKind::Top, GtkToolbarDragPhase::Move, 999); publisher.publish(latest_top.clone()).unwrap(); let drained = publisher.drain(GTK_FEEDBACK_CAPACITY); assert_eq!(drained.len(), GTK_FEEDBACK_CAPACITY); - assert_eq!(drained[GTK_FEEDBACK_CAPACITY - 3], event()); - assert_eq!( - drained[GTK_FEEDBACK_CAPACITY - 2], - drag(GtkToolbarKind::Side, GtkToolbarDragPhase::Move, 1) - ); + assert_eq!(drained[GTK_FEEDBACK_CAPACITY - 2], event()); assert_eq!(drained.last(), Some(&latest_top)); assert!(!health.failed()); } diff --git a/src/toolbar_gtk/css.rs b/src/toolbar_gtk/css.rs index 774c77da..fb0a1552 100644 --- a/src/toolbar_gtk/css.rs +++ b/src/toolbar_gtk/css.rs @@ -230,31 +230,12 @@ window.wayscriber-toolbar {{ outline-color: transparent; }} -/* ===== Side palette ==================================================== */ -.wayscriber-toolbar .header-band {{ - background-color: {header_band}; - border-radius: {radius_card}px; - padding: {spacing_sm}px {spacing_md}px; -}} -.wayscriber-toolbar .card {{ - background-color: {card}; - border-radius: {radius_card}px; - padding: {pad_std}px; -}} +/* ===== Popover / strip shared chrome =================================== */ .wayscriber-toolbar .section-title {{ color: {section_title}; font-size: {font_label}px; font-weight: {weight_bold}; }} -.wayscriber-toolbar button.section-header {{ - background: none; - background-color: transparent; - box-shadow: none; - padding: {spacing_xs}px 0; -}} -.wayscriber-toolbar button.section-header:hover {{ - background-color: {section_header_hover}; -}} .wayscriber-toolbar button.tab {{ font-size: {font_tooltip}px; font-weight: {weight_bold}; @@ -265,9 +246,8 @@ window.wayscriber-toolbar {{ box-shadow: none; }} /* The style pill's Sans│Mono segment gets horizontal breathing room and a - rounded pill (M7-C3). Scoped to `.pill` so the Settings-pane and - side-palette tabs keep their prior flush styling (M7-C5) and the builtin — - whose segmented-control metrics are pill-only — stays matched. */ + rounded pill. Scoped to `.pill` so Settings layout-mode tabs keep their + flush styling and the builtin segmented-control metrics stay matched. */ .wayscriber-toolbar .pill button.tab {{ padding: {spacing_xs}px {segment_pad_h}px; border-radius: {radius_button}px; @@ -275,18 +255,6 @@ window.wayscriber-toolbar {{ .wayscriber-toolbar .pill button.tab.active {{ border-radius: {radius_button}px; }} -.wayscriber-toolbar button.board-chip {{ - background-color: {field_bg}; - border: {hairline}px solid {field_border}; - border-radius: {radius_lg_fixed}px; - padding: {spacing_xxs}px {spacing_std}px; - font-size: {font_label}px; - font-weight: {weight_bold}; -}} -.wayscriber-toolbar button.board-chip:hover {{ - background-color: {field_bg_hover}; - border-color: {field_border_hover}; -}} .wayscriber-toolbar entry {{ background-color: {field_bg}; border: {hairline}px solid {field_border}; @@ -360,18 +328,12 @@ tooltip.{capture_transparent_class} {{ badge_border = v.badge_border, popover_shadow = v.popover_shadow, popover_shadow_clear = v.popover_shadow_clear, - header_band = v.header_band, - card = v.card, section_title = v.section_title, - section_header_hover = v.section_header_hover, field_bg = v.field_bg, - field_bg_hover = v.field_bg_hover, field_border = v.field_border, - field_border_hover = v.field_border_hover, scrollbar_slider = v.scrollbar_slider, capture_transparent_class = CAPTURE_TRANSPARENT_CLASS, radius_panel = v.radius_panel, - radius_card = v.radius_card, radius_button = v.radius_button, radius_sm = v.radius_sm, radius_full = v.radius_full, @@ -396,7 +358,6 @@ tooltip.{capture_transparent_class} {{ indicator_height = v.indicator_height, spacing_xxs = v.spacing_xxs, spacing_xs = v.spacing_xs, - spacing_sm = v.spacing_sm, spacing_md = v.spacing_md, spacing_std = v.spacing_std, scrollbar_radius = v.scrollbar_radius, @@ -494,35 +455,18 @@ mod tests { "COLOR_POPOVER_SHADOW", rgba_css_compact(t::COLOR_POPOVER_SHADOW), ), - ("COLOR_HEADER_BAND", rgba_css_compact(t::COLOR_HEADER_BAND)), - ( - "COLOR_CARD_BACKGROUND", - rgba_css_compact(t::COLOR_CARD_BACKGROUND), - ), ( "COLOR_LABEL_SECTION", rgba_css_compact(t::COLOR_LABEL_SECTION), ), - ( - "COLOR_SECTION_HEADER_HOVER", - rgba_css_compact(t::COLOR_SECTION_HEADER_HOVER), - ), ( "COLOR_FIELD_BACKGROUND", rgba_css_compact(t::COLOR_FIELD_BACKGROUND), ), - ( - "COLOR_FIELD_BACKGROUND_HOVER", - rgba_css_compact(t::COLOR_FIELD_BACKGROUND_HOVER), - ), ( "COLOR_FIELD_BORDER", rgba_css_compact(t::COLOR_FIELD_BORDER), ), - ( - "COLOR_FIELD_BORDER_HOVER", - rgba_css_compact(t::COLOR_FIELD_BORDER_HOVER), - ), ( "COLOR_SCROLLBAR_SLIDER", rgba_css_compact(t::COLOR_SCROLLBAR_SLIDER), diff --git a/src/toolbar_gtk/icons.rs b/src/toolbar_gtk/icons.rs index 347ae254..1a7d9c69 100644 --- a/src/toolbar_gtk/icons.rs +++ b/src/toolbar_gtk/icons.rs @@ -12,19 +12,10 @@ use std::rc::Rc; use gtk4::prelude::*; -use crate::input::Tool; use crate::toolbar_icons; -use crate::ui::toolbar::model; pub(super) type IconPainter = toolbar_icons::ToolbarIconPainter; -/// Painter for a tool's semantic icon; mirrors the built-in mapping. -pub(super) fn tool_icon_painter(tool: Tool) -> IconPainter { - toolbar_icons::top_toolbar_icon_painter(model::TopToolbarIcon::Tool( - model::semantic_icon_for_tool(tool), - )) -} - /// Handle to an icon widget whose painter can be swapped when live toolbar /// state changes the represented action (for example pin/unpin). #[derive(Clone)] diff --git a/src/toolbar_gtk/mod.rs b/src/toolbar_gtk/mod.rs index 0d3d654d..cc299dd2 100644 --- a/src/toolbar_gtk/mod.rs +++ b/src/toolbar_gtk/mod.rs @@ -1,6 +1,6 @@ //! GTK4 toolbar frontend. //! -//! Renders the top strip and side palette as GTK4 windows on their own +//! Renders the top strip as a GTK4 window on its own //! layer-shell surfaces, replacing the built-in Cairo toolbars on //! compositors where those would have used separate layer surfaces anyway. //! The built-in toolbars stay compiled in as the fallback everywhere else: @@ -56,18 +56,15 @@ pub(crate) fn drag_debug_log(message: impl AsRef) { pub struct GtkToolbarUpdate { pub snapshot: ToolbarSnapshot, pub top_visible: bool, - pub side_visible: bool, - /// Drag offsets relative to each bar's base margins, already clamped - /// by the backend. + /// Drag offset relative to the bar's base margins, already clamped by the + /// backend. pub top_offset: (f64, f64), - pub side_offset: (f64, f64), - /// Highest drag sequence number the backend has drained per bar; the - /// GTK side ignores offset echoes older than its own counter so a - /// mid-drag mirror can never snap a just-released bar backwards. + /// Highest drag sequence number the backend has drained; the GTK side + /// ignores offset echoes older than its own counter so a mid-drag mirror + /// can never snap a just-released bar backwards. pub top_offset_seq: u64, - pub side_offset_seq: u64, - /// Base X for the top strip in spec units (the side palette pushes it - /// right when they would overlap), mirroring the backend clamp math. + /// Base X for the top strip in spec units, mirroring the backend clamp + /// math. pub top_base_x: f64, /// Connector name of the output hosting the overlay (e.g. "DP-1"), /// used to pin the GTK bars to the same monitor. @@ -93,11 +90,10 @@ pub struct GtkToolbarUpdate { pub capture_suppression_generation: Option, } -/// Identifies one of the two GTK toolbar surfaces. +/// Identifies a GTK toolbar surface. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GtkToolbarKind { Top, - Side, } /// Explicit lifecycle for a GTK move gesture. The backend starts the inline @@ -161,12 +157,9 @@ pub enum GtkToolbarFeedback { event: ToolbarEvent, rebind_requested: bool, }, - /// Pointer entered/left a GTK toolbar window (top strip or side - /// palette). GTK runs on its own Wayland connection, so the backend - /// cannot observe this hover itself; it drives the top-strip idle-fade - /// restore/hold. Both windows report through this one flag — the - /// pointer can only be over one of them, and leave precedes enter when - /// it crosses between them. + /// Pointer entered/left the GTK top strip. GTK runs on its own Wayland + /// connection, so the backend cannot observe this hover itself; it drives + /// the top-strip idle-fade restore/hold. TopHover { hovered: bool }, /// Drag-to-move lifecycle for the top bar. `End` is when the offsets get /// clamped and persisted; `seq` is the bar's monotonically increasing @@ -179,14 +172,6 @@ pub enum GtkToolbarFeedback { seq: u64, phase: GtkToolbarDragPhase, }, - /// Drag-to-move progress for the side palette. - SetSideOffset { - x: f64, - y: f64, - surface_size: GtkToolbarSurfaceSize, - seq: u64, - phase: GtkToolbarDragPhase, - }, } #[cfg(feature = "toolbar-gtk")] diff --git a/src/toolbar_gtk/view/drag.rs b/src/toolbar_gtk/view/drag.rs index dd23356e..9b5f4c84 100644 --- a/src/toolbar_gtk/view/drag.rs +++ b/src/toolbar_gtk/view/drag.rs @@ -1,8 +1,7 @@ //! Shared GTK toolbar move-drag state and geometry. //! -//! Top and side toolbar adapters both keep their GTK input surface parked -//! while the main overlay renders a moving preview. This module owns the -//! lifecycle mechanics that must remain identical across those adapters. +//! The top toolbar adapter keeps its GTK input surface parked while the +//! main overlay renders a moving preview. This module owns that lifecycle. use std::cell::{Cell, RefCell}; use std::collections::VecDeque; @@ -166,13 +165,6 @@ pub(super) fn cancel_move_drag( seq: seq.get(), phase: GtkToolbarDragPhase::End, }, - GtkToolbarKind::Side => GtkToolbarFeedback::SetSideOffset { - x, - y, - surface_size, - seq: seq.get(), - phase: GtkToolbarDragPhase::End, - }, }; if feedback.send(end).is_err() { super::set_visual_hidden(window, visual, kind, false); diff --git a/src/toolbar_gtk/view/mod.rs b/src/toolbar_gtk/view/mod.rs index 30f986b9..d8aeefb8 100644 --- a/src/toolbar_gtk/view/mod.rs +++ b/src/toolbar_gtk/view/mod.rs @@ -1,10 +1,9 @@ -//! GTK toolbar windows: owns the top strip and side palette, the shared +//! GTK toolbar windows: owns the top strip, the shared //! stylesheet, and output pinning. mod capture_suppression; mod drag; mod sections; -mod side_bar; mod top_bar; use gtk4::prelude::*; @@ -414,7 +413,6 @@ where pub(super) struct Windows { top: top_bar::TopBar, - side: side_bar::SideBar, tooltip_capture: capture_suppression::TooltipCapture, css_provider: gtk4::CssProvider, css_scale_milli: i64, @@ -436,7 +434,6 @@ impl Windows { } Self { top: top_bar::TopBar::new(feedback.clone()), - side: side_bar::SideBar::new(feedback.clone()), tooltip_capture: capture_suppression::TooltipCapture::new(), css_provider, css_scale_milli: 1000, @@ -466,7 +463,6 @@ impl Windows { self.tooltip_capture .set_suppressed(update.capture_suppressed); let top_mapped = self.top.apply(update); - let side_mapped = self.side.apply(update); self.refresh_popup_capture_sources(); if let CaptureUpdatePlan::ApplyAndAcknowledge(generation) = capture_plan { let proof = async { @@ -474,7 +470,7 @@ impl Windows { // regions together. Confirming this first presentation proves // both changes reached the compositor before the GTK main // context is allowed to settle queued popup work. - self.wait_for_capture_paints(generation, top_mapped, side_mapped) + self.wait_for_capture_paints(generation, top_mapped) .await?; let display = gtk4::gdk::Display::default().ok_or_else(|| { @@ -543,15 +539,11 @@ impl Windows { &self, generation: u64, top_mapped: bool, - side_mapped: bool, ) -> Result<(), String> { let mut targets = Vec::new(); if top_mapped { targets.push(self.top.capture_target()); } - if side_mapped { - targets.push(self.side.capture_target()); - } targets.extend(self.top.capture_popover_targets()); targets.extend(self.tooltip_capture.capture_popover_targets()); if let Some(tooltip) = self.tooltip_capture.capture_target() { @@ -570,10 +562,7 @@ impl Windows { } fn popup_capture_roots(&self) -> Vec { - let mut roots = vec![ - self.top.window.clone().upcast::(), - self.side.window.clone().upcast::(), - ]; + let mut roots = vec![self.top.window.clone().upcast::()]; roots.extend(self.top.tooltip_roots()); roots } @@ -608,7 +597,6 @@ impl Windows { self.pinned_output = update.output_name.clone(); // None lets the compositor pick, matching a missing preference. self.top.window.set_monitor(monitor.as_ref()); - self.side.window.set_monitor(monitor.as_ref()); } } @@ -671,17 +659,6 @@ mod tests { )); } - #[test] - fn another_bars_preview_does_not_hide_this_surface() { - assert!(!drag_visual_should_be_hidden( - Some(GtkToolbarKind::Side), - GtkToolbarKind::Top, - false, - 7, - 7, - )); - } - #[test] fn capture_suppression_keeps_a_configured_toolbar_mapped_but_transparent() { assert_eq!( diff --git a/src/toolbar_gtk/view/sections/actions.rs b/src/toolbar_gtk/view/sections/actions.rs deleted file mode 100644 index c2c5cd47..00000000 --- a/src/toolbar_gtk/view/sections/actions.rs +++ /dev/null @@ -1,221 +0,0 @@ -//! Actions section: the History / Zoom / Advanced command groups with the -//! built-in sub-labels, destructive split, and stateful lock/freeze faces. - -use gtk4::prelude::*; - -use crate::label_format::format_binding_label; -use crate::toolbar_icons; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection, ToolbarSnapshot, model}; - -use super::super::super::icons::{IconPainter, IconWidget}; -use super::super::super::widgets::{send_event, set_active_class, sized_button}; -use super::{SectionCtx, command_row, section_card}; - -/// Tooltip fallback the built-in passes for action buttons. -const NOUN: &str = "Action"; - -pub(in crate::toolbar_gtk) fn build(ctx: &mut SectionCtx) -> Option { - let actions_model = model::ToolbarActionsModel::from_snapshot(ctx.snapshot)?; - let card = section_card( - ctx, - ToolbarSideSection::Actions, - ToolbarSideSection::Actions.label(), - ); - for group in actions_model.groups() { - // Sub-label and buttons sit tight together; the body's spacing - // separates the groups like the built-in's group gap. - let block = gtk4::Box::new(gtk4::Orientation::Vertical, ctx.px(2.0)); - if let Some(label) = group.kind.sub_label() { - let sub = gtk4::Label::new(Some(label)); - sub.add_css_class("hint"); - sub.set_xalign(0.0); - block.append(&sub); - } - block.append(&build_group(ctx, group)); - card.body.append(&block); - } - Some(card.root.upcast()) -} - -/// One command group's buttons in the built-in arrangement: History reuses -/// the shared split row in icon mode (Undo/Redo left, destructive Clear -/// right) and stacks full-width rows in text mode; Zoom and Advanced sit -/// in a centered icon row or a two-column text grid, with Zoom's text -/// buttons carrying the accent "active" styling the built-in gives them. -fn build_group(ctx: &mut SectionCtx, group: &model::ToolbarCommandGroup) -> gtk4::Widget { - let kind = group.kind; - let history = kind == model::ToolbarCommandGroupKind::History; - if ctx.use_icons && history { - return command_row(ctx, &group.buttons, NOUN, history_icon, history_group).upcast(); - } - - let text_active_style = !ctx.use_icons && kind == model::ToolbarCommandGroupKind::Zoom; - let handles: Vec = group - .buttons - .iter() - .map(|button_model| action_button(ctx, button_model, text_active_style)) - .collect(); - - let container: gtk4::Widget = if ctx.use_icons { - // Zoom / Advanced: a single centered row (the built-in centers a - // five-column grid; both groups hold at most five buttons). - let row = gtk4::Box::new(gtk4::Orientation::Horizontal, ctx.px(6.0)); - row.set_halign(gtk4::Align::Center); - for handle in &handles { - row.append(&handle.button); - } - row.upcast() - } else if history { - // History text mode: one full-width button per row. - let column = gtk4::Box::new(gtk4::Orientation::Vertical, ctx.px(5.0)); - for handle in &handles { - column.append(&handle.button); - } - column.upcast() - } else { - // Zoom / Advanced text mode: two equal-width columns. - let grid = gtk4::Grid::new(); - grid.set_column_homogeneous(true); - grid.set_row_spacing(ctx.px(5.0) as u32); - grid.set_column_spacing(ctx.px(6.0) as u32); - for (index, handle) in handles.iter().enumerate() { - grid.attach(&handle.button, (index % 2) as i32, (index / 2) as i32, 1, 1); - } - grid.upcast() - }; - - ctx.updaters.push(Box::new(move |snapshot| { - let Some(group) = group_of(snapshot, kind) else { - return; - }; - for (handle, button_model) in handles.iter().zip(group.buttons.iter()) { - handle.button.set_sensitive(button_model.enabled); - if text_active_style { - set_active_class(&handle.button, button_model.enabled); - } - // Only the lock/freeze toggles change face with the snapshot. - if !matches!( - handle.event, - ToolbarEvent::ToggleZoomLock | ToolbarEvent::ToggleFreeze - ) { - continue; - } - match &handle.icon { - Some(icon) => icon.set_painter(action_icon(snapshot, &handle.event)), - None => handle - .button - .set_label(button_model.short_label(snapshot, NOUN)), - } - handle.button.set_tooltip_text(Some(&format_binding_label( - button_model.tooltip_label(snapshot, NOUN), - button_model.binding_hint(snapshot), - ))); - } - })); - container -} - -/// Handles the group updater needs to keep one button in sync. -struct ActionHandle { - button: gtk4::Button, - icon: Option, - event: ToolbarEvent, -} - -fn action_button( - ctx: &SectionCtx, - button_model: &model::ToolbarButtonModel, - text_active_style: bool, -) -> ActionHandle { - let snapshot = ctx.snapshot; - let (button, icon) = if ctx.use_icons { - let size = ctx.sz(32.0); - let button = sized_button(size, size); - let icon = IconWidget::new(action_icon(snapshot, &button_model.event), ctx.sz(18.0)); - button.set_child(Some(&icon.area)); - (button, Some(icon)) - } else { - let button = gtk4::Button::with_label(button_model.short_label(snapshot, NOUN)); - button.set_size_request(-1, ctx.px(24.0)); - button.set_hexpand(true); - (button, None) - }; - button.set_tooltip_text(Some(&format_binding_label( - button_model.tooltip_label(snapshot, NOUN), - button_model.binding_hint(snapshot), - ))); - button.set_sensitive(button_model.enabled); - if button_model.event.is_destructive() { - button.add_css_class("destructive"); - } - if text_active_style && button_model.enabled { - set_active_class(&button, true); - } - let sender = ctx.feedback.clone(); - let event = button_model.event.clone(); - button.connect_clicked(move |_| { - send_event(&sender, event.clone()); - }); - ActionHandle { - button, - icon, - event: button_model.event.clone(), - } -} - -/// Re-derive one group from a later snapshot; looked up by kind so a group -/// set change between snapshots never cross-wires the updaters. -fn group_of( - snapshot: &ToolbarSnapshot, - kind: model::ToolbarCommandGroupKind, -) -> Option { - model::ToolbarActionsModel::from_snapshot(snapshot)? - .groups() - .iter() - .find(|group| group.kind == kind) - .cloned() -} - -fn history_group(snapshot: &ToolbarSnapshot) -> Option { - group_of(snapshot, model::ToolbarCommandGroupKind::History) -} - -fn history_icon(event: &ToolbarEvent) -> IconPainter { - match event { - ToolbarEvent::Undo => toolbar_icons::draw_icon_undo, - ToolbarEvent::Redo => toolbar_icons::draw_icon_redo, - _ => toolbar_icons::draw_icon_clear, - } -} - -/// Port of the built-in `icon_for_button`: the zoom-lock and freeze -/// buttons swap their glyph with the snapshot state. -fn action_icon(snapshot: &ToolbarSnapshot, event: &ToolbarEvent) -> IconPainter { - match event { - ToolbarEvent::Undo => toolbar_icons::draw_icon_undo, - ToolbarEvent::Redo => toolbar_icons::draw_icon_redo, - ToolbarEvent::ClearCanvas { .. } => toolbar_icons::draw_icon_clear, - ToolbarEvent::ZoomIn => toolbar_icons::draw_icon_zoom_in, - ToolbarEvent::ZoomOut => toolbar_icons::draw_icon_zoom_out, - ToolbarEvent::ResetZoom => toolbar_icons::draw_icon_zoom_reset, - ToolbarEvent::ToggleZoomLock => { - if snapshot.zoom_locked { - toolbar_icons::draw_icon_lock - } else { - toolbar_icons::draw_icon_unlock - } - } - ToolbarEvent::UndoAll => toolbar_icons::draw_icon_undo_all, - ToolbarEvent::RedoAll => toolbar_icons::draw_icon_redo_all, - ToolbarEvent::UndoAllDelayed => toolbar_icons::draw_icon_undo_all_delay, - ToolbarEvent::RedoAllDelayed => toolbar_icons::draw_icon_redo_all_delay, - ToolbarEvent::ToggleFreeze => { - if snapshot.frozen_active { - toolbar_icons::draw_icon_unfreeze - } else { - toolbar_icons::draw_icon_freeze - } - } - _ => toolbar_icons::draw_icon_clear, - } -} diff --git a/src/toolbar_gtk/view/sections/arrow.rs b/src/toolbar_gtk/view/sections/arrow.rs deleted file mode 100644 index a65d5992..00000000 --- a/src/toolbar_gtk/view/sections/arrow.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Arrow labels section: "Auto-number" toggle, a "Next: N" header hint, -//! and a Reset button shown only while numbering is enabled. - -use std::cell::Cell; -use std::rc::Rc; - -use gtk4::prelude::*; - -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection}; - -use super::super::super::widgets::send_event; -use super::{SectionCard, SectionCtx, section_card}; - -pub(in crate::toolbar_gtk) fn build(ctx: &mut SectionCtx) -> Option { - let card = section_card( - ctx, - ToolbarSideSection::ArrowLabels, - ToolbarSideSection::ArrowLabels.label(), - ); - let hint = attach_header_hint( - &card, - &format!("Next: {}", ctx.snapshot.arrow_label_next), - ctx.snapshot.arrow_label_enabled, - ); - - let check = gtk4::CheckButton::with_label("Auto-number"); - check.add_css_class("mini"); - check.set_tooltip_text(Some("Auto-number arrows 1, 2, 3.")); - check.set_active(ctx.snapshot.arrow_label_enabled); - let sender = ctx.feedback.clone(); - let syncing = Rc::new(Cell::new(false)); - let toggle_sync = syncing.clone(); - check.connect_toggled(move |check| { - if !toggle_sync.get() { - send_event(&sender, ToolbarEvent::ToggleArrowLabels(check.is_active())); - } - }); - card.body.append(&check); - - let reset = gtk4::Button::with_label("Reset"); - reset.set_size_request(-1, ctx.px(24.0)); - reset.set_tooltip_text(Some("Reset numbering to 1.")); - reset.set_visible(ctx.snapshot.arrow_label_enabled); - let sender = ctx.feedback.clone(); - reset.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::ResetArrowLabelCounter); - }); - card.body.append(&reset); - - ctx.updaters.push(Box::new(move |snapshot| { - if check.is_active() != snapshot.arrow_label_enabled { - syncing.set(true); - check.set_active(snapshot.arrow_label_enabled); - syncing.set(false); - } - reset.set_visible(snapshot.arrow_label_enabled); - if let Some(hint) = &hint { - hint.set_text(&format!("Next: {}", snapshot.arrow_label_next)); - hint.set_visible(snapshot.arrow_label_enabled); - } - })); - Some(card.root.upcast()) -} - -/// Right-aligned counter hint on the card's header row, kept visible even -/// while the section is collapsed like the built-in palette draws it. -fn attach_header_hint(card: &SectionCard, text: &str, visible: bool) -> Option { - let header = card.root.first_child().and_downcast::()?; - let row = header.child().and_downcast::()?; - let title = row.first_child()?; - let hint = gtk4::Label::new(Some(text)); - hint.add_css_class("hint"); - hint.set_visible(visible); - row.insert_child_after(&hint, Some(&title)); - Some(hint) -} diff --git a/src/toolbar_gtk/view/sections/boards.rs b/src/toolbar_gtk/view/sections/boards.rs deleted file mode 100644 index 661cb695..00000000 --- a/src/toolbar_gtk/view/sections/boards.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Boards section: Prev / Next / New / Duplicate / Delete command row, -//! plus the board-picker keybinding hint the built-in shows. - -use gtk4::prelude::*; - -use crate::toolbar_icons; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection, model}; - -use super::super::super::icons::IconPainter; -use super::{SectionCtx, command_row, section_card}; - -pub(in crate::toolbar_gtk) fn build(ctx: &mut SectionCtx) -> Option { - let group = model::toolbar_boards_model(ctx.snapshot)?; - let card = section_card( - ctx, - ToolbarSideSection::Boards, - ToolbarSideSection::Boards.label(), - ); - let row = command_row( - ctx, - &group.buttons, - "Board", - board_icon, - model::toolbar_boards_model, - ); - card.body.append(&row); - // The built-in right-aligns the board-picker binding inside the section - // header; the GTK header is owned by `section_card`, so the hint sits - // right-aligned under the buttons instead. - if let Some(binding) = ctx - .snapshot - .binding_hints - .binding_for_event(&ToolbarEvent::ToggleBoardPicker) - { - let hint = gtk4::Label::new(Some(binding)); - hint.add_css_class("hint"); - hint.set_xalign(1.0); - card.body.append(&hint); - } - Some(card.root.upcast()) -} - -fn board_icon(event: &ToolbarEvent) -> IconPainter { - match event { - ToolbarEvent::BoardPrev => toolbar_icons::draw_icon_chevron_left, - ToolbarEvent::BoardNext => toolbar_icons::draw_icon_chevron_right, - ToolbarEvent::BoardNew => toolbar_icons::draw_icon_plus, - ToolbarEvent::BoardDuplicate => toolbar_icons::draw_icon_copy, - _ => toolbar_icons::draw_icon_clear, - } -} diff --git a/src/toolbar_gtk/view/sections/canvas_pane.rs b/src/toolbar_gtk/view/sections/canvas_pane.rs index fe36762f..13493f0a 100644 --- a/src/toolbar_gtk/view/sections/canvas_pane.rs +++ b/src/toolbar_gtk/view/sections/canvas_pane.rs @@ -1,8 +1,7 @@ -//! Canvas pane popover content: the Boards / Pages / Advanced / Zoom command -//! sections (each gated on its display toggle) and the Step Undo/Redo -//! configuration — the side Canvas pane re-homed into the top strip's Canvas -//! popover without the collapsible-card chrome. Most state (sections, step -//! counts, enabled/glyph faces) rides the popover host's content-key rebuild, +//! Canvas popover content: the Actions / Boards / Pages / Advanced / Zoom +//! command sections (each gated on its display toggle) and the Step Undo/Redo +//! configuration. Most state (sections, step counts, enabled/glyph faces) +//! rides the popover host's content-key rebuild, //! but the Step Undo/Redo section registers persistent slider updaters: the //! host retains them (in `canvas_updaters`) and runs them every apply, so a //! continuous delay-slider drag reads its live value without rebuilding the @@ -33,6 +32,9 @@ pub(in crate::toolbar_gtk) fn build_popover_content(ctx: &mut SectionCtx) -> gtk column.set_margin_start(pad_h); column.set_margin_end(pad_h); + if let Some(group) = model::toolbar_actions_model_for_popover(ctx.snapshot) { + command_section(ctx, &column, "Actions", "Action", &group, action_icon); + } if let Some(group) = model::toolbar_boards_model_for_popover(ctx.snapshot) { command_section(ctx, &column, "Boards", "Board", &group, board_icon); } @@ -149,9 +151,12 @@ fn page_icon(_snapshot: &ToolbarSnapshot, event: &ToolbarEvent) -> IconPainter { } /// Zoom/Advanced glyphs, with the zoom-lock and freeze faces following the -/// snapshot state (port of the side pane's `action_icon`). +/// snapshot state. fn action_icon(snapshot: &ToolbarSnapshot, event: &ToolbarEvent) -> IconPainter { match event { + ToolbarEvent::Undo => toolbar_icons::draw_icon_undo, + ToolbarEvent::Redo => toolbar_icons::draw_icon_redo, + ToolbarEvent::ClearCanvas { .. } => toolbar_icons::draw_icon_clear, ToolbarEvent::ZoomIn => toolbar_icons::draw_icon_zoom_in, ToolbarEvent::ZoomOut => toolbar_icons::draw_icon_zoom_out, ToolbarEvent::ResetZoom => toolbar_icons::draw_icon_zoom_reset, diff --git a/src/toolbar_gtk/view/sections/colors.rs b/src/toolbar_gtk/view/sections/colors.rs deleted file mode 100644 index d558b023..00000000 --- a/src/toolbar_gtk/view/sections/colors.rs +++ /dev/null @@ -1,762 +0,0 @@ -//! Colors section: HSV gradient picker, preview swatch + hex row, and the -//! quick-color swatch rows with the more/less palette toggle. - -use std::cell::Cell; -use std::f64::consts::PI; -use std::rc::Rc; - -/// Combines the current HSV triple with a drag position into a new triple. -type HsvApply = fn((f64, f64, f64), f64, f64) -> (f64, f64, f64); - -use gtk4::prelude::*; - -use crate::config::{Action, QuickColorPalette, QuickColorPaletteEntry}; -use crate::draw::Color; -use crate::draw::color::{hsv_to_rgb, rgb_to_hsv}; -use crate::input::state::{HEX_INPUT_MAX_CHARS, color_to_hex, parse_hex_color}; -use crate::label_format::format_quick_color_tooltip; -use crate::toolbar_icons; -use crate::ui::theme::toolbar::COLOR_TEXT_SECONDARY; -use crate::ui::theme::{Rgba, SHADOW_RGBA, set_color}; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection, ToolbarSnapshot}; - -use super::super::super::icons::IconPainter; -use super::super::super::widgets::{ - COLOR_SWATCH_HAIRLINE, COLOR_SWATCH_HAIRLINE_DARK, FeedbackSender, icon_button, - install_quick_color_recolor, rounded_rect_path, send_event, sized_button, -}; -use super::{SectionCtx, scoped_title, section_card}; - -/// Outline around the gradient picker areas (sat/val square, hue bar) and -/// the outer white ring of the position indicator dot; mirror the built-in -/// `render::widgets::color`. Specific tints — no theme token. -/// TODO(theme-consolidation): hoist these mirrored pairs into -/// `theme::toolbar` so the two frontends cannot drift. -const COLOR_PICKER_OUTLINE: Rgba = (1.0, 1.0, 1.0, 0.4); -const COLOR_INDICATOR_RING: Rgba = (1.0, 1.0, 1.0, 0.9); -/// Preview swatch idle outline: muted blue-gray with no theme token -/// (mirrors the built-in `side_palette::colors::helpers`). -const COLOR_PREVIEW_BORDER: Rgba = (0.5, 0.55, 0.6, 0.6); -/// Dark circle behind the expand arrow; deeper than SHADOW_RGBA so the -/// glyph stays legible over any swatch color. -const COLOR_EXPAND_ICON_BACKDROP: Rgba = (0.0, 0.0, 0.0, 0.4); - -// Spec units mirroring `ToolbarLayoutSpec::SIDE_COLOR_*`. -const SV_HEIGHT: f64 = 72.0; -const HUE_HEIGHT: f64 = 14.0; -const PREVIEW_SIZE: f64 = 28.0; -const PREVIEW_GAP_TOP: f64 = 10.0; -const PREVIEW_GAP_BOTTOM: f64 = 8.0; -const HEX_INPUT_WIDTH: f64 = 86.0; -const HEX_INPUT_HEIGHT: f64 = 20.0; -const EXPAND_ICON_SIZE: f64 = 8.0; -const SWATCH: f64 = 24.0; -const SWATCH_GAP: f64 = 6.0; -const SWATCHES_PER_ROW: usize = 6; -/// Body spacing already contributed by `section_card` between rows. -const BODY_SPACING: f64 = 6.0; - -/// `(color, label, bound quick-color action, palette slot index)` like the -/// built-in swatch rows. -type ColorSwatch = (Color, String, Option, usize); - -#[derive(Default)] -struct HexPasteRequests { - generation: Cell, -} - -impl HexPasteRequests { - fn invalidate(&self) { - self.generation.set(self.generation.get().wrapping_add(1)); - } - - fn begin(&self) -> u64 { - self.invalidate(); - self.generation.get() - } - - fn is_current(&self, generation: u64) -> bool { - self.generation.get() == generation - } -} - -pub(in crate::toolbar_gtk) fn build(ctx: &mut SectionCtx) -> Option { - let card = section_card( - ctx, - ToolbarSideSection::Colors, - &scoped_title("Color", ctx.snapshot), - ); - build_picker(ctx, &card.body); - build_preview_row(ctx, &card.body); - build_swatch_rows(ctx, &card.body); - Some(card.root.upcast()) -} - -// ===== HSV gradient picker ================================================= - -/// The 2-D saturation/value area plus the hue bar. Both share one HSV cell: -/// drags write it and emit `SetColorHsv`; snapshot updates only land when no -/// drag is in flight (same rule as `SliderRow`). -fn build_picker(ctx: &mut SectionCtx, body: >k4::Box) { - let hsv = Rc::new(Cell::new(effective_hsv(ctx.snapshot))); - let dragging = Rc::new(Cell::new(false)); - let scale = ctx.scale; - - let sv_area = gtk4::DrawingArea::new(); - sv_area.set_content_height(ctx.px(SV_HEIGHT)); - sv_area.set_hexpand(true); - let draw_hsv = hsv.clone(); - sv_area.set_draw_func(move |_, cr, width, height| { - let (h, s, v) = draw_hsv.get(); - let w = f64::from(width); - let hgt = f64::from(height); - draw_sat_val(cr, w, hgt, h); - draw_indicator(cr, s * w, (1.0 - v) * hgt, hsv_to_rgb(h, s, v), scale); - }); - - let hue_area = gtk4::DrawingArea::new(); - hue_area.set_content_height(ctx.px(HUE_HEIGHT)); - hue_area.set_hexpand(true); - let draw_hsv = hsv.clone(); - hue_area.set_draw_func(move |_, cr, width, height| { - let (h, _, _) = draw_hsv.get(); - let w = f64::from(width); - let hgt = f64::from(height); - draw_hue_bar(cr, w, hgt); - draw_indicator(cr, h * w, hgt / 2.0, hsv_to_rgb(h, 1.0, 1.0), scale); - }); - - // SV area varies s/v at the current hue; hue bar varies h keeping s/v. - attach_picker_drag( - &sv_area, - &hue_area, - &hsv, - &dragging, - ctx.feedback.clone(), - |(h, _, _), tx, ty| (h, tx, 1.0 - ty), - ); - attach_picker_drag( - &hue_area, - &sv_area, - &hsv, - &dragging, - ctx.feedback.clone(), - |(_, s, v), tx, _| (tx, s, v), - ); - - body.append(&sv_area); - body.append(&hue_area); - - let update_hsv = hsv.clone(); - let update_dragging = dragging.clone(); - ctx.updaters.push(Box::new(move |snapshot| { - if update_dragging.get() { - return; - } - let next = effective_hsv(snapshot); - if next != update_hsv.get() { - update_hsv.set(next); - sv_area.queue_draw(); - hue_area.queue_draw(); - } - })); -} - -/// Press-and-drag picking on one gradient area. `apply` folds the clamped -/// normalized pointer position into the shared HSV triple; the peer area -/// redraws too because hue changes recolor the SV gradient. -fn attach_picker_drag( - area: >k4::DrawingArea, - peer: >k4::DrawingArea, - hsv: &Rc>, - dragging: &Rc>, - sender: FeedbackSender, - apply: HsvApply, -) { - let apply_at: Rc = Rc::new({ - let hsv = hsv.clone(); - let area = area.clone(); - let peer = peer.clone(); - move |x: f64, y: f64| { - let w = f64::from(area.width().max(1)); - let hgt = f64::from(area.height().max(1)); - let (h, s, v) = apply( - hsv.get(), - (x / w).clamp(0.0, 1.0), - (y / hgt).clamp(0.0, 1.0), - ); - hsv.set((h, s, v)); - area.queue_draw(); - peer.queue_draw(); - send_event(&sender, ToolbarEvent::SetColorHsv { h, s, v }); - } - }); - - let drag = gtk4::GestureDrag::new(); - let start = Rc::new(Cell::new((0.0f64, 0.0f64))); - let begin_dragging = dragging.clone(); - let begin_start = start.clone(); - let begin_apply = apply_at.clone(); - drag.connect_drag_begin(move |_, x, y| { - begin_dragging.set(true); - begin_start.set((x, y)); - begin_apply(x, y); - }); - let update_apply = apply_at.clone(); - drag.connect_drag_update(move |_, dx, dy| { - let (sx, sy) = start.get(); - update_apply(sx + dx, sy + dy); - }); - let end_dragging = dragging.clone(); - drag.connect_drag_end(move |_, _, _| { - end_dragging.set(false); - }); - area.add_controller(drag); -} - -/// HSV triple the picker should display. The remembered picker triple wins -/// while it still resolves to the current color, so hue and saturation stay -/// put when the RGB value collapses to gray/black; any other color source -/// (swatch, hex, preset) falls back to a plain RGB→HSV conversion. -fn effective_hsv(snapshot: &ToolbarSnapshot) -> (f64, f64, f64) { - if let Some((h, s, v)) = snapshot.picker_hsv { - let remembered = hsv_to_rgb(h, s, v); - let current = snapshot.color; - if (remembered.r - current.r).abs() < 1e-3 - && (remembered.g - current.g).abs() < 1e-3 - && (remembered.b - current.b).abs() < 1e-3 - { - return (h, s, v); - } - } - rgb_to_hsv(snapshot.color.r, snapshot.color.g, snapshot.color.b) -} - -fn draw_sat_val(cr: &cairo::Context, w: f64, h: f64, hue: f64) { - let hue_color = hsv_to_rgb(hue, 1.0, 1.0); - - let sat_grad = cairo::LinearGradient::new(0.0, 0.0, w, 0.0); - sat_grad.add_color_stop_rgba(0.0, 1.0, 1.0, 1.0, 1.0); - sat_grad.add_color_stop_rgba(1.0, hue_color.r, hue_color.g, hue_color.b, 1.0); - cr.rectangle(0.0, 0.0, w, h); - let _ = cr.set_source(&sat_grad); - let _ = cr.fill(); - - let val_grad = cairo::LinearGradient::new(0.0, 0.0, 0.0, h); - val_grad.add_color_stop_rgba(0.0, 0.0, 0.0, 0.0, 0.0); - val_grad.add_color_stop_rgba(1.0, 0.0, 0.0, 0.0, 1.0); - cr.rectangle(0.0, 0.0, w, h); - let _ = cr.set_source(&val_grad); - let _ = cr.fill(); - - stroke_picker_border(cr, w, h); -} - -fn draw_hue_bar(cr: &cairo::Context, w: f64, h: f64) { - let hue_grad = cairo::LinearGradient::new(0.0, 0.0, w, 0.0); - hue_grad.add_color_stop_rgba(0.0, 1.0, 0.0, 0.0, 1.0); - hue_grad.add_color_stop_rgba(0.17, 1.0, 1.0, 0.0, 1.0); - hue_grad.add_color_stop_rgba(0.33, 0.0, 1.0, 0.0, 1.0); - hue_grad.add_color_stop_rgba(0.5, 0.0, 1.0, 1.0, 1.0); - hue_grad.add_color_stop_rgba(0.66, 0.0, 0.0, 1.0, 1.0); - hue_grad.add_color_stop_rgba(0.83, 1.0, 0.0, 1.0, 1.0); - hue_grad.add_color_stop_rgba(1.0, 1.0, 0.0, 0.0, 1.0); - cr.rectangle(0.0, 0.0, w, h); - let _ = cr.set_source(&hue_grad); - let _ = cr.fill(); - - stroke_picker_border(cr, w, h); -} - -fn stroke_picker_border(cr: &cairo::Context, w: f64, h: f64) { - set_color(cr, COLOR_PICKER_OUTLINE); - cr.rectangle(0.5, 0.5, w - 1.0, h - 1.0); - cr.set_line_width(1.0); - let _ = cr.stroke(); -} - -/// The color indicator dot the built-in picker draws at the current value. -fn draw_indicator(cr: &cairo::Context, x: f64, y: f64, color: Color, scale: f64) { - let radius = 5.0 * scale; - let ring = radius + 1.5 * scale; - - set_color(cr, COLOR_INDICATOR_RING); - cr.arc(x, y, ring, 0.0, PI * 2.0); - let _ = cr.fill(); - - cr.set_source_rgba(color.r, color.g, color.b, 1.0); - cr.arc(x, y, radius, 0.0, PI * 2.0); - let _ = cr.fill(); - - set_color(cr, SHADOW_RGBA); - cr.set_line_width(1.0); - cr.arc(x, y, ring, 0.0, PI * 2.0); - let _ = cr.stroke(); -} - -// ===== Preview swatch + hex row ============================================ - -fn build_preview_row(ctx: &mut SectionCtx, body: >k4::Box) { - let row = gtk4::Box::new(gtk4::Orientation::Horizontal, ctx.px(4.0)); - row.set_margin_top(ctx.px(PREVIEW_GAP_TOP - BODY_SPACING)); - - // Current-color swatch with the expand-arrow badge; opens the popup. - let preview = sized_button(ctx.sz(PREVIEW_SIZE), ctx.sz(PREVIEW_SIZE)); - preview.add_css_class("swatch"); - preview.set_tooltip_text(Some("Click to pick color")); - // The whole color drives the redraw, alpha included: an alpha-only change - // still repaints, and the swatch shows the transparency it will draw with. - let color_cell = Rc::new(Cell::new(ctx.snapshot.color)); - let preview_area = gtk4::DrawingArea::new(); - preview_area.set_content_width(ctx.px(PREVIEW_SIZE)); - preview_area.set_content_height(ctx.px(PREVIEW_SIZE)); - preview_area.set_can_target(false); - let scale = ctx.scale; - let draw_color = color_cell.clone(); - preview_area.set_draw_func(move |_, cr, width, height| { - let size = f64::from(width.min(height)); - draw_preview_swatch(cr, size, draw_color.get(), scale); - }); - preview.set_child(Some(&preview_area)); - let sender = ctx.feedback.clone(); - preview.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::OpenColorPickerPopup); - }); - row.append(&preview); - ctx.updaters.push(Box::new(move |snapshot| { - if color_cell.get() != snapshot.color { - color_cell.set(snapshot.color); - preview_area.queue_draw(); - } - })); - - // Hex entry: focus selects the whole token, Enter applies typed input, and - // paste atomically replaces and immediately applies a valid color. - // Snapshot updates never overwrite the text while the user is editing. - let entry = gtk4::Entry::new(); - super::super::super::widgets::keyboard_on_demand_for_entry(&entry); - entry.set_text(&color_to_hex(ctx.snapshot.color)); - // Sized for `#RRGGBBAA`: a shorter limit would truncate a translucent - // color's readout and make its alpha digits untypable. - let hex_chars = i32::try_from(HEX_INPUT_MAX_CHARS).unwrap_or(i32::MAX); - entry.set_width_chars(hex_chars); - entry.set_max_length(hex_chars); - gtk4::prelude::EditableExt::set_alignment(&entry, 0.5); - entry.set_size_request(ctx.px(HEX_INPUT_WIDTH), ctx.px(HEX_INPUT_HEIGHT)); - entry.set_valign(gtk4::Align::Center); - entry.set_margin_start(ctx.px(4.0)); - entry.set_tooltip_text(Some( - "Type a hex color (Enter applies); paste applies immediately", - )); - let sender = ctx.feedback.clone(); - entry.connect_activate(move |entry| { - if let Some(color) = parse_hex_color(&entry.text()) { - send_event(&sender, ToolbarEvent::SetColor(color)); - } - }); - install_hex_paste_replacement(&entry, ctx.feedback.clone()); - row.append(&entry); - let entry_handle = entry.clone(); - ctx.updaters.push(Box::new(move |snapshot| { - if entry_handle.has_focus() - || entry_handle - .state_flags() - .contains(gtk4::StateFlags::FOCUS_WITHIN) - { - return; - } - let hex = color_to_hex(snapshot.color); - if entry_handle.text() != hex { - entry_handle.set_text(&hex); - } - })); - - let copy = icon_button( - toolbar_icons::draw_icon_copy, - (ctx.sz(HEX_INPUT_HEIGHT), ctx.sz(HEX_INPUT_HEIGHT)), - ctx.sz(10.0), - "Copy hex color", - ); - let sender = ctx.feedback.clone(); - copy.button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::CopyHexColor); - }); - row.append(©.button); - - let paste = icon_button( - toolbar_icons::draw_icon_paste, - (ctx.sz(HEX_INPUT_HEIGHT), ctx.sz(HEX_INPUT_HEIGHT)), - ctx.sz(12.0), - "Paste hex color from clipboard", - ); - let sender = ctx.feedback.clone(); - paste.button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::PasteHexColor); - }); - row.append(&paste.button); - - let eyedropper = icon_button( - toolbar_icons::draw_icon_eyedropper, - (ctx.sz(HEX_INPUT_HEIGHT), ctx.sz(HEX_INPUT_HEIGHT)), - ctx.sz(12.0), - "Pick color from screen", - ); - let sender = ctx.feedback.clone(); - eyedropper.button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::PickScreenColor); - }); - row.append(&eyedropper.button); - - body.append(&row); -} - -fn install_hex_paste_replacement(entry: >k4::Entry, sender: FeedbackSender) { - let Some(text) = entry.delegate().and_downcast::() else { - log::warn!("GTK hex entry has no text delegate; using default paste behavior"); - return; - }; - let paste_requests = Rc::new(HexPasteRequests::default()); - let changed_paste_requests = paste_requests.clone(); - entry.connect_changed(move |_| { - changed_paste_requests.invalidate(); - }); - let weak_entry = entry.downgrade(); - let clipboard_sender = sender.clone(); - let clipboard_paste_requests = paste_requests.clone(); - text.connect_paste_clipboard(move |text| { - // GtkText owns Ctrl+V and the native context-menu Paste action. Stop - // its insertion semantics because a color is one token, not text to - // splice at the caret. - text.stop_signal_emission_by_name("paste-clipboard"); - let Some(entry) = weak_entry.upgrade() else { - return; - }; - read_hex_paste( - &entry, - &text.display().clipboard(), - clipboard_sender.clone(), - clipboard_paste_requests.clone(), - ); - }); - - // GtkText handles PRIMARY-selection paste directly from its middle-click - // gesture instead of emitting `paste-clipboard`. Claim that gesture before - // it reaches the delegate and route it through the same replacement path. - let primary_paste = gtk4::GestureClick::new(); - primary_paste.set_button(gtk4::gdk::BUTTON_MIDDLE); - primary_paste.set_propagation_phase(gtk4::PropagationPhase::Capture); - primary_paste.connect_pressed(move |gesture, _, _, _| { - let Some(entry) = gesture.widget().and_downcast::() else { - gesture.set_state(gtk4::EventSequenceState::Denied); - return; - }; - if !entry.settings().is_gtk_enable_primary_paste() { - gesture.set_state(gtk4::EventSequenceState::Denied); - return; - } - gesture.set_state(gtk4::EventSequenceState::Claimed); - entry.grab_focus(); - read_hex_paste( - &entry, - &entry.primary_clipboard(), - sender.clone(), - paste_requests.clone(), - ); - }); - entry.add_controller(primary_paste); -} - -fn read_hex_paste( - entry: >k4::Entry, - clipboard: >k4::gdk::Clipboard, - sender: FeedbackSender, - paste_requests: Rc, -) { - let generation = paste_requests.begin(); - let weak_entry = entry.downgrade(); - clipboard.read_text_async(None::<>k4::gio::Cancellable>, move |result| { - let Some(entry) = weak_entry.upgrade() else { - return; - }; - if !paste_requests.is_current(generation) { - return; - } - let parsed = result - .ok() - .flatten() - .and_then(|value| parse_hex_color(&value)); - let Some(color) = parsed else { - entry.select_region(0, -1); - return; - }; - entry.set_text(&color_to_hex(color)); - entry.select_region(0, -1); - send_event(&sender, ToolbarEvent::SetColor(color)); - }); -} - -/// Rounded-square preview like the built-in `draw_swatch`, drawn one pixel -/// inset so the resting outline stays inside the widget, with the popup -/// expand-arrow badge in the bottom-right corner. -fn draw_preview_swatch(cr: &cairo::Context, size: f64, color: Color, scale: f64) { - let Color { r, g, b, a } = color; - - set_color(cr, COLOR_PREVIEW_BORDER); - cr.set_line_width(1.0); - rounded_rect_path(cr, 0.5, 0.5, size - 1.0, size - 1.0, 5.0); - let _ = cr.stroke(); - - let fill_path = - |cr: &cairo::Context| rounded_rect_path(cr, 1.5, 1.5, size - 3.0, size - 3.0, 4.0); - crate::ui::checkerboard_behind(cr, a, fill_path); - cr.set_source_rgba(r, g, b, a); - fill_path(cr); - let _ = cr.fill(); - - let luminance = 0.299 * r + 0.587 * g + 0.114 * b; - if luminance < 0.3 { - set_color(cr, COLOR_SWATCH_HAIRLINE_DARK); - cr.set_line_width(1.5); - rounded_rect_path(cr, 1.5, 1.5, size - 3.0, size - 3.0, 4.0); - let _ = cr.stroke(); - } - - let icon = EXPAND_ICON_SIZE * scale; - let icon_x = size - icon - 2.0; - let icon_y = size - icon - 2.0; - set_color(cr, COLOR_EXPAND_ICON_BACKDROP); - cr.arc( - icon_x + icon / 2.0, - icon_y + icon / 2.0, - icon / 2.0 + 1.0, - 0.0, - PI * 2.0, - ); - let _ = cr.fill(); - set_color(cr, COLOR_TEXT_SECONDARY); - cr.set_line_width(1.2); - cr.set_line_cap(cairo::LineCap::Round); - let margin = icon * 0.2; - let arrow_x1 = icon_x + margin; - let arrow_y1 = icon_y + icon - margin; - let arrow_x2 = icon_x + icon - margin; - let arrow_y2 = icon_y + margin; - cr.move_to(arrow_x1, arrow_y1); - cr.line_to(arrow_x2, arrow_y2); - let _ = cr.stroke(); - let head = icon * 0.3; - cr.move_to(arrow_x2 - head, arrow_y2); - cr.line_to(arrow_x2, arrow_y2); - cr.line_to(arrow_x2, arrow_y2 + head); - let _ = cr.stroke(); -} - -// ===== Quick-color swatch rows ============================================= - -fn build_swatch_rows(ctx: &mut SectionCtx, body: >k4::Box) { - let snapshot = ctx.snapshot; - let compact = compact_palette_swatches(&snapshot.quick_colors); - let expanded = expanded_palette_swatches(&snapshot.quick_colors); - - let column = gtk4::Box::new(gtk4::Orientation::Vertical, ctx.px(SWATCH_GAP)); - column.set_margin_top(ctx.px(PREVIEW_GAP_BOTTOM - BODY_SPACING)); - let mut tracked: Vec<(Color, Rc>, gtk4::DrawingArea)> = Vec::new(); - - let compact_toggle: Option<(ToolbarEvent, &'static str, IconPainter)> = - if snapshot.show_more_colors || expanded.is_empty() { - None - } else { - Some(( - ToolbarEvent::ToggleMoreColors(true), - "More colors", - toolbar_icons::draw_icon_plus, - )) - }; - column.append(&swatch_row(ctx, &compact, compact_toggle, &mut tracked)); - - if snapshot.show_more_colors { - let rows = expanded.chunks(SWATCHES_PER_ROW).collect::>(); - let row_count = rows.len(); - for (row_index, row_colors) in rows.iter().enumerate() { - let toggle = (row_index + 1 == row_count).then_some(( - ToolbarEvent::ToggleMoreColors(false), - "Hide colors", - toolbar_icons::draw_icon_minus as IconPainter, - )); - column.append(&swatch_row(ctx, row_colors, toggle, &mut tracked)); - } - } - body.append(&column); - - ctx.updaters.push(Box::new(move |snapshot| { - for (color, selected, area) in &tracked { - let now = *color == snapshot.color; - if selected.get() != now { - selected.set(now); - area.queue_draw(); - } - } - })); -} - -fn swatch_row( - ctx: &SectionCtx, - colors: &[ColorSwatch], - toggle: Option<(ToolbarEvent, &'static str, IconPainter)>, - tracked: &mut Vec<(Color, Rc>, gtk4::DrawingArea)>, -) -> gtk4::Box { - let row = gtk4::Box::new(gtk4::Orientation::Horizontal, ctx.px(SWATCH_GAP)); - for (color, name, action, index) in colors { - let action = *action; - let index = *index; - let binding = - action.and_then(|action| ctx.snapshot.binding_hints.binding_for_action(action)); - let tooltip = format_quick_color_tooltip(name, binding); - let color = *color; - let (button, selected, area) = - rect_swatch(ctx, color, color == ctx.snapshot.color, &tooltip); - let sender = ctx.feedback.clone(); - button.connect_clicked(move |_| { - send_event( - &sender, - ToolbarEvent::SetQuickColor { - color, - action, - index, - }, - ); - }); - install_quick_color_recolor(&button, index, &ctx.feedback); - row.append(&button); - tracked.push((color, selected, area)); - } - - if let Some((event, tooltip, painter)) = toggle { - let toggle_button = icon_button( - painter, - (ctx.sz(SWATCH), ctx.sz(SWATCH)), - ctx.sz(14.0), - tooltip, - ); - let sender = ctx.feedback.clone(); - toggle_button.button.connect_clicked(move |_| { - send_event(&sender, event.clone()); - }); - row.append(&toggle_button.button); - } - row -} - -/// Rounded-square quick-color swatch mirroring the built-in `draw_swatch`: -/// fill with a subtle inner hairline (boosted for low-luminance colors) and -/// the 2px accent selection ring with a ~2px gap (drawn inset because the -/// built-in paints it outside the bounds). -fn rect_swatch( - ctx: &SectionCtx, - color: Color, - selected: bool, - tooltip: &str, -) -> (gtk4::Button, Rc>, gtk4::DrawingArea) { - let button = sized_button(ctx.sz(SWATCH), ctx.sz(SWATCH)); - button.add_css_class("swatch"); - button.set_tooltip_text(Some(tooltip)); - let selected_cell = Rc::new(Cell::new(selected)); - let area = gtk4::DrawingArea::new(); - area.set_content_width(ctx.px(SWATCH)); - area.set_content_height(ctx.px(SWATCH)); - area.set_can_target(false); - let draw_selected = selected_cell.clone(); - area.set_draw_func(move |_, cr, width, height| { - let size = f64::from(width.min(height)); - let fill_path = - |cr: &cairo::Context| rounded_rect_path(cr, 4.0, 4.0, size - 8.0, size - 8.0, 4.0); - crate::ui::checkerboard_behind(cr, color.a, fill_path); - cr.set_source_rgba(color.r, color.g, color.b, color.a); - fill_path(cr); - let _ = cr.fill(); - - let luminance = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b; - if luminance < 0.3 { - set_color(cr, COLOR_SWATCH_HAIRLINE_DARK); - } else { - set_color(cr, COLOR_SWATCH_HAIRLINE); - } - cr.set_line_width(1.0); - rounded_rect_path(cr, 4.5, 4.5, size - 9.0, size - 9.0, 3.5); - let _ = cr.stroke(); - - if draw_selected.get() { - set_color(cr, super::super::super::css::ACCENT); - cr.set_line_width(2.0); - rounded_rect_path(cr, 1.0, 1.0, size - 2.0, size - 2.0, 6.0); - let _ = cr.stroke(); - } - }); - button.set_child(Some(&area)); - (button, selected_cell, area) -} - -// ===== Palette selection (ported from the built-in colors section) ======== - -/// Legacy one-click colors keep their compact-row spots (Red, Green, Blue, -/// Yellow, White, Black in the default palette). -const COMPACT_PALETTE_INDICES: [usize; SWATCHES_PER_ROW] = [0, 1, 2, 3, 6, 7]; - -fn compact_palette_swatches(palette: &QuickColorPalette) -> Vec { - let entries = palette.rendered_entries(); - compact_palette_indices(palette) - .into_iter() - .filter_map(|index| { - entries - .get(index) - .map(|entry| palette_swatch((index, entry))) - }) - .collect() -} - -fn compact_palette_indices(palette: &QuickColorPalette) -> Vec { - let mut indices = Vec::with_capacity(SWATCHES_PER_ROW); - let rendered_len = palette.rendered_len(); - for index in COMPACT_PALETTE_INDICES { - if index < rendered_len { - indices.push(index); - } - } - - if indices.len() < SWATCHES_PER_ROW { - for index in 0..rendered_len { - if indices.contains(&index) { - continue; - } - indices.push(index); - if indices.len() == SWATCHES_PER_ROW { - break; - } - } - } - - indices -} - -fn expanded_palette_swatches(palette: &QuickColorPalette) -> Vec { - let compact_indices = compact_palette_indices(palette); - palette - .rendered_entries() - .iter() - .enumerate() - .filter(|(index, _)| !compact_indices.contains(index)) - .map(palette_swatch) - .collect() -} - -fn palette_swatch((index, entry): (usize, &QuickColorPaletteEntry)) -> ColorSwatch { - ( - entry.color, - entry.label.clone(), - QuickColorPalette::action_for_index(index), - index, - ) -} - -#[cfg(test)] -mod tests; diff --git a/src/toolbar_gtk/view/sections/colors/tests.rs b/src/toolbar_gtk/view/sections/colors/tests.rs deleted file mode 100644 index 10f36b55..00000000 --- a/src/toolbar_gtk/view/sections/colors/tests.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! GTK colors-section unit tests. - -use super::*; -use crate::toolbar_gtk::GtkToolbarFeedback; -use crate::toolbar_gtk::widgets::emit_secondary_press; -use crate::ui::toolbar::ToolbarBindingHints; -use std::sync::mpsc::channel; - -/// Both palette gestures on one GTK swatch: the primary click selects the -/// slot, the secondary press opens its recolor popup. Widget construction -/// needs a GTK display, so the body runs in an isolated child process the -/// same way the top-strip widget contract test does. -#[test] -fn side_palette_swatch_selects_on_primary_and_recolors_on_secondary() { - const CHILD_ENV: &str = "WAYSCRIBER_GTK_SIDE_SWATCH_CHILD"; - const TEST_NAME: &str = "toolbar_gtk::view::sections::colors::tests::side_palette_swatch_selects_on_primary_and_recolors_on_secondary"; - - if std::env::var_os(CHILD_ENV).is_none() { - let status = std::process::Command::new(std::env::current_exe().expect("test binary")) - .arg(TEST_NAME) - .arg("--exact") - .arg("--test-threads=1") - .env(CHILD_ENV, "1") - .status() - .expect("run isolated GTK side-swatch test"); - assert!(status.success(), "isolated GTK side-swatch test failed"); - return; - } - - if let Err(error) = gtk4::init() { - eprintln!("skipping GTK side-swatch test: {error}"); - return; - } - - let state = crate::input::state::test_support::make_test_input_state(); - let snapshot = ToolbarSnapshot::from_input_with_bindings( - &state, - ToolbarBindingHints::from_input_state(&state), - ); - let (tx, rx) = channel::(); - let mut updaters: Vec = Vec::new(); - let ctx = SectionCtx { - snapshot: &snapshot, - feedback: FeedbackSender::new(tx), - scale: 1.0, - use_icons: true, - updaters: &mut updaters, - }; - - // The compact row reorders the palette, so take its first swatch and - // assert both gestures name the slot it actually draws. - let swatches = compact_palette_swatches(&snapshot.quick_colors); - let (color, _, action, index) = swatches.first().expect("compact swatch").clone(); - let mut tracked = Vec::new(); - let row = swatch_row(&ctx, &swatches[..1], None, &mut tracked); - let button = row - .first_child() - .expect("swatch widget") - .downcast::() - .expect("swatch button"); - - button.emit_clicked(); - assert_eq!( - rx.recv().expect("primary click event"), - GtkToolbarFeedback::Event { - event: ToolbarEvent::SetQuickColor { - color, - action, - index, - }, - rebind_requested: false, - } - ); - - emit_secondary_press(button.upcast_ref()); - assert_eq!( - rx.recv().expect("secondary click event"), - GtkToolbarFeedback::Event { - event: ToolbarEvent::EditQuickColor { index }, - rebind_requested: false, - } - ); - - assert!( - button - .tooltip_text() - .is_some_and(|text| text.contains("right-click to recolor")), - "the swatch advertises the recolor gesture" - ); -} - -#[test] -fn newer_hex_paste_request_supersedes_pending_callback() { - let requests = HexPasteRequests::default(); - let older = requests.begin(); - let newer = requests.begin(); - - assert!(!requests.is_current(older)); - assert!(requests.is_current(newer)); -} - -#[test] -fn editing_hex_text_invalidates_pending_paste_callback() { - let requests = HexPasteRequests::default(); - let pending = requests.begin(); - - requests.invalidate(); - - assert!(!requests.is_current(pending)); -} diff --git a/src/toolbar_gtk/view/sections/marker.rs b/src/toolbar_gtk/view/sections/marker.rs deleted file mode 100644 index d36a4d6a..00000000 --- a/src/toolbar_gtk/view/sections/marker.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! Marker opacity section: minus / drag-safe slider / plus with a "{n}%" -//! readout, mirroring the built-in nudge step and range. - -use gtk4::prelude::*; - -use crate::toolbar_icons; -use crate::ui::toolbar::model::ToolbarSliderSpec; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection}; - -use super::super::super::widgets::{SliderRow, icon_button, send_event}; -use super::{SectionCtx, section_card}; - -pub(in crate::toolbar_gtk) fn build(ctx: &mut SectionCtx) -> Option { - let card = section_card( - ctx, - ToolbarSideSection::MarkerOpacity, - ToolbarSideSection::MarkerOpacity.label(), - ); - - let spec = ToolbarSliderSpec::MARKER_OPACITY; - let step = spec.step.unwrap_or(0.05); - // Built-in SIDE_NUDGE_SIZE / SIDE_NUDGE_ICON_SIZE. - let btn = ctx.sz(24.0); - let icon = ctx.sz(14.0); - let row = gtk4::Box::new(gtk4::Orientation::Horizontal, ctx.px(6.0)); - - let minus = icon_button( - toolbar_icons::draw_icon_minus, - (btn, btn), - icon, - "Decrease opacity", - ); - let sender = ctx.feedback.clone(); - minus.button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::NudgeMarkerOpacity(-step)); - }); - row.append(&minus.button); - - let sender = ctx.feedback.clone(); - let slider = SliderRow::new( - ctx.scale, - (spec.min, spec.max), - ctx.snapshot.marker_opacity, - format_percent, - move |value| { - send_event(&sender, ToolbarEvent::SetMarkerOpacity(value)); - }, - ); - slider.root.set_hexpand(true); - row.append(&slider.root); - - let plus = icon_button( - toolbar_icons::draw_icon_plus, - (btn, btn), - icon, - "Increase opacity", - ); - let sender = ctx.feedback.clone(); - plus.button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::NudgeMarkerOpacity(step)); - }); - row.append(&plus.button); - - card.body.append(&row); - ctx.updaters.push(Box::new(move |snapshot| { - slider.set_value(snapshot.marker_opacity); - })); - Some(card.root.upcast()) -} - -fn format_percent(value: f64) -> String { - format!("{:.0}%", value * 100.0) -} diff --git a/src/toolbar_gtk/view/sections/mod.rs b/src/toolbar_gtk/view/sections/mod.rs index bb06c318..f2280f0f 100644 --- a/src/toolbar_gtk/view/sections/mod.rs +++ b/src/toolbar_gtk/view/sections/mod.rs @@ -1,33 +1,14 @@ -//! Side-palette pane content: one module per section, dispatched in the -//! user's configured order with the same gating the built-in palette uses -//! (`ToolContext` + per-section hidden flags + block grouping). +//! Content builders for the top strip's Canvas, Session, and Settings +//! popovers. -mod actions; -mod arrow; -mod boards; pub(in crate::toolbar_gtk) mod canvas_pane; -mod colors; -mod marker; -mod pages; -mod presets; pub(in crate::toolbar_gtk) mod session_pane; pub(in crate::toolbar_gtk) mod settings_pane; -mod step_marker; mod step_undo; -mod text_controls; -mod thickness; -use gtk4::prelude::*; +use crate::ui::toolbar::ToolbarSnapshot; -use crate::label_format::format_binding_label; -use crate::toolbar_icons; -use crate::ui::toolbar::snapshot::ToolContext; -use crate::ui::toolbar::{ - SidePane, ToolbarEvent, ToolbarSideSection, ToolbarSnapshot, bindings, model, -}; - -use super::super::icons::IconPainter; -use super::super::widgets::{FeedbackSender, send_event, sized_button}; +use super::super::widgets::FeedbackSender; use super::Updater; /// Everything a section builder needs; `updaters` collects the closures @@ -49,236 +30,3 @@ impl SectionCtx<'_> { (value * self.scale).round() as i32 } } - -/// Scope a section title to the tool it currently edits ("Color — Pen"), -/// mirroring the built-in `scoped_title`. -pub(in crate::toolbar_gtk) fn scoped_title(base: &str, snapshot: &ToolbarSnapshot) -> String { - if !snapshot.context_aware_ui { - return base.to_string(); - } - let scope = if snapshot.text_active { - "Text" - } else if snapshot.note_active { - "Note" - } else { - bindings::tool_label(snapshot.active_tool) - }; - format!("{base} — {scope}") -} - -/// Collapsible group card. The header toggles the section's collapsed -/// flag; the body is only built when expanded (collapse changes are -/// structural, so a rebuild follows the toggle). -pub(in crate::toolbar_gtk) struct SectionCard { - pub(in crate::toolbar_gtk) root: gtk4::Box, - pub(in crate::toolbar_gtk) body: gtk4::Box, -} - -pub(in crate::toolbar_gtk) fn section_card( - ctx: &SectionCtx, - section: ToolbarSideSection, - title: &str, -) -> SectionCard { - let collapsed = ctx.snapshot.collapsed_side_sections.contains(§ion); - let root = gtk4::Box::new(gtk4::Orientation::Vertical, ctx.px(4.0)); - root.add_css_class("card"); - - let header = gtk4::Button::new(); - header.add_css_class("section-header"); - header.set_tooltip_text(Some(&format!( - "{} {}", - if collapsed { "Expand" } else { "Collapse" }, - title - ))); - let header_row = gtk4::Box::new(gtk4::Orientation::Horizontal, ctx.px(4.0)); - let label = gtk4::Label::new(Some(title)); - label.add_css_class("section-title"); - label.set_xalign(0.0); - label.set_hexpand(true); - header_row.append(&label); - let chevron = super::super::icons::IconWidget::new( - if collapsed { - toolbar_icons::draw_icon_chevron_right - } else { - toolbar_icons::draw_icon_chevron_down - }, - ctx.sz(12.0), - ); - header_row.append(&chevron.area); - header.set_child(Some(&header_row)); - let sender = ctx.feedback.clone(); - header.connect_clicked(move |_| { - send_event( - &sender, - ToolbarEvent::ToggleSideSectionCollapsed(section, !collapsed), - ); - }); - root.append(&header); - - let body = gtk4::Box::new(gtk4::Orientation::Vertical, ctx.px(6.0)); - body.set_visible(!collapsed); - root.append(&body); - SectionCard { root, body } -} - -/// Command row shared by Boards/Pages/Actions: icon mode packs plain -/// buttons left and destructive ones right; text mode uses an equal-width -/// grid. Enabled state tracks later snapshots through an updater. -pub(in crate::toolbar_gtk) fn command_row( - ctx: &mut SectionCtx, - buttons: &[model::ToolbarButtonModel], - noun: &'static str, - icon_for: fn(&ToolbarEvent) -> IconPainter, - model_fn: fn(&ToolbarSnapshot) -> Option, -) -> gtk4::Box { - let row = gtk4::Box::new(gtk4::Orientation::Horizontal, ctx.px(6.0)); - row.set_homogeneous(!ctx.use_icons); - let btn_h = if ctx.use_icons { - ctx.sz(32.0) - } else { - ctx.sz(24.0) - }; - let mut handles: Vec = Vec::new(); - let mut spacer_added = false; - for button_model in buttons { - let button = if ctx.use_icons { - let handle = sized_button(btn_h, btn_h); - let icon = - super::super::icons::IconWidget::new(icon_for(&button_model.event), ctx.sz(18.0)); - handle.set_child(Some(&icon.area)); - handle - } else { - let handle = gtk4::Button::with_label(button_model.short_label(ctx.snapshot, noun)); - handle.set_size_request(-1, btn_h.round() as i32); - handle - }; - button.set_tooltip_text(Some(&format_binding_label( - button_model.tooltip_label(ctx.snapshot, noun), - button_model.binding_hint(ctx.snapshot), - ))); - button.set_sensitive(button_model.enabled); - if button_model.event.is_destructive() { - button.add_css_class("destructive"); - if ctx.use_icons && !spacer_added { - // Guard gap: destructive buttons sit apart on the right. - let spacer = gtk4::Box::new(gtk4::Orientation::Horizontal, 0); - spacer.set_hexpand(true); - row.append(&spacer); - spacer_added = true; - } - } - let sender = ctx.feedback.clone(); - let event = button_model.event.clone(); - button.connect_clicked(move |_| { - send_event(&sender, event.clone()); - }); - row.append(&button); - handles.push(button); - } - ctx.updaters.push(Box::new(move |snapshot| { - if let Some(group) = model_fn(snapshot) { - for (handle, button_model) in handles.iter().zip(group.buttons.iter()) { - handle.set_sensitive(button_model.enabled); - } - } - })); - row -} - -/// Build the active pane's content, mirroring the built-in dispatch: -/// Session and Settings are whole panes; Draw and Canvas run their -/// sections in the user's order with `ToolContext` gating and the -/// thickness/text block grouping. -pub(in crate::toolbar_gtk) fn build_pane_content( - snapshot: &ToolbarSnapshot, - feedback: FeedbackSender, - scale: f64, - updaters: &mut Vec, -) -> gtk4::Box { - let content = gtk4::Box::new(gtk4::Orientation::Vertical, (12.0 * scale).round() as i32); - let mut ctx = SectionCtx { - snapshot, - feedback, - scale, - use_icons: snapshot.use_icons, - updaters, - }; - match snapshot.active_side_pane { - SidePane::Session => { - if let Some(widget) = session_pane::build(&mut ctx) { - content.append(&widget); - } - } - SidePane::Settings => { - if let Some(widget) = settings_pane::build(&mut ctx) { - content.append(&widget); - } - } - SidePane::Draw | SidePane::Canvas => { - build_pane_sections(&mut ctx, &content); - } - } - content -} - -fn build_pane_sections(ctx: &mut SectionCtx, content: >k4::Box) { - let snapshot = ctx.snapshot; - let tool_context = ToolContext::from_snapshot(snapshot); - let mut thickness_block_built = false; - let mut text_block_built = false; - for section in model::ordered_pane_sections(snapshot) { - let widget = match section { - ToolbarSideSection::Colors - if tool_context.needs_color - && !snapshot.side_section_hidden(ToolbarSideSection::Colors) => - { - colors::build(ctx) - } - ToolbarSideSection::Presets - if !snapshot.side_section_hidden(ToolbarSideSection::Presets) => - { - presets::build(ctx) - } - ToolbarSideSection::Thickness - | ToolbarSideSection::EraserMode - | ToolbarSideSection::PolygonSides - if tool_context.needs_thickness && !thickness_block_built => - { - thickness_block_built = true; - thickness::build(ctx) - } - ToolbarSideSection::ArrowLabels - if tool_context.show_arrow_labels - && !snapshot.side_section_hidden(ToolbarSideSection::ArrowLabels) => - { - arrow::build(ctx) - } - ToolbarSideSection::StepMarkers - if tool_context.show_step_counter - && !snapshot.side_section_hidden(ToolbarSideSection::StepMarkers) => - { - step_marker::build(ctx) - } - ToolbarSideSection::MarkerOpacity - if tool_context.show_marker_opacity - && !snapshot.side_section_hidden(ToolbarSideSection::MarkerOpacity) => - { - marker::build(ctx) - } - ToolbarSideSection::TextSize | ToolbarSideSection::Font - if tool_context.show_font_controls && !text_block_built => - { - text_block_built = true; - text_controls::build(ctx) - } - ToolbarSideSection::Actions => actions::build(ctx), - ToolbarSideSection::Boards => boards::build(ctx), - ToolbarSideSection::Pages => pages::build(ctx), - ToolbarSideSection::StepUndo => step_undo::build(ctx), - _ => None, - }; - if let Some(widget) = widget { - content.append(&widget); - } - } -} diff --git a/src/toolbar_gtk/view/sections/pages.rs b/src/toolbar_gtk/view/sections/pages.rs deleted file mode 100644 index 2f0fb197..00000000 --- a/src/toolbar_gtk/view/sections/pages.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! Pages section: Prev / Next / New / Duplicate / Delete command row. - -use gtk4::prelude::*; - -use crate::toolbar_icons; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection, model}; - -use super::super::super::icons::IconPainter; -use super::{SectionCtx, command_row, section_card}; - -pub(in crate::toolbar_gtk) fn build(ctx: &mut SectionCtx) -> Option { - let group = model::toolbar_pages_model(ctx.snapshot)?; - let card = section_card( - ctx, - ToolbarSideSection::Pages, - ToolbarSideSection::Pages.label(), - ); - let row = command_row( - ctx, - &group.buttons, - "Page", - page_icon, - model::toolbar_pages_model, - ); - card.body.append(&row); - Some(card.root.upcast()) -} - -fn page_icon(event: &ToolbarEvent) -> IconPainter { - match event { - ToolbarEvent::PagePrev => toolbar_icons::draw_icon_chevron_left, - ToolbarEvent::PageNext => toolbar_icons::draw_icon_chevron_right, - ToolbarEvent::PageNew => toolbar_icons::draw_icon_plus, - ToolbarEvent::PageDuplicate => toolbar_icons::draw_icon_copy, - _ => toolbar_icons::draw_icon_clear, - } -} diff --git a/src/toolbar_gtk/view/sections/presets.rs b/src/toolbar_gtk/view/sections/presets.rs deleted file mode 100644 index 061bf350..00000000 --- a/src/toolbar_gtk/view/sections/presets.rs +++ /dev/null @@ -1,591 +0,0 @@ -//! Presets section: numbered slots that apply, save, or clear tool presets. -//! -//! Filled slots redraw the built-in slot face (color tint, keycap, tool -//! icon, thickness preview, color swatch) in a `DrawingArea`; the clear -//! badge is revealed through an `EventControllerMotion` because GTK CSS -//! `:hover` cannot toggle a sibling's visibility. Slot filled/empty shape -//! is structural (rebuild); preset content, the active highlight, and the -//! transient feedback flash flow through updaters. - -/// Feedback flash tint, RGBA; `None` while no preset feedback is active. -type FeedbackTint = std::rc::Rc>>; - -use std::cell::{Cell, RefCell}; -use std::rc::Rc; - -use gtk4::prelude::*; - -use crate::config::action_label; -use crate::draw::EraserKind; -use crate::input::EraserMode; -use crate::input::state::PresetFeedbackKind; -use crate::label_format::format_binding_label; -use crate::toolbar_icons; -use crate::ui::theme::toolbar::{ - COLOR_CARD_BACKGROUND, COLOR_TEXT_PRIMARY, COLOR_TEXT_SECONDARY, FONT_FAMILY_DEFAULT, - FONT_SIZE_SECONDARY, -}; -use crate::ui::theme::{Rgb, Rgba, set_color, set_color_alpha, with_alpha}; -use crate::ui::toolbar::bindings::{action_for_clear_preset, action_for_save_preset, tool_label}; -use crate::ui::toolbar::{PresetSlotSnapshot, ToolbarEvent, ToolbarSideSection, ToolbarSnapshot}; -use crate::ui_text::{UiTextStyle, text_layout}; -use crate::util::color_to_name; - -use super::super::super::icons::{IconPainter, tool_icon_painter}; -use super::super::super::widgets::{ - COLOR_SWATCH_HAIRLINE_DARK, rounded_rect_path, send_event, set_active_class, sized_button, -}; -use super::{SectionCard, SectionCtx, section_card}; - -/// Built-in slot geometry (spec units, multiplied by the toolbar scale). -const SLOT_SIZE: f64 = 40.0; -const SLOT_GAP: f64 = 8.0; -/// Size of the hover-revealed clear (✕) badge in a filled slot's corner. -const CLEAR_BADGE_SIZE: f64 = 14.0; - -// Slot chrome tints mirroring the built-in `side_palette::presets::slot`; -// specific values with no theme token, kept to avoid a visible shift. -// TODO(theme-consolidation): every const below is a name-for-name copy of a -// built-in side_palette const — hoist the pairs into `theme::toolbar` so the -// two frontends cannot drift. -/// Empty slot well: near-black tint one step darker than -/// COLOR_PANEL_BACKGROUND (alpha varies with hover). -const EMPTY_SLOT_BG_RGB: Rgb = (0.05, 0.05, 0.07); -/// Dashed outline marking an empty slot. -const COLOR_EMPTY_SLOT_DASH: Rgba = (1.0, 1.0, 1.0, 0.35); -/// Dim save-hint plus glyph in an idle empty slot. -const COLOR_EMPTY_SLOT_PLUS: Rgba = (1.0, 1.0, 1.0, 0.45); -/// Clear badge fill: muted destructive red, quieter than the destructive -/// root. TODO(theme-consolidation): near COLOR_CLOSE_HOVER. -const COLOR_CLEAR_BADGE: Rgba = (0.75, 0.2, 0.2, 0.9); -/// Thickness preview line across a filled slot's bottom edge. -const COLOR_PREVIEW_LINE: Rgba = (1.0, 1.0, 1.0, 0.8); -/// Outline around the mini color swatch in a filled slot. -const COLOR_SWATCH_OUTLINE: Rgba = (1.0, 1.0, 1.0, 0.75); -/// Preset color tint over a filled slot's face (fill and outline alphas). -const PRESET_TINT_FILL_ALPHA: f64 = 0.12; -const PRESET_TINT_BORDER_ALPHA: f64 = 0.35; -/// White root for the keycap's border/text alpha ladder. -const WHITE_RGB: Rgb = (1.0, 1.0, 1.0); -/// Per-kind feedback flash tints (apply/save/clear); mirror the built-in -/// `presets::slot::feedback`. TODO(theme-consolidation): near the overlay -/// toast palette but not equal. -const FEEDBACK_APPLY_RGB: Rgb = (0.35, 0.55, 0.95); -const FEEDBACK_SAVE_RGB: Rgb = (0.25, 0.75, 0.4); -const FEEDBACK_CLEAR_RGB: Rgb = (0.9, 0.3, 0.3); - -pub(in crate::toolbar_gtk) fn build(ctx: &mut SectionCtx) -> Option { - let snapshot = ctx.snapshot; - let slot_count = snapshot.preset_slot_count.min(snapshot.presets.len()); - if !snapshot.show_presets || slot_count == 0 { - return None; - } - - let card = section_card( - ctx, - ToolbarSideSection::Presets, - ToolbarSideSection::Presets.label(), - ); - attach_header_hint(ctx, &card, slot_count); - - let row = gtk4::Box::new(gtk4::Orientation::Horizontal, ctx.px(SLOT_GAP)); - for slot_index in 0..slot_count { - let preset = snapshot - .presets - .get(slot_index) - .and_then(|slot| slot.as_ref()); - let widget = match preset { - Some(preset) => filled_slot(ctx, slot_index, preset), - None => empty_slot(ctx, slot_index), - }; - row.append(&widget); - } - card.body.append(&row); - Some(card.root.upcast()) -} - -/// Right-align the built-in "Keys 1-N apply" hint inside the card header -/// (next to the collapse chevron, visible while collapsed); falls back to -/// the body if the header shape ever changes. -fn attach_header_hint(ctx: &SectionCtx, card: &SectionCard, slot_count: usize) { - let hint = gtk4::Label::new(Some(&apply_hint_text(ctx.snapshot, slot_count))); - hint.add_css_class("hint"); - hint.set_xalign(1.0); - let header_row = card - .root - .first_child() - .and_then(|header| header.downcast::().ok()) - .and_then(|header| header.child()) - .and_then(|row| row.downcast::().ok()); - match header_row { - Some(row) => row.insert_child_after(&hint, row.first_child().as_ref()), - None => card.body.prepend(&hint), - } -} - -/// "Keys 1-N apply" when every slot keeps its default digit binding, -/// otherwise the generic reminder — mirroring the built-in header. -fn apply_hint_text(snapshot: &ToolbarSnapshot, slot_count: usize) -> String { - let uses_digit_bindings = (1..=slot_count) - .all(|slot| snapshot.binding_hints.apply_preset(slot) == Some(slot.to_string().as_str())); - if uses_digit_bindings { - format!("Keys 1-{slot_count} apply") - } else { - "Keys apply presets".to_string() - } -} - -/// Filled slot: applies the preset on click; hover reveals the clear badge. -fn filled_slot( - ctx: &mut SectionCtx, - slot_index: usize, - preset: &PresetSlotSnapshot, -) -> gtk4::Widget { - let slot = slot_index + 1; - let size = ctx.sz(SLOT_SIZE); - let button = sized_button(size, size); - button.set_tooltip_text(Some(&preset_tooltip_text( - preset, - slot, - ctx.snapshot.binding_hints.apply_preset(slot), - ))); - let face = FilledFace::new(preset, slot, size); - button.set_child(Some(&face.area)); - let sender = ctx.feedback.clone(); - button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::ApplyPreset(slot)); - }); - - let overlay = gtk4::Overlay::new(); - overlay.set_child(Some(&button)); - let badge = clear_badge(ctx, slot); - overlay.add_overlay(&badge); - let motion = gtk4::EventControllerMotion::new(); - let enter_badge = badge.clone(); - motion.connect_enter(move |_, _, _| enter_badge.set_visible(true)); - let leave_badge = badge.clone(); - motion.connect_leave(move |_| leave_badge.set_visible(false)); - overlay.add_controller(motion); - - // Content, tooltip, active highlight, and feedback flash track later - // snapshots (filled/empty flips are structural and rebuild instead). - let last = RefCell::new(preset.clone()); - let handle = button.clone(); - ctx.updaters.push(Box::new(move |snapshot| { - set_active_class(&handle, snapshot.active_preset_slot == Some(slot)); - if let Some(Some(current)) = snapshot.presets.get(slot_index) - && *last.borrow() != *current - { - face.apply(current); - handle.set_tooltip_text(Some(&preset_tooltip_text( - current, - slot, - snapshot.binding_hints.apply_preset(slot), - ))); - *last.borrow_mut() = current.clone(); - } - face.set_feedback(feedback_overlay(snapshot, slot_index)); - })); - - overlay.upcast() -} - -/// Empty slot: dashed outline with a "+", saves the current setup on click. -fn empty_slot(ctx: &mut SectionCtx, slot_index: usize) -> gtk4::Widget { - let slot = slot_index + 1; - let size = ctx.sz(SLOT_SIZE); - let button = sized_button(size, size); - button.set_tooltip_text(Some(&format_binding_label( - action_for_save_preset(slot) - .map(action_label) - .unwrap_or("Save Preset"), - ctx.snapshot.binding_hints.save_preset(slot), - ))); - - let hovered = Rc::new(Cell::new(false)); - let feedback: FeedbackTint = Rc::new(Cell::new(None)); - let area = slot_area(size); - let label = slot.to_string(); - let draw_hovered = hovered.clone(); - let draw_feedback_cell = feedback.clone(); - area.set_draw_func(move |_, ctx, width, height| { - let s = width.min(height) as f64; - let k = s / SLOT_SIZE; - let hover = draw_hovered.get(); - // Dark backing + dashed outline the built-in empty slot draws. - set_color_alpha(ctx, EMPTY_SLOT_BG_RGB, if hover { 0.45 } else { 0.35 }); - rounded_rect_path(ctx, k, k, s - 2.0 * k, s - 2.0 * k, 6.0 * k); - let _ = ctx.fill(); - set_color(ctx, COLOR_EMPTY_SLOT_DASH); - ctx.set_line_width(1.0); - ctx.set_dash(&[3.0 * k, 2.0 * k], 0.0); - rounded_rect_path(ctx, k, k, s - 2.0 * k, s - 2.0 * k, 6.0 * k); - let _ = ctx.stroke(); - ctx.set_dash(&[], 0.0); - let plus = (s * 0.45).round().min(14.0 * k); - set_color( - ctx, - if hover { - COLOR_TEXT_SECONDARY - } else { - COLOR_EMPTY_SLOT_PLUS - }, - ); - toolbar_icons::draw_icon_plus(ctx, (s - plus) / 2.0, (s - plus) / 2.0, plus); - draw_keycap(ctx, s, &label, false); - draw_feedback(ctx, s, draw_feedback_cell.get()); - }); - button.set_child(Some(&area)); - - let motion = gtk4::EventControllerMotion::new(); - let enter_hovered = hovered.clone(); - let enter_area = area.clone(); - motion.connect_enter(move |_, _, _| { - enter_hovered.set(true); - enter_area.queue_draw(); - }); - let leave_area = area.clone(); - motion.connect_leave(move |_| { - hovered.set(false); - leave_area.queue_draw(); - }); - button.add_controller(motion); - - let sender = ctx.feedback.clone(); - button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::SavePreset(slot)); - }); - - ctx.updaters.push(Box::new(move |snapshot| { - let overlay = feedback_overlay(snapshot, slot_index); - if feedback.get() != overlay { - feedback.set(overlay); - area.queue_draw(); - } - })); - - button.upcast() -} - -/// The destructive clear badge: a red circle with a white ✕, exactly like -/// the built-in hover badge, delivered as a corner overlay button. -fn clear_badge(ctx: &SectionCtx, slot: usize) -> gtk4::Button { - let size = ctx.sz(CLEAR_BADGE_SIZE); - let badge = sized_button(size, size); - badge.add_css_class("swatch"); - badge.set_halign(gtk4::Align::End); - badge.set_valign(gtk4::Align::Start); - badge.set_margin_top(ctx.px(2.0)); - badge.set_margin_end(ctx.px(2.0)); - badge.set_visible(false); - badge.set_tooltip_text(Some(&format_binding_label( - action_for_clear_preset(slot) - .map(action_label) - .unwrap_or("Clear Preset"), - ctx.snapshot.binding_hints.clear_preset(slot), - ))); - let area = slot_area(size); - area.set_draw_func(|_, ctx, width, height| { - let size = width.min(height) as f64; - set_color(ctx, COLOR_CLEAR_BADGE); - ctx.arc( - size / 2.0, - size / 2.0, - size / 2.0, - 0.0, - std::f64::consts::PI * 2.0, - ); - let _ = ctx.fill(); - set_color(ctx, COLOR_TEXT_PRIMARY); - let inset = size * 0.3; - ctx.set_line_width(1.6 * size / CLEAR_BADGE_SIZE); - ctx.set_line_cap(cairo::LineCap::Round); - ctx.move_to(inset, inset); - ctx.line_to(size - inset, size - inset); - let _ = ctx.stroke(); - ctx.move_to(size - inset, inset); - ctx.line_to(inset, size - inset); - let _ = ctx.stroke(); - }); - badge.set_child(Some(&area)); - let sender = ctx.feedback.clone(); - badge.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::ClearPreset(slot)); - }); - badge -} - -/// A filled slot's drawn face; content-only preset edits update the cells -/// and redraw instead of rebuilding the widget tree. -struct FilledFace { - area: gtk4::DrawingArea, - color: Rc>, - thickness: Rc>, - painter: Rc>, - feedback: FeedbackTint, -} - -impl FilledFace { - fn new(preset: &PresetSlotSnapshot, slot: usize, size: f64) -> Self { - let area = slot_area(size); - let color = Rc::new(Cell::new(( - preset.color.r, - preset.color.g, - preset.color.b, - preset.color.a, - ))); - let thickness = Rc::new(Cell::new(preset.size)); - let painter = Rc::new(Cell::new(tool_icon_painter(preset.tool))); - let feedback: FeedbackTint = Rc::new(Cell::new(None)); - let label = slot.to_string(); - let draw_color = color.clone(); - let draw_thickness = thickness.clone(); - let draw_painter = painter.clone(); - let draw_feedback_cell = feedback.clone(); - area.set_draw_func(move |_, ctx, width, height| { - let s = width.min(height) as f64; - let k = s / SLOT_SIZE; - let (r, g, b, a) = draw_color.get(); - // Preset color tint over the button base. The tint keeps its own - // fixed alphas rather than folding in the color's: it identifies - // the slot, and a barely-there color would leave it unreadable. - // The corner swatch below is what shows the color faithfully. - set_color_alpha(ctx, (r, g, b), PRESET_TINT_FILL_ALPHA); - rounded_rect_path(ctx, k, k, s - 2.0 * k, s - 2.0 * k, 6.0 * k); - let _ = ctx.fill(); - set_color_alpha(ctx, (r, g, b), PRESET_TINT_BORDER_ALPHA); - ctx.set_line_width(1.0); - rounded_rect_path(ctx, k, k, s - 2.0 * k, s - 2.0 * k, 6.0 * k); - let _ = ctx.stroke(); - // Centered tool icon. - let icon = (s * 0.45).round(); - set_color(ctx, COLOR_TEXT_SECONDARY); - (draw_painter.get())(ctx, (s - icon) / 2.0, (s - icon) / 2.0, icon); - // Thickness preview line along the bottom. - let preview = (draw_thickness.get() / 50.0 * 6.0).clamp(1.0, 6.0) * k; - set_color(ctx, COLOR_PREVIEW_LINE); - ctx.set_line_width(preview); - ctx.move_to(4.0 * k, s - 6.0 * k); - ctx.line_to(s - 4.0 * k, s - 6.0 * k); - let _ = ctx.stroke(); - // Color swatch in the bottom-right corner, over the checkerboard - // when translucent so it previews the color the preset applies. - let sw = (s * 0.35).round(); - let (sx, sy) = (s - sw - 4.0 * k, s - sw - 4.0 * k); - let swatch_path = - |ctx: &cairo::Context| rounded_rect_path(ctx, sx, sy, sw, sw, 4.0 * k); - crate::ui::checkerboard_behind(ctx, a, swatch_path); - ctx.set_source_rgba(r, g, b, a); - swatch_path(ctx); - let _ = ctx.fill(); - let luminance = 0.299 * r + 0.587 * g + 0.114 * b; - if luminance < 0.3 { - set_color(ctx, COLOR_SWATCH_HAIRLINE_DARK); - ctx.set_line_width(1.5); - rounded_rect_path(ctx, sx, sy, sw, sw, 4.0 * k); - let _ = ctx.stroke(); - } - set_color(ctx, COLOR_SWATCH_OUTLINE); - ctx.set_line_width(1.0); - rounded_rect_path(ctx, sx, sy, sw, sw, 4.0 * k); - let _ = ctx.stroke(); - draw_keycap(ctx, s, &label, true); - draw_feedback(ctx, s, draw_feedback_cell.get()); - }); - Self { - area, - color, - thickness, - painter, - feedback, - } - } - - fn apply(&self, preset: &PresetSlotSnapshot) { - self.color.set(( - preset.color.r, - preset.color.g, - preset.color.b, - preset.color.a, - )); - self.thickness.set(preset.size); - self.painter.set(tool_icon_painter(preset.tool)); - self.area.queue_draw(); - } - - fn set_feedback(&self, overlay: Option<(f64, f64, f64, f64)>) { - if self.feedback.get() != overlay { - self.feedback.set(overlay); - self.area.queue_draw(); - } - } -} - -/// Square, click-transparent canvas for a slot face or badge glyph. -fn slot_area(size: f64) -> gtk4::DrawingArea { - let area = gtk4::DrawingArea::new(); - let px = size.round() as i32; - area.set_content_width(px); - area.set_content_height(px); - area.set_can_target(false); - area -} - -/// The numbered keycap in the slot's top-left corner (bright when the -/// slot holds a preset, dim when empty), matching the built-in alphas. -fn draw_keycap(ctx: &cairo::Context, s: f64, label: &str, active: bool) { - let k = s / SLOT_SIZE; - let number_box = (s * 0.4).round(); - let pad = (s * 0.1).round().max(3.0); - let radius = (number_box * 0.25).max(3.0); - let (bg_alpha, border_alpha, text_alpha) = if active { - (0.65, 0.50, 0.95) - } else { - (0.30, 0.25, 0.55) - }; - set_color(ctx, with_alpha(COLOR_CARD_BACKGROUND, bg_alpha)); - rounded_rect_path(ctx, pad, pad, number_box, number_box, radius); - let _ = ctx.fill(); - set_color_alpha(ctx, WHITE_RGB, border_alpha); - ctx.set_line_width(1.0); - rounded_rect_path(ctx, pad, pad, number_box, number_box, radius); - let _ = ctx.stroke(); - let style = UiTextStyle { - family: FONT_FAMILY_DEFAULT, - slant: cairo::FontSlant::Normal, - weight: cairo::FontWeight::Bold, - size: FONT_SIZE_SECONDARY * k, - }; - let layout = text_layout(ctx, style, label, None); - let ext = layout.ink_extents(); - let tx = pad + (number_box - ext.width()) / 2.0 - ext.x_bearing(); - let ty = pad + (number_box - ext.height()) / 2.0 - ext.y_bearing(); - set_color_alpha(ctx, WHITE_RGB, text_alpha); - layout.show_at_baseline(ctx, tx, ty); -} - -/// The transient apply/save/clear flash the built-in palette fades out. -fn draw_feedback(ctx: &cairo::Context, s: f64, overlay: Option<(f64, f64, f64, f64)>) { - if let Some(tint) = overlay { - let k = s / SLOT_SIZE; - set_color(ctx, tint); - rounded_rect_path(ctx, k, k, s - 2.0 * k, s - 2.0 * k, 6.0 * k); - let _ = ctx.fill(); - } -} - -fn feedback_overlay(snapshot: &ToolbarSnapshot, slot_index: usize) -> Option<(f64, f64, f64, f64)> { - let feedback = snapshot.preset_feedback.get(slot_index)?.as_ref()?; - let fade = (1.0 - feedback.progress as f64).clamp(0.0, 1.0); - if fade <= 0.0 { - return None; - } - let (r, g, b) = match feedback.kind { - PresetFeedbackKind::Apply => FEEDBACK_APPLY_RGB, - PresetFeedbackKind::Save => FEEDBACK_SAVE_RGB, - PresetFeedbackKind::Clear => FEEDBACK_CLEAR_RGB, - }; - Some((r, g, b, 0.35 * fade)) -} - -// ===== Tooltip text (replicates the built-in presets/format.rs) ========= - -fn preset_tooltip_text(preset: &PresetSlotSnapshot, slot: usize, binding: Option<&str>) -> String { - let preset_name = preset - .name - .as_deref() - .map(str::trim) - .filter(|name| !name.is_empty()); - let mut extra_details = Vec::new(); - if let Some(fill) = preset.fill_enabled { - extra_details.push(format!("fill:{}", on_off(fill))); - } - if let Some(opacity) = preset.marker_opacity { - let percent = (opacity * 100.0).round() as i32; - extra_details.push(format!("opacity:{}%", percent)); - } - if let Some(kind) = preset.eraser_kind { - extra_details.push(format!("eraser:{}", eraser_kind_label(kind))); - } - if let Some(mode) = preset.eraser_mode { - extra_details.push(format!("mode:{}", eraser_mode_label(mode))); - } - if let Some(font_size) = preset.font_size { - extra_details.push(format!("font:{}", px_label(font_size))); - } - if let Some(text_bg) = preset.text_background_enabled { - extra_details.push(format!("text bg:{}", on_off(text_bg))); - } - let mut arrow_bits = Vec::new(); - if let Some(length) = preset.arrow_length { - arrow_bits.push(format!("len {}", px_label(length))); - } - if let Some(angle) = preset.arrow_angle { - arrow_bits.push(format!("ang {}", angle_label(angle))); - } - if let Some(head_at_end) = preset.arrow_head_at_end { - let head = if head_at_end { "end" } else { "start" }; - arrow_bits.push(format!("head {}", head)); - } - if !arrow_bits.is_empty() { - extra_details.push(format!("arrow:{}", arrow_bits.join(", "))); - } - if let Some(show_status_bar) = preset.show_status_bar { - extra_details.push(format!("status:{}", on_off(show_status_bar))); - } - - let base_summary = format!( - "{}, {}, {}", - tool_label(preset.tool), - color_to_name(&preset.color), - px_label(preset.size) - ); - let summary = if extra_details.is_empty() { - base_summary - } else { - format!("{}; {}", base_summary, extra_details.join("; ")) - }; - let label = if let Some(name) = preset_name { - format!("Apply preset {}: {} ({})", slot, name, summary) - } else { - format!("Apply preset {} ({})", slot, summary) - }; - match binding { - Some(binding) => format!("{label} (key: {binding})"), - None => label, - } -} - -fn px_label(value: f64) -> String { - if (value - value.round()).abs() < 0.05 { - format!("{:.0}px", value) - } else { - format!("{:.1}px", value) - } -} - -fn angle_label(value: f64) -> String { - if (value - value.round()).abs() < 0.05 { - format!("{:.0}deg", value) - } else { - format!("{:.1}deg", value) - } -} - -fn on_off(value: bool) -> &'static str { - if value { "on" } else { "off" } -} - -fn eraser_kind_label(kind: EraserKind) -> &'static str { - match kind { - EraserKind::Circle => "circle", - EraserKind::Rect => "rect", - } -} - -fn eraser_mode_label(mode: EraserMode) -> &'static str { - match mode { - EraserMode::Brush => "brush", - EraserMode::Stroke => "stroke", - } -} diff --git a/src/toolbar_gtk/view/sections/session_pane.rs b/src/toolbar_gtk/view/sections/session_pane.rs index 9824ec6a..6120b509 100644 --- a/src/toolbar_gtk/view/sections/session_pane.rs +++ b/src/toolbar_gtk/view/sections/session_pane.rs @@ -6,37 +6,18 @@ use gtk4::prelude::*; use crate::label_format::format_binding_label; use crate::toolbar_icons; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection, model}; +use crate::ui::toolbar::session_format::strip_session_extension; +use crate::ui::toolbar::{ToolbarEvent, model}; use super::super::super::icons::{IconPainter, IconWidget}; use super::super::super::widgets::send_event; -use super::{SectionCtx, section_card}; +use super::SectionCtx; -pub(in crate::toolbar_gtk) fn build(ctx: &mut SectionCtx) -> Option { - let session = model::ToolbarSessionModel::from_snapshot(ctx.snapshot)?; - let card = section_card( - ctx, - ToolbarSideSection::Session, - ToolbarSideSection::Session.label(), - ); - card.body.append(&content(ctx, &session, true)); - Some(card.root.upcast()) -} - -/// The pane's content for the top strip's Session popover: identical -/// controls without the collapsible-card chrome. Live updates come from -/// content-key rebuilds in the popover host, so no updaters register. +/// The pane's content for the top strip's Session popover. Live updates come +/// from content-key rebuilds in the popover host, so no updaters register. pub(in crate::toolbar_gtk) fn build_popover_content( - ctx: &mut SectionCtx, - session: &model::ToolbarSessionModel, -) -> gtk4::Box { - content(ctx, session, false) -} - -fn content( - ctx: &mut SectionCtx, + ctx: &SectionCtx, session: &model::ToolbarSessionModel, - register_updaters: bool, ) -> gtk4::Box { let column = gtk4::Box::new(gtk4::Orientation::Vertical, ctx.px(6.0)); let name_label = gtk4::Label::new(Some(&session.active_name)); @@ -52,7 +33,6 @@ fn content( column.append(&path_label); // A pending Save-As overwrite confirmation replaces the button grid. - let mut handles: Vec = Vec::new(); if let Some(confirmation) = session.overwrite_confirmation.as_ref() { column.append(&overwrite_confirmation_rows(ctx, confirmation)); } else { @@ -70,7 +50,6 @@ fn content( 1, 1, ); - handles.push(button); } column.append(&grid); } @@ -78,19 +57,6 @@ fn content( for recent in &session.recents { column.append(&recent_row(ctx, recent)); } - - if register_updaters { - ctx.updaters.push(Box::new(move |snapshot| { - let Some(session) = model::ToolbarSessionModel::from_snapshot(snapshot) else { - return; - }; - name_label.set_text(&session.active_name); - path_label.set_text(&session.active_path_label); - for (handle, button_model) in handles.iter().zip(session.buttons.iter()) { - handle.set_sensitive(button_model.enabled); - } - })); - } column } @@ -192,12 +158,3 @@ fn recent_row(ctx: &SectionCtx, recent: &model::ToolbarSessionRecent) -> gtk4::B }); button } - -/// Drop the constant session-file extension in list rows; it costs the -/// characters that distinguish one session from another. -fn strip_session_extension(value: &str) -> &str { - value - .strip_suffix(".wayscriber-session") - .or_else(|| value.strip_suffix(".wayscriber")) - .unwrap_or(value) -} diff --git a/src/toolbar_gtk/view/sections/settings_pane.rs b/src/toolbar_gtk/view/sections/settings_pane.rs index 855a9da4..e8c7a42d 100644 --- a/src/toolbar_gtk/view/sections/settings_pane.rs +++ b/src/toolbar_gtk/view/sections/settings_pane.rs @@ -5,60 +5,24 @@ use gtk4::prelude::*; use crate::toolbar_icons; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection, ToolbarSnapshot, model}; +use crate::ui::toolbar::{ToolbarEvent, model}; use super::super::super::icons::{IconPainter, IconWidget}; use super::super::super::widgets::{send_event, set_active_class, text_button}; -use super::{SectionCtx, section_card}; +use super::SectionCtx; -type SettingsModelSource = fn(&ToolbarSnapshot) -> Option; - -pub(in crate::toolbar_gtk) fn build(ctx: &mut SectionCtx) -> Option { - let settings_model = model::ToolbarSettingsModel::from_snapshot(ctx.snapshot)?; - let customizing = ctx.snapshot.customize_items_open; - let title = if customizing { - "Customize toolbar" - } else { - ToolbarSideSection::Settings.label() - }; - let card = section_card(ctx, ToolbarSideSection::Settings, title); - if customizing { - // The customization sub-panel is a dedicated view: it stays open - // even while the Settings section is flagged collapsed. - card.body.set_visible(true); - } - card.body.append(&content( - ctx, - &settings_model, - model::ToolbarSettingsModel::from_snapshot, - )); - Some(card.root.upcast()) -} - -/// The pane's content for the top strip's Settings popover: identical -/// controls without the collapsible-card chrome. The popover host retains the -/// registered updaters so ordinary checkbox echoes update in place. +/// The pane's content for the top strip's Settings popover. The popover host +/// retains the registered updaters so ordinary checkbox echoes update in +/// place. pub(in crate::toolbar_gtk) fn build_popover_content( ctx: &mut SectionCtx, settings_model: &model::ToolbarSettingsModel, -) -> gtk4::Box { - content( - ctx, - settings_model, - model::ToolbarSettingsModel::for_popover, - ) -} - -fn content( - ctx: &mut SectionCtx, - settings_model: &model::ToolbarSettingsModel, - model_source: SettingsModelSource, ) -> gtk4::Box { let column = gtk4::Box::new(gtk4::Orientation::Vertical, ctx.px(6.0)); if !ctx.snapshot.customize_items_open { column.append(&layout_mode_segments(ctx)); } - if let Some(grid) = toggle_grid(ctx, settings_model, model_source) { + if let Some(grid) = toggle_grid(ctx, settings_model) { column.append(&grid); } for notice in settings_model.notices() { @@ -107,7 +71,7 @@ fn layout_mode_segments(ctx: &mut SectionCtx) -> gtk4::Box { } set_active_class(&button, active == Some(segment.id)); let sender = ctx.feedback.clone(); - let event = segment.activation.compatibility_event(); + let event = segment.activation.clone(); button.connect_clicked(move |_| { send_event(&sender, event.clone()); }); @@ -169,7 +133,6 @@ fn settings_toggle_event(template: &ToolbarEvent, checked: bool) -> ToolbarEvent fn toggle_grid( ctx: &mut SectionCtx, settings_model: &model::ToolbarSettingsModel, - model_source: SettingsModelSource, ) -> Option { let rows = settings_model.toggle_rows(); if rows.is_empty() { @@ -190,7 +153,7 @@ fn toggle_grid( if let Some(tooltip) = toggle.tooltip.as_string() { check.set_tooltip_text(Some(&tooltip)); } - let event = toggle.activation.compatibility_event(); + let event = toggle.activation.clone(); let sender = ctx.feedback.clone(); let handler = check.connect_toggled(move |check| { send_event(&sender, settings_toggle_event(&event, check.is_active())); @@ -205,7 +168,7 @@ fn toggle_grid( } } ctx.updaters.push(Box::new(move |snapshot| { - let Some(fresh) = model_source(snapshot) else { + let Some(fresh) = model::ToolbarSettingsModel::for_popover(snapshot) else { return; }; for handle in &handles { @@ -356,7 +319,7 @@ fn item_override_rows( ); button.set_sensitive(enabled); let sender = ctx.feedback.clone(); - let event = activation.compatibility_event(); + let event = activation.clone(); button.connect_clicked(move |_| { send_event(&sender, event.clone()); }); diff --git a/src/toolbar_gtk/view/sections/step_marker.rs b/src/toolbar_gtk/view/sections/step_marker.rs deleted file mode 100644 index 86050f50..00000000 --- a/src/toolbar_gtk/view/sections/step_marker.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Step markers section: a "Next: N" header hint plus a Reset button. - -use gtk4::prelude::*; - -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection}; - -use super::super::super::widgets::send_event; -use super::{SectionCard, SectionCtx, section_card}; - -pub(in crate::toolbar_gtk) fn build(ctx: &mut SectionCtx) -> Option { - let card = section_card( - ctx, - ToolbarSideSection::StepMarkers, - ToolbarSideSection::StepMarkers.label(), - ); - let hint = attach_header_hint(&card, &format!("Next: {}", ctx.snapshot.step_marker_next)); - - let reset = gtk4::Button::with_label("Reset"); - reset.set_size_request(-1, ctx.px(24.0)); - reset.set_tooltip_text(Some("Reset numbering to 1.")); - let sender = ctx.feedback.clone(); - reset.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::ResetStepMarkerCounter); - }); - card.body.append(&reset); - - ctx.updaters.push(Box::new(move |snapshot| { - if let Some(hint) = &hint { - hint.set_text(&format!("Next: {}", snapshot.step_marker_next)); - } - })); - Some(card.root.upcast()) -} - -/// Right-aligned counter hint on the card's header row, kept visible even -/// while the section is collapsed like the built-in palette draws it. -fn attach_header_hint(card: &SectionCard, text: &str) -> Option { - let header = card.root.first_child().and_downcast::()?; - let row = header.child().and_downcast::()?; - let title = row.first_child()?; - let hint = gtk4::Label::new(Some(text)); - hint.add_css_class("hint"); - row.insert_child_after(&hint, Some(&title)); - Some(hint) -} diff --git a/src/toolbar_gtk/view/sections/step_undo.rs b/src/toolbar_gtk/view/sections/step_undo.rs index ad36da9a..79aeea30 100644 --- a/src/toolbar_gtk/view/sections/step_undo.rs +++ b/src/toolbar_gtk/view/sections/step_undo.rs @@ -8,30 +8,16 @@ use std::rc::Rc; use gtk4::prelude::*; use crate::toolbar_icons; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection, ToolbarSnapshot, model}; +use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot, model}; use super::super::super::icons::{IconPainter, IconWidget}; use super::super::super::widgets::{SliderRow, icon_button, send_event, sized_button}; -use super::{SectionCtx, section_card}; +use super::SectionCtx; -pub(in crate::toolbar_gtk) fn build(ctx: &mut SectionCtx) -> Option { - let snapshot = ctx.snapshot; - if snapshot.side_section_hidden(ToolbarSideSection::StepUndo) || !snapshot.show_step_section { - return None; - } - let card = section_card( - ctx, - ToolbarSideSection::StepUndo, - ToolbarSideSection::StepUndo.label(), - ); - populate(ctx, &card.body); - Some(card.root.upcast()) -} - -/// The section's controls for the top strip's Canvas popover: the same -/// toggles, step rows, and delay sliders without the collapsible-card -/// chrome. Callers gate on `show_step_section`; liveness comes from the -/// popover host's content-key rebuild, so the updaters go to a scratch list. +/// The section's controls for the top strip's Canvas popover: the toggles, +/// step rows, and delay sliders. Callers gate on `show_step_section`; +/// liveness comes from the popover host's content-key rebuild, so the +/// updaters go to a scratch list. pub(in crate::toolbar_gtk) fn build_popover_content(ctx: &mut SectionCtx) -> gtk4::Box { let body = gtk4::Box::new(gtk4::Orientation::Vertical, ctx.px(6.0)); populate(ctx, &body); diff --git a/src/toolbar_gtk/view/sections/text_controls.rs b/src/toolbar_gtk/view/sections/text_controls.rs deleted file mode 100644 index 617b06db..00000000 --- a/src/toolbar_gtk/view/sections/text_controls.rs +++ /dev/null @@ -1,132 +0,0 @@ -//! Text block: font-size slider and the Sans / Monospace font row. - -use gtk4::prelude::*; - -use crate::draw::FontDescriptor; -use crate::toolbar_icons; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection, model}; - -use super::super::super::widgets::{SliderRow, icon_button, send_event, set_active_class}; -use super::{SectionCtx, section_card}; - -/// Spec-unit sizes from the built-in side palette (`SIDE_NUDGE_SIZE`, -/// `SIDE_NUDGE_ICON_SIZE`, `SIDE_FONT_BUTTON_HEIGHT`, `SIDE_FONT_BUTTON_GAP`). -const NUDGE_BUTTON_SIZE: f64 = 24.0; -const NUDGE_ICON_SIZE: f64 = 14.0; -const FONT_BUTTON_HEIGHT: f64 = 24.0; -const FONT_BUTTON_GAP: f64 = 8.0; - -pub(in crate::toolbar_gtk) fn build(ctx: &mut SectionCtx) -> Option { - let snapshot = ctx.snapshot; - let block = gtk4::Box::new(gtk4::Orientation::Vertical, ctx.px(12.0)); - - if !snapshot.side_section_hidden(ToolbarSideSection::TextSize) { - block.append(&text_size_card(ctx)); - } - if !snapshot.side_section_hidden(ToolbarSideSection::Font) { - block.append(&font_card(ctx)); - } - block.first_child()?; - Some(block.upcast()) -} - -fn text_size_card(ctx: &mut SectionCtx) -> gtk4::Widget { - let snapshot = ctx.snapshot; - let card = section_card(ctx, ToolbarSideSection::TextSize, "Text size"); - if snapshot.side_section_collapsed(ToolbarSideSection::TextSize) { - return card.root.upcast(); - } - - let spec = model::ToolbarSliderSpec::FONT_SIZE; - let step = spec.step.unwrap_or(2.0); - let (min, max) = (spec.min, spec.max); - let row = gtk4::Box::new(gtk4::Orientation::Horizontal, ctx.px(6.0)); - let btn = ctx.sz(NUDGE_BUTTON_SIZE); - let icon = ctx.sz(NUDGE_ICON_SIZE); - - let minus = icon_button( - toolbar_icons::draw_icon_minus, - (btn, btn), - icon, - "Decrease font size", - ); - let sender = ctx.feedback.clone(); - minus.button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::NudgeFontSize(-step)); - }); - row.append(&minus.button); - - let sender = ctx.feedback.clone(); - let slider = SliderRow::new( - ctx.scale, - (min, max), - snapshot.font_size, - format_pt, - move |value| { - send_event(&sender, ToolbarEvent::SetFontSize(value)); - }, - ); - slider.root.set_hexpand(true); - row.append(&slider.root); - - let plus = icon_button( - toolbar_icons::draw_icon_plus, - (btn, btn), - icon, - "Increase font size", - ); - let sender = ctx.feedback.clone(); - plus.button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::NudgeFontSize(step)); - }); - row.append(&plus.button); - - card.body.append(&row); - ctx.updaters.push(Box::new(move |snapshot| { - slider.set_value(snapshot.font_size); - })); - card.root.upcast() -} - -fn format_pt(value: f64) -> String { - format!("{value:.0}pt") -} - -fn font_card(ctx: &mut SectionCtx) -> gtk4::Widget { - let snapshot = ctx.snapshot; - let card = section_card(ctx, ToolbarSideSection::Font, "Font"); - if snapshot.side_section_collapsed(ToolbarSideSection::Font) { - return card.root.upcast(); - } - - let row = gtk4::Box::new(gtk4::Orientation::Horizontal, ctx.px(FONT_BUTTON_GAP)); - row.set_homogeneous(true); - let fonts = [ - FontDescriptor::new("Sans".to_string(), "bold".to_string(), "normal".to_string()), - FontDescriptor::new( - "Monospace".to_string(), - "normal".to_string(), - "normal".to_string(), - ), - ]; - let mut handles: Vec<(gtk4::Button, String)> = Vec::new(); - for font in fonts { - let button = gtk4::Button::with_label(&font.family); - button.set_size_request(-1, ctx.px(FONT_BUTTON_HEIGHT)); - set_active_class(&button, font.family == snapshot.font.family); - let sender = ctx.feedback.clone(); - let family = font.family.clone(); - button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::SetFont(font.clone())); - }); - row.append(&button); - handles.push((button, family)); - } - card.body.append(&row); - ctx.updaters.push(Box::new(move |snapshot| { - for (button, family) in &handles { - set_active_class(button, *family == snapshot.font.family); - } - })); - card.root.upcast() -} diff --git a/src/toolbar_gtk/view/sections/thickness.rs b/src/toolbar_gtk/view/sections/thickness.rs deleted file mode 100644 index 729dd227..00000000 --- a/src/toolbar_gtk/view/sections/thickness.rs +++ /dev/null @@ -1,202 +0,0 @@ -//! Thickness block: the thickness slider, eraser mode toggle, and polygon -//! sides cards the built-in palette draws together. - -use std::cell::Cell; -use std::rc::Rc; - -use gtk4::prelude::*; - -use crate::config::Action; -use crate::input::EraserMode; -use crate::label_format::format_binding_label; -use crate::toolbar_icons; -use crate::ui::toolbar::snapshot::ToolContext; -use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection, model}; - -use super::super::super::widgets::{SliderRow, icon_button, send_event}; -use super::{SectionCtx, scoped_title, section_card}; - -/// Spec-unit sizes from the built-in side palette (`SIDE_NUDGE_SIZE`, -/// `SIDE_NUDGE_ICON_SIZE`). -const NUDGE_BUTTON_SIZE: f64 = 24.0; -const NUDGE_ICON_SIZE: f64 = 14.0; - -pub(in crate::toolbar_gtk) fn build(ctx: &mut SectionCtx) -> Option { - let snapshot = ctx.snapshot; - let tool_context = ToolContext::from_snapshot(snapshot); - let block = gtk4::Box::new(gtk4::Orientation::Vertical, ctx.px(12.0)); - - if !snapshot.side_section_hidden(ToolbarSideSection::Thickness) { - block.append(&thickness_card(ctx, &tool_context)); - } - if tool_context.show_eraser_mode - && !snapshot.side_section_hidden(ToolbarSideSection::EraserMode) - { - block.append(&eraser_mode_card(ctx)); - } - if tool_context.show_polygon_sides_control - && !snapshot.side_section_hidden(ToolbarSideSection::PolygonSides) - { - block.append(&polygon_sides_card(ctx)); - } - block.first_child()?; - Some(block.upcast()) -} - -fn thickness_card(ctx: &mut SectionCtx, tool_context: &ToolContext) -> gtk4::Widget { - let snapshot = ctx.snapshot; - // Generic titles gain the tool scope; specific ones ("Eraser size") - // already name their target. - let title = if tool_context.thickness_label == "Thickness" { - scoped_title(tool_context.thickness_label, snapshot) - } else { - tool_context.thickness_label.to_string() - }; - let card = section_card(ctx, ToolbarSideSection::Thickness, &title); - if snapshot.side_section_collapsed(ToolbarSideSection::Thickness) { - return card.root.upcast(); - } - - let spec = model::ToolbarSliderSpec::THICKNESS; - let nudge_step = spec.step.unwrap_or(1.0); - let row = gtk4::Box::new(gtk4::Orientation::Horizontal, ctx.px(6.0)); - let btn = ctx.sz(NUDGE_BUTTON_SIZE); - let icon = ctx.sz(NUDGE_ICON_SIZE); - - let minus = icon_button( - toolbar_icons::draw_icon_minus, - (btn, btn), - icon, - "Decrease thickness", - ); - let sender = ctx.feedback.clone(); - minus.button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::NudgeThickness(-nudge_step)); - }); - row.append(&minus.button); - - // `snapshot.thickness` already carries the active target's value - // (eraser size when `thickness_targets_eraser`), like the built-in. - let sender = ctx.feedback.clone(); - let slider = SliderRow::new( - ctx.scale, - (spec.min, spec.max), - snapshot.thickness, - format_px, - move |value| { - send_event(&sender, ToolbarEvent::SetThickness(value)); - }, - ); - slider.root.set_hexpand(true); - row.append(&slider.root); - - let plus = icon_button( - toolbar_icons::draw_icon_plus, - (btn, btn), - icon, - "Increase thickness", - ); - let sender = ctx.feedback.clone(); - plus.button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::NudgeThickness(nudge_step)); - }); - row.append(&plus.button); - - card.body.append(&row); - ctx.updaters.push(Box::new(move |snapshot| { - slider.set_value(snapshot.thickness); - })); - card.root.upcast() -} - -fn format_px(value: f64) -> String { - format!("{value:.0}px") -} - -fn eraser_mode_card(ctx: &mut SectionCtx) -> gtk4::Widget { - let snapshot = ctx.snapshot; - let card = section_card(ctx, ToolbarSideSection::EraserMode, "Eraser mode"); - if snapshot.side_section_collapsed(ToolbarSideSection::EraserMode) { - return card.root.upcast(); - } - - let toggle = gtk4::CheckButton::with_label("Erase by stroke"); - toggle.set_tooltip_text(Some(&format_binding_label( - "Erase by stroke", - snapshot - .binding_hints - .binding_for_action(Action::ToggleEraserMode), - ))); - toggle.set_active(snapshot.eraser_mode == EraserMode::Stroke); - let sender = ctx.feedback.clone(); - let syncing = Rc::new(Cell::new(false)); - let toggle_sync = syncing.clone(); - toggle.connect_toggled(move |check| { - if !toggle_sync.get() { - send_event( - &sender, - ToolbarEvent::SetEraserMode(if check.is_active() { - EraserMode::Stroke - } else { - EraserMode::Brush - }), - ); - } - }); - card.body.append(&toggle); - ctx.updaters.push(Box::new(move |snapshot| { - let stroke_active = snapshot.eraser_mode == EraserMode::Stroke; - if toggle.is_active() != stroke_active { - syncing.set(true); - toggle.set_active(stroke_active); - syncing.set(false); - } - })); - card.root.upcast() -} - -fn polygon_sides_card(ctx: &mut SectionCtx) -> gtk4::Widget { - let snapshot = ctx.snapshot; - let card = section_card(ctx, ToolbarSideSection::PolygonSides, "Sides"); - if snapshot.side_section_collapsed(ToolbarSideSection::PolygonSides) { - return card.root.upcast(); - } - - let row = gtk4::Box::new(gtk4::Orientation::Horizontal, ctx.px(6.0)); - let btn = ctx.sz(NUDGE_BUTTON_SIZE); - let icon = ctx.sz(NUDGE_ICON_SIZE); - - let minus = icon_button( - toolbar_icons::draw_icon_minus, - (btn, btn), - icon, - "Decrease polygon sides", - ); - let sender = ctx.feedback.clone(); - minus.button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::NudgePolygonSides(-1)); - }); - row.append(&minus.button); - - let label = gtk4::Label::new(Some(&format!("{} sides", snapshot.polygon_sides))); - label.set_hexpand(true); - row.append(&label); - - let plus = icon_button( - toolbar_icons::draw_icon_plus, - (btn, btn), - icon, - "Increase polygon sides", - ); - let sender = ctx.feedback.clone(); - plus.button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::NudgePolygonSides(1)); - }); - row.append(&plus.button); - - card.body.append(&row); - ctx.updaters.push(Box::new(move |snapshot| { - label.set_text(&format!("{} sides", snapshot.polygon_sides)); - })); - card.root.upcast() -} diff --git a/src/toolbar_gtk/view/side_bar.rs b/src/toolbar_gtk/view/side_bar.rs deleted file mode 100644 index 0204da18..00000000 --- a/src/toolbar_gtk/view/side_bar.rs +++ /dev/null @@ -1,250 +0,0 @@ -//! The GTK side palette. -//! -//! This module owns the side-window lifecycle. Widget assembly, chrome, -//! structure tracking, and drag adaptation live in focused child modules. - -mod chrome; -mod drag; -mod palette; -mod structure; - -#[cfg(test)] -mod tests; - -use std::cell::Cell; -use std::collections::BTreeSet; -use std::rc::Rc; - -use gtk4::prelude::*; -use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; - -use crate::toolbar_icons; -use crate::ui::toolbar::model::{SideHeaderModel, ToolbarPresentationPayload}; -use crate::ui::toolbar::snapshot::ToolContext; -use crate::ui::toolbar::{SidePane, ToolbarEvent, ToolbarSideSection, ToolbarSnapshot}; - -use super::super::icons::IconWidget; -use super::super::widgets::{ - FeedbackSender, install_shortcut_focus_policy, send_event, set_active_class, sized_button, -}; -use super::super::{GtkToolbarDragPhase, GtkToolbarFeedback, GtkToolbarKind}; -use super::{CaptureProofTarget, CaptureSurfaceContent, Updater, sections}; -use structure::{StructureKey, effective_scale}; - -const SIDE_WIDTH: f64 = 260.0; -const MINIMIZED_SIZE: (f64, f64) = (24.0, 64.0); -const BASE_MARGIN: (i32, i32) = (24, 24); // (top, left) -const END_MARGIN: (f64, f64) = (0.0, 24.0); - -pub(in crate::toolbar_gtk) struct SideBar { - pub(in crate::toolbar_gtk) window: gtk4::Window, - feedback: FeedbackSender, - root: gtk4::Box, - capture_surface: CaptureSurfaceContent, - structure: Option, - /// Chrome updaters survive content rebuilds; content updaters are - /// replaced together with the pane body. - chrome_updaters: Vec, - content_updaters: Vec, - scrolled: Option, - /// Per-pane scroll positions, saved on rebuild and restored when the - /// same pane is built again — switching panes must not leak one - /// pane's scroll into another (the backend keeps per-pane offsets - /// too; this mirrors that behavior GTK-side). - saved_scroll: std::rc::Rc>>, - drag_active: Rc>, - drag_blocked: Rc>, - move_drag: Option, - move_drag_cancel: Option>, - offsets: Rc>, - /// Monotonic counter for outgoing drag offsets; stale echoes from the - /// backend are ignored by comparing against it. - offset_seq: Rc>, - capture_suppressed: bool, - mapped_before_capture: bool, -} - -impl SideBar { - pub(in crate::toolbar_gtk) fn new(feedback: FeedbackSender) -> Self { - let window = gtk4::Window::new(); - window.add_css_class("wayscriber-toolbar"); - window.init_layer_shell(); - window.set_layer(Layer::Overlay); - window.set_namespace(Some("wayscriber-toolbar-side")); - window.set_anchor(Edge::Left, true); - window.set_anchor(Edge::Top, true); - window.set_margin(Edge::Top, BASE_MARGIN.0); - window.set_margin(Edge::Left, BASE_MARGIN.1); - // Stay focusable for the editable hex field, but relinquish focus - // immediately after ordinary toolbar interaction. - window.set_keyboard_mode(KeyboardMode::OnDemand); - install_shortcut_focus_policy(&window, &feedback); - // Match the built-in bars: do not shift for other exclusive zones - // (panels/bars) and do not reserve one. - window.set_exclusive_zone(-1); - - let root = gtk4::Box::new(gtk4::Orientation::Vertical, 0); - root.add_css_class("panel"); - let capture_surface = CaptureSurfaceContent::new(&root); - window.set_child(Some(capture_surface.widget())); - - // Hovering the side palette holds the top-strip idle fade, matching - // the built-in bars where `pointer_over_toolbar` covers both bars. - // The same `TopHover` feedback is reused: GTK runs on its own - // Wayland connection, so the backend cannot observe this pointer - // itself, and leave/enter arrive in protocol order when the pointer - // crosses between the two toolbar windows. - let hover = gtk4::EventControllerMotion::new(); - let enter_feedback = feedback.clone(); - hover.connect_enter(move |_, _, _| { - let _ = enter_feedback.send(GtkToolbarFeedback::TopHover { hovered: true }); - }); - let leave_feedback = feedback.clone(); - hover.connect_leave(move |_| { - let _ = leave_feedback.send(GtkToolbarFeedback::TopHover { hovered: false }); - }); - window.add_controller(hover); - - Self { - window, - feedback, - root, - capture_surface, - structure: None, - chrome_updaters: Vec::new(), - content_updaters: Vec::new(), - scrolled: None, - saved_scroll: Rc::new(std::cell::RefCell::new(Vec::new())), - drag_active: Rc::new(Cell::new(false)), - drag_blocked: Rc::new(Cell::new(false)), - move_drag: None, - move_drag_cancel: None, - offsets: Rc::new(Cell::new((0.0, 0.0))), - offset_seq: Rc::new(Cell::new(0)), - capture_suppressed: false, - mapped_before_capture: false, - } - } - - pub(in crate::toolbar_gtk) fn apply( - &mut self, - update: &super::super::GtkToolbarUpdate, - ) -> bool { - let snapshot = &update.snapshot; - let entering_capture_suppression = update.capture_suppressed && !self.capture_suppressed; - if entering_capture_suppression { - self.mapped_before_capture = self.window.is_visible() || self.window.is_mapped(); - } else if !update.capture_suppressed { - self.mapped_before_capture = false; - } - self.capture_suppressed = update.capture_suppressed; - self.drag_blocked - .set(update.modal_engaged || update.capture_suppressed); - if entering_capture_suppression && let Some(cancel) = self.move_drag_cancel.as_ref() { - cancel(); - } - let presentation = super::toolbar_surface_presentation( - update.side_visible, - update.capture_suppressed, - super::drag_visual_should_be_hidden( - update.drag_preview, - GtkToolbarKind::Side, - self.drag_active.get(), - self.offset_seq.get(), - update.side_offset_seq, - ), - self.mapped_before_capture, - ); - if !presentation.window_visible { - self.capture_surface - .set_transparent(presentation.capture_transparent); - super::set_surface_input_enabled(&self.window, false); - self.window.set_visible(false); - if let Some(generation) = update.capture_suppression_generation { - super::log_capture_surface_state( - generation, - super::CaptureSurfaceLog { - name: "side", - configured_visible: update.side_visible, - mapped_before_capture: self.mapped_before_capture, - presentation, - window: &self.window, - visual: &self.root, - capture_surface: &self.capture_surface, - }, - ); - } - return false; - } - self.apply_offsets(update.side_offset, update.side_offset_seq); - - let key = StructureKey::of(snapshot); - if self.structure.as_ref() != Some(&key) { - self.rebuild(snapshot); - self.structure = Some(key); - } - for updater in self.chrome_updaters.iter().chain(&self.content_updaters) { - updater(snapshot); - } - self.sync_viewport(snapshot); - self.window.set_visible(true); - self.capture_surface - .set_transparent(presentation.capture_transparent); - super::set_visual_hidden( - &self.window, - &self.root, - GtkToolbarKind::Side, - presentation.visual_hidden, - ); - super::set_surface_input_enabled(&self.window, presentation.input_enabled); - if let Some(generation) = update.capture_suppression_generation { - super::log_capture_surface_state( - generation, - super::CaptureSurfaceLog { - name: "side", - configured_visible: update.side_visible, - mapped_before_capture: self.mapped_before_capture, - presentation, - window: &self.window, - visual: &self.root, - capture_surface: &self.capture_surface, - }, - ); - } - true - } - - pub(in crate::toolbar_gtk::view) fn capture_target(&self) -> CaptureProofTarget { - CaptureProofTarget::new("side", &self.window, &self.capture_surface) - } - - /// Mirror backend offsets into layer margins unless a local drag is in - /// flight or the echo is older than what this bar already sent. Side - /// offsets are (x, y) like the update carries them. - fn apply_offsets(&self, offsets: (f64, f64), echo_seq: u64) { - if self.drag_active.get() || echo_seq < self.offset_seq.get() { - crate::toolbar_gtk::drag_debug_log(format!( - "side echo rejected echo_seq={echo_seq} local_seq={} active={} backend=({:.3},{:.3}) local=({:.3},{:.3})", - self.offset_seq.get(), - self.drag_active.get(), - offsets.0, - offsets.1, - self.offsets.get().0, - self.offsets.get().1, - )); - return; - } - let (left, x) = super::drag::rounded_margin_and_offset(BASE_MARGIN.1 as f64, offsets.0); - let (top, y) = super::drag::rounded_margin_and_offset(BASE_MARGIN.0 as f64, offsets.1); - self.offsets.set((x, y)); - self.window.set_margin(Edge::Left, left); - self.window.set_margin(Edge::Top, top); - crate::toolbar_gtk::drag_debug_log(format!( - "side echo applied echo_seq={echo_seq} backend=({:.3},{:.3}) stored=({x:.3},{y:.3}) margin=({left},{top}) size={}x{}", - offsets.0, - offsets.1, - self.window.width(), - self.window.height(), - )); - } -} diff --git a/src/toolbar_gtk/view/side_bar/chrome.rs b/src/toolbar_gtk/view/side_bar/chrome.rs deleted file mode 100644 index b1d05cc0..00000000 --- a/src/toolbar_gtk/view/side_bar/chrome.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! Side-palette header controls. - -use super::*; -use crate::ui::theme::{Rgba, set_color}; - -/// Empty board-dot outline: muted blue-gray with no theme token (mirrors -/// the built-in `side_palette::header`). -/// TODO(theme-consolidation): hoist the mirrored pair into `theme::toolbar`. -const COLOR_BOARD_CHIP_EMPTY_DOT: Rgba = (0.62, 0.68, 0.76, 0.7); - -/// Board chip color dot, RGBA; `None` draws the empty outline. -type BoardDotColor = Rc>>; - -impl SideBar { - pub(super) fn board_chip(&mut self, _snapshot: &ToolbarSnapshot, scale: f64) -> gtk4::Button { - let chip = gtk4::Button::new(); - chip.add_css_class("board-chip"); - chip.set_hexpand(true); - chip.set_size_request(-1, (22.0 * scale).round() as i32); - let row = gtk4::Box::new(gtk4::Orientation::Horizontal, (4.0 * scale).round() as i32); - let dot_color: BoardDotColor = Rc::new(Cell::new(None)); - let dot = gtk4::DrawingArea::new(); - let dot_size = (14.0 * scale).round() as i32; - dot.set_content_width(dot_size); - dot.set_content_height(dot_size); - dot.set_valign(gtk4::Align::Center); - let draw_color = dot_color.clone(); - dot.set_draw_func(move |_, ctx, width, height| { - let size = width.min(height) as f64; - match draw_color.get() { - Some((r, g, b, a)) => { - super::super::super::widgets::rounded_rect_path( - ctx, - 0.5, - 0.5, - size - 1.0, - size - 1.0, - 3.0, - ); - ctx.set_source_rgba(r, g, b, a); - let _ = ctx.fill(); - } - None => { - super::super::super::widgets::rounded_rect_path( - ctx, - 0.5, - 0.5, - size - 1.0, - size - 1.0, - 3.0, - ); - set_color(ctx, COLOR_BOARD_CHIP_EMPTY_DOT); - ctx.set_line_width(1.0); - let _ = ctx.stroke(); - } - } - }); - row.append(&dot); - let board_icon = IconWidget::new(toolbar_icons::draw_icon_board, 10.0 * scale); - row.append(&board_icon.area); - let label = gtk4::Label::new(None); - label.set_ellipsize(gtk4::pango::EllipsizeMode::End); - label.set_xalign(0.0); - label.set_hexpand(true); - row.append(&label); - let chevron = IconWidget::new(toolbar_icons::draw_icon_chevron_right, 12.0 * scale); - row.append(&chevron.area); - chip.set_child(Some(&row)); - let sender = self.feedback.clone(); - chip.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::ToggleBoardPicker); - }); - let chip_handle = chip.clone(); - self.chrome_updaters.push(Box::new(move |snapshot| { - let header_model = SideHeaderModel::from_snapshot(snapshot); - let (text, color) = match &header_model.board_chip.presentation.payload { - ToolbarPresentationPayload::BoardChip(board) => ( - board.label.clone(), - board.color.map(|c| (c.r, c.g, c.b, c.a)), - ), - ToolbarPresentationPayload::None => { - (header_model.board_chip.presentation.label.to_string(), None) - } - }; - label.set_text(&text); - if let Some(tooltip) = header_model.board_chip.presentation.tooltip.as_string() { - chip_handle.set_tooltip_text(Some(&tooltip)); - } - if dot_color.get() != color { - dot_color.set(color); - dot.queue_draw(); - } - })); - chip - } - - pub(super) fn pin_button(&mut self, snapshot: &ToolbarSnapshot, size: f64) -> gtk4::Button { - let button = sized_button(size, size); - button.add_css_class("chrome"); - let icon = IconWidget::new( - if snapshot.side_pinned { - toolbar_icons::draw_icon_pin - } else { - toolbar_icons::draw_icon_unpin - }, - size * 0.62, - ); - button.set_child(Some(&icon.area)); - let sender = self.feedback.clone(); - let pinned = Rc::new(Cell::new(snapshot.side_pinned)); - let click_pinned = pinned.clone(); - button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::PinSideToolbar(!click_pinned.get())); - }); - let handle = button.clone(); - self.chrome_updaters.push(Box::new(move |snapshot| { - pinned.set(snapshot.side_pinned); - icon.set_painter(if snapshot.side_pinned { - toolbar_icons::draw_icon_pin - } else { - toolbar_icons::draw_icon_unpin - }); - if snapshot.side_pinned { - handle.add_css_class("pinned"); - handle.set_tooltip_text(Some("Pinned: opens at startup (click to disable)")); - } else { - handle.remove_css_class("pinned"); - handle.set_tooltip_text(Some("Pin: click to open at startup")); - } - })); - button - } - - pub(super) fn minimize_button(&mut self, size: f64) -> gtk4::Button { - let button = sized_button(size, size); - button.add_css_class("chrome"); - button.add_css_class("minimize"); - button.set_tooltip_text(Some("Minimize (leaves a restore tab)")); - let icon = IconWidget::new(toolbar_icons::draw_icon_side_minimize, size * 0.6); - button.set_child(Some(&icon.area)); - let sender = self.feedback.clone(); - button.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::SetSideMinimized(true)); - }); - button - } -} diff --git a/src/toolbar_gtk/view/side_bar/drag.rs b/src/toolbar_gtk/view/side_bar/drag.rs deleted file mode 100644 index abf337ef..00000000 --- a/src/toolbar_gtk/view/side_bar/drag.rs +++ /dev/null @@ -1,225 +0,0 @@ -//! Side-toolbar adapter for the shared GTK drag lifecycle. - -use super::super::drag as shared_drag; -use super::*; - -impl SideBar { - /// Keep the input surface parked and coalesce stable start-relative - /// motion into the inline preview. - pub(super) fn attach_move_drag(&mut self, grip: >k4::DrawingArea) { - if let Some(cancel) = self.move_drag_cancel.take() { - cancel(); - } - if let Some(previous) = self.move_drag.take() { - previous.reset(); - self.window.remove_controller(&previous); - } - let drag = gtk4::GestureDrag::new(); - let window = self.window.clone(); - let feedback = self.feedback.clone(); - let drag_active = self.drag_active.clone(); - let offsets = self.offsets.clone(); - let seq = self.offset_seq.clone(); - let pending = Rc::new(shared_drag::FrameCoalescedDrag::default()); - let active_generation = Rc::new(Cell::new(0)); - let ready_generation = Rc::new(Cell::new(0)); - let drag_origin = Rc::new(Cell::new((0.0, 0.0))); - - let begin_active = drag_active.clone(); - let begin_blocked = self.drag_blocked.clone(); - let begin_pending = pending.clone(); - let begin_generation = active_generation.clone(); - let begin_ready = ready_generation.clone(); - let begin_origin = drag_origin.clone(); - let begin_window = window.downgrade(); - let begin_visual = self.root.downgrade(); - let begin_grip = grip.downgrade(); - let frame_offsets = offsets.clone(); - let frame_feedback = feedback.clone(); - let frame_seq = seq.clone(); - drag.connect_drag_begin(move |gesture, start_x, start_y| { - if begin_blocked.get() { - gesture.set_state(gtk4::EventSequenceState::Denied); - return; - } - let (Some(frame_window), Some(visual), Some(grip)) = ( - begin_window.upgrade(), - begin_visual.upgrade(), - begin_grip.upgrade(), - ) else { - gesture.set_state(gtk4::EventSequenceState::Denied); - return; - }; - let start = gtk4::graphene::Point::new(start_x as f32, start_y as f32); - if !grip - .compute_bounds(&frame_window) - .is_some_and(|bounds| bounds.contains_point(&start)) - { - gesture.set_state(gtk4::EventSequenceState::Denied); - return; - } - begin_active.set(true); - let generation = begin_pending.begin(); - begin_generation.set(generation); - begin_origin.set(frame_offsets.get()); - let start_seq = shared_drag::ReservedDragSequence::reserve(&frame_seq); - let origin = begin_origin.get(); - let start = GtkToolbarFeedback::SetSideOffset { - x: origin.0, - y: origin.1, - surface_size: crate::toolbar_gtk::GtkToolbarSurfaceSize::from_window( - &frame_window, - ), - seq: start_seq.value(), - phase: GtkToolbarDragPhase::Start, - }; - super::super::set_visual_hidden( - &frame_window, - &visual, - GtkToolbarKind::Side, - true, - ); - let start_feedback = frame_feedback.clone(); - let start_window = frame_window.clone(); - let start_visual = visual.clone(); - let start_active = begin_active.clone(); - let start_generation = begin_generation.clone(); - let start_ready = begin_ready.clone(); - let start_sequence = frame_seq.clone(); - super::super::after_next_surface_paint(&frame_window, move || { - if start_generation.get() != generation { - return; - } - if start_feedback.send(start).is_ok() { - start_seq.publish(&start_sequence); - start_ready.set(generation); - } else { - super::super::set_visual_hidden( - &start_window, - &start_visual, - GtkToolbarKind::Side, - false, - ); - start_active.set(false); - start_generation.set(0); - } - }); - - let pending = begin_pending.clone(); - let window = frame_window.clone(); - let offsets = frame_offsets.clone(); - let feedback = frame_feedback.clone(); - let seq = frame_seq.clone(); - let drag_active = begin_active.clone(); - let active_generation = begin_generation.clone(); - let ready_generation = begin_ready.clone(); - let drag_origin = begin_origin.clone(); - frame_window.add_tick_callback(move |_, _| { - if active_generation.get() != generation { - return gtk4::glib::ControlFlow::Break; - } - if ready_generation.get() != generation { - return gtk4::glib::ControlFlow::Continue; - } - let Some(frame) = pending.take_frame(generation) else { - return gtk4::glib::ControlFlow::Continue; - }; - let (cx, cy) = offsets.get(); - let (next_x, next_y) = - shared_drag::drag_frame_position(drag_origin.get(), frame.delta); - let (x, y) = shared_drag::clamp_drag_offsets( - &window, - (next_x, next_y), - (BASE_MARGIN.1 as f64, BASE_MARGIN.0 as f64), - END_MARGIN, - ); - offsets.set((x, y)); - seq.set(seq.get() + 1); - crate::toolbar_gtk::drag_debug_log(format!( - "side frame generation={generation} seq={} phase={:?} delta=({:.3},{:.3}) origin=({:.3},{:.3}) before=({cx:.3},{cy:.3}) preview=({x:.3},{y:.3}) parked_margin=({}, {}) size={}x{}", - seq.get(), - frame.phase, - frame.delta.0, - frame.delta.1, - drag_origin.get().0, - drag_origin.get().1, - window.margin(Edge::Left), - window.margin(Edge::Top), - window.width(), - window.height(), - )); - let _ = feedback.send(GtkToolbarFeedback::SetSideOffset { - x, - y, - surface_size: crate::toolbar_gtk::GtkToolbarSurfaceSize::from_window(&window), - seq: seq.get(), - phase: frame.phase, - }); - if frame.phase.is_end() { - if active_generation.get() == generation { - drag_active.set(false); - active_generation.set(0); - ready_generation.set(0); - } - gtk4::glib::ControlFlow::Break - } else { - gtk4::glib::ControlFlow::Continue - } - }); - }); - - let update_pending = pending.clone(); - let update_generation = active_generation.clone(); - drag.connect_drag_update(move |_, dx, dy| { - let generation = update_generation.get(); - crate::toolbar_gtk::drag_debug_log(format!( - "side raw generation={generation} start_relative=({dx:.3},{dy:.3})", - )); - update_pending.update(generation, dx, dy); - }); - - let end_pending = pending.clone(); - let end_generation = active_generation.clone(); - drag.connect_drag_end(move |_, dx, dy| { - crate::toolbar_gtk::drag_debug_log(format!( - "side end generation={} delta=({dx:.3},{dy:.3})", - end_generation.get(), - )); - end_pending.end(end_generation.get(), dx, dy); - }); - - let cancel_window = window.downgrade(); - let cancel_visual = self.root.downgrade(); - let cancel_feedback = feedback; - let cancel_active = drag_active; - let cancel_generation = active_generation; - let cancel_ready = ready_generation; - let cancel_offsets = offsets; - let cancel_seq = seq; - let cancel: Rc = Rc::new(move || { - let (Some(window), Some(visual)) = (cancel_window.upgrade(), cancel_visual.upgrade()) - else { - cancel_active.set(false); - cancel_generation.set(0); - cancel_ready.set(0); - return; - }; - shared_drag::cancel_move_drag( - GtkToolbarKind::Side, - &window, - &visual, - &cancel_feedback, - &cancel_active, - &cancel_generation, - &cancel_ready, - &cancel_offsets, - &cancel_seq, - ); - }); - let signal_cancel = cancel.clone(); - drag.connect_cancel(move |_, _| signal_cancel()); - window.add_controller(drag.clone()); - self.move_drag = Some(drag); - self.move_drag_cancel = Some(cancel); - } -} diff --git a/src/toolbar_gtk/view/side_bar/palette.rs b/src/toolbar_gtk/view/side_bar/palette.rs deleted file mode 100644 index 07a3ff3f..00000000 --- a/src/toolbar_gtk/view/side_bar/palette.rs +++ /dev/null @@ -1,161 +0,0 @@ -//! Side-palette assembly, pane navigation, and scroll restoration. - -use super::*; - -impl SideBar { - pub(super) fn sync_viewport(&self, snapshot: &ToolbarSnapshot) { - // Resizing a layer surface while its gesture coordinates are being - // consumed makes the surface catch up after button release. Keep the - // allocated height stable for the gesture; the final backend echo - // applies the viewport for the accepted resting position. - if self.drag_active.get() { - return; - } - let Some(scrolled) = &self.scrolled else { - return; - }; - let scale = effective_scale(snapshot); - if let Some(viewport) = snapshot.side_viewport_max { - // The viewport budget covers the whole palette; subtract the - // fixed chrome above the scrolled body. - let chrome = 76.0; - let max = ((viewport - chrome).max(120.0) * scale).round() as i32; - scrolled.set_max_content_height(max); - } - } - - pub(super) fn rebuild(&mut self, snapshot: &ToolbarSnapshot) { - // Preserve the outgoing pane's scroll position, keyed by pane so - // it is only restored into the same pane. - if let (Some(scrolled), Some(key)) = (&self.scrolled, &self.structure) { - let value = scrolled.vadjustment().value(); - let mut saved = self.saved_scroll.borrow_mut(); - saved.retain(|(pane, _)| *pane != key.pane); - saved.push((key.pane, value)); - } - while let Some(child) = self.root.first_child() { - self.root.remove(&child); - } - self.chrome_updaters.clear(); - self.content_updaters.clear(); - self.scrolled = None; - - if snapshot.side_minimized { - self.build_minimized(snapshot); - } else { - self.build_palette(snapshot); - } - } - - fn build_minimized(&mut self, snapshot: &ToolbarSnapshot) { - let scale = effective_scale(snapshot); - self.root.add_css_class("minimized"); - // Drop the palette's 260px default width so the tab shrinks. - self.window.set_default_size( - (MINIMIZED_SIZE.0 * scale).round() as i32, - (MINIMIZED_SIZE.1 * scale).round() as i32, - ); - let restore = sized_button(MINIMIZED_SIZE.0 * scale, MINIMIZED_SIZE.1 * scale); - restore.add_css_class("chrome"); - restore.set_tooltip_text(Some("Show toolbar")); - let icon = IconWidget::new( - toolbar_icons::draw_icon_chevron_right, - (MINIMIZED_SIZE.0 * 0.75 * scale).min(18.0 * scale), - ); - restore.set_child(Some(&icon.area)); - let sender = self.feedback.clone(); - restore.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::SetSideMinimized(false)); - }); - self.root.append(&restore); - } - - fn build_palette(&mut self, snapshot: &ToolbarSnapshot) { - self.root.remove_css_class("minimized"); - let scale = effective_scale(snapshot); - let px = |value: f64| (value * scale).round() as i32; - self.window - .set_default_size((SIDE_WIDTH * scale).round() as i32, -1); - - // ===== Header band: grip · board chip · pin · minimize ===== - let band = gtk4::Box::new(gtk4::Orientation::Horizontal, px(6.0)); - band.add_css_class("header-band"); - - let grip = IconWidget::new(toolbar_icons::draw_icon_drag, 18.0 * scale); - grip.area.set_can_target(true); - grip.area.add_css_class("drag-handle"); - grip.area.set_tooltip_text(Some("Drag toolbar")); - grip.area.set_valign(gtk4::Align::Center); - grip.area.set_cursor_from_name(Some("grab")); - self.attach_move_drag(&grip.area); - band.append(&grip.area); - - let chip = self.board_chip(snapshot, scale); - band.append(&chip); - - band.append(&self.pin_button(snapshot, 22.0 * scale)); - band.append(&self.minimize_button(22.0 * scale)); - self.root.append(&band); - - // ===== Pane navigation ===== - let nav = gtk4::Box::new(gtk4::Orientation::Horizontal, px(4.0)); - nav.set_homogeneous(true); - nav.set_margin_top(px(6.0)); - for pane in SidePane::ALL { - let tab = gtk4::Button::with_label(pane.label()); - tab.add_css_class("tab"); - tab.set_size_request(-1, px(26.0)); - tab.set_tooltip_text(Some(&format!("{} pane", pane.label()))); - let sender = self.feedback.clone(); - tab.connect_clicked(move |_| { - send_event(&sender, ToolbarEvent::SetSidePane(pane)); - }); - let handle = tab.clone(); - self.chrome_updaters.push(Box::new(move |snapshot| { - set_active_class(&handle, snapshot.active_side_pane == pane); - })); - nav.append(&tab); - } - self.root.append(&nav); - - // ===== Scrolled pane body ===== - let mut content_updaters = Vec::new(); - let content = sections::build_pane_content( - snapshot, - self.feedback.clone(), - scale, - &mut content_updaters, - ); - content.set_margin_top(px(8.0)); - self.content_updaters = content_updaters; - - let scrolled = gtk4::ScrolledWindow::new(); - scrolled.set_policy(gtk4::PolicyType::Never, gtk4::PolicyType::Automatic); - scrolled.set_propagate_natural_height(true); - scrolled.set_child(Some(&content)); - self.root.append(&scrolled); - - // Restore this pane's scroll position once the new content is - // laid out; one-shot so later size changes never yank the user's - // scroll. - let adjustment = scrolled.vadjustment(); - let pane = snapshot.active_side_pane; - let pending = std::cell::Cell::new( - self.saved_scroll - .borrow() - .iter() - .find(|(saved_pane, _)| *saved_pane == pane) - .map(|(_, value)| *value) - .unwrap_or(0.0), - ); - adjustment.connect_changed(move |adjustment| { - let target = pending.get(); - let reachable = adjustment.upper() - adjustment.page_size(); - if target > 0.0 && reachable >= target { - adjustment.set_value(target); - pending.set(0.0); - } - }); - self.scrolled = Some(scrolled); - } -} diff --git a/src/toolbar_gtk/view/side_bar/structure.rs b/src/toolbar_gtk/view/side_bar/structure.rs deleted file mode 100644 index 5bb7769a..00000000 --- a/src/toolbar_gtk/view/side_bar/structure.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Snapshot inputs that determine side-palette widget structure. - -use super::*; - -/// Discrete inputs that force a rebuild of the pane content. -#[derive(PartialEq)] -pub(super) struct StructureKey { - pub(super) minimized: bool, - pub(super) pane: SidePane, - scale_milli: i64, - use_icons: bool, - layout_mode: crate::config::ToolbarLayoutMode, - items: crate::config::ResolvedToolbarItems, - /// Shortcut hints shown in button tooltips (e.g. the command-palette - /// button). Rebuild when a rebind changes them so a tooltip never keeps - /// showing a stale shortcut, matching the top bar's structure key. - binding_hints: crate::ui::toolbar::ToolbarBindingHints, - collapsed: BTreeSet, - tool_flags: (bool, bool, bool, bool, bool, bool), - show_more_colors: bool, - quick_colors: crate::config::QuickColorPalette, - preset_slots: Vec, - custom_section_enabled: bool, - show_delay_sliders: bool, - delay_actions_enabled: bool, - show_actions_advanced: bool, - show_zoom_actions: bool, - customize_open: bool, - customize_group: Option, - status_bar_contents_open: bool, - runtime_ui_persistence: Option, - recents: Vec, - pending_save_as: Option, - active_session: Option, - eraser_kind_targets: (bool, bool), - polygon_active: bool, - font_mono: bool, - /// Drives the scoped section titles ("Color — Pen"): tool or text/note - /// scope changes must rebuild even when nothing else did. - title_scope: (crate::input::Tool, bool, bool, bool), -} - -impl StructureKey { - pub(super) fn of(snapshot: &ToolbarSnapshot) -> Self { - let tool_context = ToolContext::from_snapshot(snapshot); - Self { - minimized: snapshot.side_minimized, - pane: snapshot.active_side_pane, - scale_milli: (effective_scale(snapshot) * 1000.0).round() as i64, - use_icons: snapshot.use_icons, - layout_mode: snapshot.layout_mode, - items: snapshot.resolved_toolbar_items.clone(), - binding_hints: snapshot.binding_hints.clone(), - collapsed: snapshot.collapsed_side_sections.clone(), - tool_flags: ( - tool_context.needs_color, - tool_context.needs_thickness, - tool_context.show_arrow_labels, - tool_context.show_step_counter, - tool_context.show_marker_opacity, - tool_context.show_font_controls, - ), - show_more_colors: snapshot.show_more_colors, - quick_colors: snapshot.quick_colors.clone(), - preset_slots: snapshot.presets.iter().map(|slot| slot.is_some()).collect(), - custom_section_enabled: snapshot.custom_section_enabled, - show_delay_sliders: snapshot.show_delay_sliders, - delay_actions_enabled: snapshot.delay_actions_enabled, - show_actions_advanced: snapshot.show_actions_advanced, - show_zoom_actions: snapshot.show_zoom_actions, - customize_open: snapshot.customize_items_open, - customize_group: snapshot.customize_items_group, - status_bar_contents_open: snapshot.status_bar_contents_open, - runtime_ui_persistence: snapshot.runtime_ui_persistence.clone(), - recents: snapshot - .recent_sessions - .iter() - .map(|recent| recent.path.clone()) - .collect(), - pending_save_as: snapshot.pending_save_as_overwrite_path.clone(), - active_session: snapshot.active_session_path.clone(), - eraser_kind_targets: ( - snapshot.thickness_targets_eraser, - snapshot.thickness_targets_marker, - ), - polygon_active: snapshot.active_tool == crate::input::Tool::RegularPolygon - || snapshot.tool_override == Some(crate::input::Tool::RegularPolygon), - font_mono: snapshot.font.family.eq_ignore_ascii_case("monospace"), - title_scope: ( - snapshot.active_tool, - snapshot.text_active, - snapshot.note_active, - snapshot.context_aware_ui, - ), - } - } -} - -pub(super) fn effective_scale(snapshot: &ToolbarSnapshot) -> f64 { - if snapshot.toolbar_scale.is_finite() { - snapshot.toolbar_scale.clamp(0.5, 3.0) - } else { - 1.0 - } -} diff --git a/src/toolbar_gtk/view/side_bar/tests.rs b/src/toolbar_gtk/view/side_bar/tests.rs deleted file mode 100644 index 8cbd0437..00000000 --- a/src/toolbar_gtk/view/side_bar/tests.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! GTK side-palette unit tests. - -use std::collections::HashMap; - -use super::*; -use crate::config::{Action, KeyBinding}; -use crate::input::state::test_support::make_test_input_state; -use crate::ui::toolbar::{ - RuntimeUiPersistenceMode, RuntimeUiPersistenceSnapshot, ToolbarBindingHints, -}; - -/// Rebinding an action shown in a side-pane button tooltip (here the -/// command-palette button) must change the structure key so the pane -/// rebuilds and the tooltip stops showing the old shortcut. -#[test] -fn side_structure_rebuilds_when_a_button_shortcut_changes() { - let mut state = make_test_input_state(); - let initial = ToolbarSnapshot::from_input_with_bindings( - &state, - ToolbarBindingHints::from_input_state(&state), - ); - let initial_key = StructureKey::of(&initial); - - state.set_action_bindings(HashMap::from([( - Action::ToggleCommandPalette, - vec![KeyBinding::parse("Ctrl+Alt+K").expect("binding")], - )])); - - let changed = ToolbarSnapshot::from_input_with_bindings( - &state, - ToolbarBindingHints::from_input_state(&state), - ); - let changed_key = StructureKey::of(&changed); - - assert!( - initial_key != changed_key, - "a shortcut change must rebuild the side pane so tooltips refresh" - ); -} - -#[test] -fn side_structure_rebuilds_when_runtime_persistence_controls_change() { - let state = make_test_input_state(); - let mut unsupported = ToolbarSnapshot::from_input_with_bindings( - &state, - ToolbarBindingHints::from_input_state(&state), - ); - unsupported.runtime_ui_persistence = Some(RuntimeUiPersistenceSnapshot { - path: "/tmp/runtime-ui.toml".into(), - mode: RuntimeUiPersistenceMode::UnsupportedReadOnly { version: Some(2) }, - detail: None, - recovery_artifacts: Vec::new(), - }); - let mut confirmation = unsupported.clone(); - confirmation.runtime_ui_persistence.as_mut().unwrap().mode = - RuntimeUiPersistenceMode::AwaitingUnsupportedResetConfirmation { version: Some(2) }; - - assert!( - StructureKey::of(&unsupported) != StructureKey::of(&confirmation), - "the side settings pane must replace reset with confirm/cancel controls" - ); -} - -#[test] -fn side_structure_rebuilds_for_status_bar_contents_subpanel() { - let state = make_test_input_state(); - let closed = ToolbarSnapshot::from_input_with_bindings( - &state, - ToolbarBindingHints::from_input_state(&state), - ); - let mut open = closed.clone(); - open.status_bar_contents_open = true; - - assert!( - StructureKey::of(&closed) != StructureKey::of(&open), - "opening status-bar contents must rebuild the GTK Settings pane" - ); -} diff --git a/src/toolbar_gtk/view/top_bar.rs b/src/toolbar_gtk/view/top_bar.rs index 41cdd145..65bcadf2 100644 --- a/src/toolbar_gtk/view/top_bar.rs +++ b/src/toolbar_gtk/view/top_bar.rs @@ -102,8 +102,8 @@ type ShapesContentKey = (Tool, Option, bool, u8); /// active states). type OverflowContentKey = (Tool, Option, bool, bool, bool, bool, bool, bool); -/// Snapshot inputs the Canvas popover content renders from — the Boards / -/// Pages / Advanced / Zoom command sections plus the Step Undo/Redo config +/// Snapshot inputs the Canvas popover content renders from — the Actions / +/// Boards / Pages / Advanced / Zoom command sections plus the Step Undo/Redo config /// are a pure function of these. Enabled states and glyph faces that shift /// without a structure rebuild are captured so the popover stays fresh. /// @@ -121,6 +121,7 @@ type OverflowContentKey = (Tool, Option, bool, bool, bool, bool, bool, boo struct CanvasMenuContentKey { items: crate::config::ResolvedToolbarItems, use_icons: bool, + show_actions_section: bool, show_boards_section: bool, show_pages_section: bool, show_zoom_actions: bool, @@ -147,6 +148,7 @@ impl CanvasMenuContentKey { Self { items: snapshot.resolved_toolbar_items.clone(), use_icons: snapshot.use_icons, + show_actions_section: snapshot.show_actions_section, show_boards_section: snapshot.show_boards_section, show_pages_section: snapshot.show_pages_section, show_zoom_actions: snapshot.show_zoom_actions, @@ -252,7 +254,7 @@ struct StructureKey { layout_mode: ToolbarLayoutMode, scale_milli: i64, /// Exact renderer-neutral top structure. Using the full resolved item store - /// here made side-only section changes tear down this bar and its popovers. + /// here made popover-only section changes tear down this bar and its popovers. top_spec: model::TopToolbarSpec, quick_colors: crate::config::QuickColorPalette, binding_hints: crate::ui::toolbar::ToolbarBindingHints, @@ -388,7 +390,7 @@ pub(in crate::toolbar_gtk) struct TopBar { move_drag: Option, move_drag_cancel: Option>, offsets: Rc>, - /// Base X in spec units from the backend (side palette pushes it). + /// Base X in spec units supplied by the backend placement owner. base_x: Rc>, /// Monotonic counter for outgoing drag offsets; stale echoes from the /// backend are ignored by comparing against it. diff --git a/src/toolbar_gtk/view/top_bar/controls.rs b/src/toolbar_gtk/view/top_bar/controls.rs index ffa3ab47..77bfdd6d 100644 --- a/src/toolbar_gtk/view/top_bar/controls.rs +++ b/src/toolbar_gtk/view/top_bar/controls.rs @@ -225,8 +225,8 @@ impl TopBar { /// A presets-island slot button: a filled slot draws the saved tool /// glyph in the neutral foreground with the preset color as a separate /// corner swatch in a DrawingArea child, so a dark preset color never - /// renders the glyph invisible against the slot body (the side-palette - /// convention). An empty slot shows its 1-based number as a plain label. + /// renders the glyph invisible against the slot body. An empty slot shows + /// its 1-based number as a plain label. /// Clicking applies a filled slot or saves the current setup into an empty /// one (the shared spec owns the event). pub(super) fn preset_button( diff --git a/src/toolbar_gtk/view/top_bar/popovers.rs b/src/toolbar_gtk/view/top_bar/popovers.rs index a5db7e7d..37b1088d 100644 --- a/src/toolbar_gtk/view/top_bar/popovers.rs +++ b/src/toolbar_gtk/view/top_bar/popovers.rs @@ -360,7 +360,7 @@ impl TopBar { scale: f64, ) -> gtk4::Widget { let mut scratch_updaters = Vec::new(); - let mut ctx = super::super::sections::SectionCtx { + let ctx = super::super::sections::SectionCtx { snapshot, feedback: self.feedback.clone(), scale, @@ -369,7 +369,7 @@ impl TopBar { }; let content = match model::ToolbarSessionModel::for_popover(snapshot) { Some(session) => { - super::super::sections::session_pane::build_popover_content(&mut ctx, &session) + super::super::sections::session_pane::build_popover_content(&ctx, &session) } None => gtk4::Box::new(gtk4::Orientation::Vertical, 0), }; diff --git a/src/toolbar_gtk/view/top_bar/tests.rs b/src/toolbar_gtk/view/top_bar/tests.rs index 767c521f..9ce6fcf2 100644 --- a/src/toolbar_gtk/view/top_bar/tests.rs +++ b/src/toolbar_gtk/view/top_bar/tests.rs @@ -196,7 +196,7 @@ fn settings_popover_rebuilds_when_item_visibility_changes_outside_customization( } #[test] -fn top_structure_ignores_side_only_section_visibility_changes() { +fn top_structure_ignores_popover_only_section_visibility_changes() { let state = make_test_input_state(); let base = ToolbarSnapshot::from_input_with_bindings( &state, @@ -239,7 +239,7 @@ fn top_structure_ignores_side_only_section_visibility_changes() { let changed_key = StructureKey::of(&changed, &plan_top_strip(&changed)); assert!( base_key == changed_key, - "side-only {flag:?} visibility must not rebuild the top bar" + "popover-only {flag:?} visibility must not rebuild the top bar" ); } } @@ -276,7 +276,14 @@ fn canvas_popover_content_key_rebuilds_on_section_and_value_changes() { ToolbarBindingHints::from_input_state(&state), ); - // A section display toggle drives a content rebuild. + // Every section display toggle drives a content rebuild. + let mut actions_toggled = base.clone(); + actions_toggled.show_actions_section = !base.show_actions_section; + assert!( + CanvasMenuContentKey::of(&base) != CanvasMenuContentKey::of(&actions_toggled), + "toggling Actions rebuilds the canvas popover content" + ); + let mut toggled = base.clone(); toggled.show_boards_section = !base.show_boards_section; assert!( @@ -2256,13 +2263,13 @@ fn actual_gtk_widgets_match_the_shared_contract_without_presenting_a_window() { .recv_timeout(Duration::from_secs(1)) .expect("GTK settings toggle event"), GtkToolbarFeedback::Event { - event: toggles[0].activation.compatibility_event(), + event: toggles[0].activation.clone(), rebind_requested: false, } ); // Backend echoes update the existing checkbox and replace its next event; - // the top popover must not depend on the side palette's active pane. + // the top popover must depend only on unified-top snapshot state. let updated_context_aware = !toggles[0].checked; settings_snapshot.context_aware_ui = updated_context_aware; for updater in &settings_updaters { @@ -2299,7 +2306,7 @@ fn actual_gtk_widgets_match_the_shared_contract_without_presenting_a_window() { .recv_timeout(Duration::from_secs(1)) .expect("GTK layout mode event"), GtkToolbarFeedback::Event { - event: segmented.segments()[0].activation.compatibility_event(), + event: segmented.segments()[0].activation.clone(), rebind_requested: false, } ); @@ -2329,6 +2336,7 @@ fn actual_gtk_widgets_match_the_shared_contract_without_presenting_a_window() { assert!(menu_top.canvas_popover.is_some(), "canvas popover exists"); let mut canvas_snapshot = regular.clone(); canvas_snapshot.canvas_popover_open = true; + canvas_snapshot.show_actions_section = true; canvas_snapshot.show_boards_section = true; canvas_snapshot.show_pages_section = true; canvas_snapshot.show_zoom_actions = true; @@ -2472,6 +2480,7 @@ fn actual_gtk_widgets_match_the_shared_contract_without_presenting_a_window() { let mut empty_canvas = regular.clone(); empty_canvas.canvas_popover_open = true; + empty_canvas.show_actions_section = false; empty_canvas.show_boards_section = false; empty_canvas.show_pages_section = false; empty_canvas.show_zoom_actions = false; diff --git a/src/toolbar_gtk/widgets.rs b/src/toolbar_gtk/widgets.rs index 79f72807..36477c66 100644 --- a/src/toolbar_gtk/widgets.rs +++ b/src/toolbar_gtk/widgets.rs @@ -14,7 +14,7 @@ use crate::draw::Color; use crate::ui::theme::{ACCENT_RGB, Rgba, rgba, set_color}; use crate::ui::toolbar::ToolbarEvent; -pub(super) use crate::ui::theme::toolbar::{COLOR_SWATCH_HAIRLINE, COLOR_SWATCH_HAIRLINE_DARK}; +pub(super) use crate::ui::theme::toolbar::COLOR_SWATCH_HAIRLINE; /// Filled (dragged) portion of the slider track: the accent at reduced /// alpha so it stays quieter than the knob (same tint as /// COLOR_SEGMENT_ACTIVE). @@ -195,75 +195,6 @@ pub(super) fn sized_button(width: f64, height: f64) -> gtk4::Button { button } -/// Request layer-shell keyboard focus only while an editable field owns GTK -/// focus, then return it to the canvas when editing finishes. -pub(super) fn keyboard_on_demand_for_entry(entry: >k4::Entry) { - entry.connect_has_focus_notify(|entry| { - if entry.has_focus() { - set_entry_keyboard_mode(entry, true); - // Pointer focus positions the caret after focus is assigned. Wait - // until that click finishes so the field starts as one atomic - // value that typing or copying can replace/read immediately. - let weak = entry.downgrade(); - gtk4::glib::idle_add_local_once(move || { - if let Some(entry) = weak.upgrade() - && entry.has_focus() - { - entry.select_region(0, -1); - } - }); - } else { - release_window_keyboard_focus(entry); - } - }); - entry.connect_activate(|entry| { - release_entry_keyboard_focus(entry); - }); - - let key = gtk4::EventControllerKey::new(); - key.connect_key_pressed(|controller, keyval, _, _| { - if keyval == gtk4::gdk::Key::Escape { - if let Some(entry) = controller.widget().and_downcast::() { - release_entry_keyboard_focus(&entry); - } - return gtk4::glib::Propagation::Stop; - } - gtk4::glib::Propagation::Proceed - }); - entry.add_controller(key); - - // GtkText normally opens its context menu with Copy disabled when the - // caret has no selection. A hex color is one atomic value, so make an - // unselected secondary click target the whole token while preserving any - // deliberate partial selection the user already made. - let context_click = gtk4::GestureClick::new(); - context_click.set_button(gtk4::gdk::BUTTON_SECONDARY); - context_click.set_propagation_phase(gtk4::PropagationPhase::Capture); - context_click.connect_pressed(|gesture, _, _, _| { - let Some(entry) = gesture.widget().and_downcast::() else { - return; - }; - if entry.selection_bounds().is_none() { - entry.select_region(0, -1); - } - }); - entry.add_controller(context_click); -} - -fn release_entry_keyboard_focus(entry: >k4::Entry) { - release_window_keyboard_focus(entry); -} - -fn set_entry_keyboard_mode(entry: >k4::Entry, editing: bool) { - if let Some(window) = entry.root().and_downcast::() { - window.set_keyboard_mode(if editing { - KeyboardMode::OnDemand - } else { - KeyboardMode::None - }); - } -} - fn release_window_keyboard_focus(widget: &impl IsA) { if let Some(window) = widget.root().and_downcast::() { window.set_keyboard_mode(KeyboardMode::None); diff --git a/src/toolbar_icons/controls.rs b/src/toolbar_icons/controls.rs index d5e6d1a7..50ab5928 100644 --- a/src/toolbar_icons/controls.rs +++ b/src/toolbar_icons/controls.rs @@ -210,10 +210,6 @@ pub fn draw_icon_minimize(ctx: &Context, x: f64, y: f64, size: f64) { super::svg::render_minimize(ctx, x, y, size); } -pub fn draw_icon_side_minimize(ctx: &Context, x: f64, y: f64, size: f64) { - super::svg::render_side_minimize(ctx, x, y, size); -} - pub fn draw_icon_restore(ctx: &Context, x: f64, y: f64, size: f64) { super::svg::render_restore(ctx, x, y, size); } diff --git a/src/toolbar_icons/mod.rs b/src/toolbar_icons/mod.rs index f801c617..59aba8a2 100644 --- a/src/toolbar_icons/mod.rs +++ b/src/toolbar_icons/mod.rs @@ -153,7 +153,7 @@ mod painter_tests { /// Every public painter. `svg.rs` covers the newer family through its own /// `render_*` entry points; this covers the shipped surface callers use, /// including the older proportional-style painters that had no coverage. - const PAINTERS: [(&str, IconPainter); 64] = [ + const PAINTERS: [(&str, IconPainter); 63] = [ ("arrow", draw_icon_arrow), ("blur", draw_icon_blur), ("board", draw_icon_board), @@ -205,7 +205,6 @@ mod painter_tests { ("session", draw_icon_session), ("settings", draw_icon_settings), ("shape_picker", draw_icon_shape_picker), - ("side_minimize", draw_icon_side_minimize), ("sliders", draw_icon_sliders), ("spotlight", draw_icon_spotlight), ("step_marker", draw_icon_step_marker), diff --git a/src/toolbar_icons/svg.rs b/src/toolbar_icons/svg.rs index 7a2ae058..4b109e8c 100644 --- a/src/toolbar_icons/svg.rs +++ b/src/toolbar_icons/svg.rs @@ -384,18 +384,6 @@ fn draw_minimize(ctx: &Context) { stroke(ctx); } -fn draw_side_minimize(ctx: &Context) { - ctx.move_to(5.0, 5.0); - ctx.line_to(8.0, 5.0); - ctx.line_to(8.0, 19.0); - ctx.line_to(5.0, 19.0); - stroke(ctx); - ctx.move_to(15.5, 8.5); - ctx.line_to(12.0, 12.0); - ctx.line_to(15.5, 15.5); - stroke(ctx); -} - fn draw_restore(ctx: &Context) { ctx.move_to(5.0, 5.0); ctx.line_to(5.0, 8.0); @@ -585,7 +573,6 @@ renderers!( (render_pin, draw_pin), (render_unpin, draw_unpin), (render_minimize, draw_minimize), - (render_side_minimize, draw_side_minimize), (render_restore, draw_restore), (render_close, draw_close), (render_rect, draw_rectangle), @@ -609,7 +596,7 @@ mod tests { type IconRender = fn(&Context, f64, f64, f64); const SIZES: [i32; 5] = [18, 20, 22, 24, 28]; - const ICONS: [(&str, IconRender); 34] = [ + const ICONS: [(&str, IconRender); 33] = [ ("drag", render_drag), ("select", render_select), ("pen", render_pen), @@ -630,7 +617,6 @@ mod tests { ("pin", render_pin), ("unpin", render_unpin), ("minimize", render_minimize), - ("side_minimize", render_side_minimize), ("restore", render_restore), ("close", render_close), ("rectangle", render_rect), diff --git a/src/ui/theme.rs b/src/ui/theme.rs index b7135339..dc244996 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -434,13 +434,12 @@ pub mod toolbar { // ---- Preset slot (M7-C2) ---- /// Tool-glyph size inside a presets-island slot, as a fraction of the - /// slot's shorter side (mirrors the side-palette preset icon ratio). + /// slot's shorter side. pub const PRESET_SLOT_ICON_RATIO: f64 = 0.5; /// Corner color-swatch size inside a filled presets-island slot, as a /// fraction of the slot's shorter side. The saved preset color rides here /// as a separate accent so the tool glyph can stay a neutral, always-legible - /// foreground rather than being tinted invisible by a dark preset color - /// (the side-palette convention, M7-C2 legibility fix). + /// foreground rather than being tinted invisible by a dark preset color. pub const PRESET_SLOT_SWATCH_RATIO: f64 = 0.4; /// Inset of the corner color swatch from the slot's bottom-right edge. pub const PRESET_SLOT_SWATCH_INSET: f64 = SPACING_XS; @@ -575,6 +574,8 @@ pub mod toolbar { pub const COLOR_FIELD_BORDER_HOVER: Rgba = rgba(FIELD_BORDER_RGB, 0.70); /// Scrollbar slider fill pub const COLOR_SCROLLBAR_SLIDER: Rgba = (1.0, 1.0, 1.0, 0.35); + /// Scrollbar track behind the slider + pub const COLOR_SCROLLBAR_TRACK: Rgba = (1.0, 1.0, 1.0, 0.08); /// Inner hairline that keeps a swatch fill defined against the panel /// (shared by the builtin renderer and the GTK swatch widget). pub const COLOR_SWATCH_HAIRLINE: Rgba = (1.0, 1.0, 1.0, 0.16); @@ -593,9 +594,8 @@ pub mod toolbar { /// Compact-mode counterpart of [`ISLAND_PAD`] (the last-resort /// tightened presentation), shared the same way. pub const COMPACT_ISLAND_PAD: f64 = 4.0; - /// Session/Settings popover content-column width, in spec units - /// (mirrors the retired side palette's content column). One source for - /// both frontends: the builtin popover tree builds its rows at this + /// Session/Settings popover content-column width, in spec units. One source + /// serves both frontends: the builtin popover tree builds its rows at this /// width and the GTK popover sizes its viewport from it. pub const MENU_CONTENT_W: f64 = 232.0; /// Canvas popover content-column width, in spec units. It is wider than diff --git a/src/ui/toolbar/AGENTS.md b/src/ui/toolbar/AGENTS.md index 217431c1..5a2e404b 100644 --- a/src/ui/toolbar/AGENTS.md +++ b/src/ui/toolbar/AGENTS.md @@ -5,16 +5,17 @@ - Runtime Wayland/Cairo toolbar rendering is governed by `src/backend/wayland/toolbar/AGENTS.md`. ## Architecture -- `model/` defines toolbar state, controls, activation/session/settings/tool models, and event policy. +- `model/` defines toolbar state, control IDs, sliders, session/settings/tool models, and event policy; activation payloads are plain toolbar events. - `snapshot/` and `snapshot.rs` build immutable snapshots consumed by runtime rendering and event handling. - `apply/` mutates `InputState` from toolbar events, page changes, board changes, action requests, delay/layout changes, and tool selections. - `bindings.rs` and `events.rs` connect toolbar models to application events without owning backend surface state. +- `session_format.rs` owns shared session-name normalization plus the built-in renderer's character-based truncation helpers. ## Invariants - Keep this layer distinct from backend Cairo/Wayland toolbar layout, surfaces, and rendering. - Snapshot data should be immutable after construction and should not perform durable state mutation. - Apply code should route mutations through `InputState` and preserve existing action/keybinding semantics. -- Keep model/event policy changes compatible with top-strip and side-palette runtime behavior. +- Keep model/event policy changes compatible with the unified top-toolbar runtime (layer-shell, inline, and GTK adapters). ## Coupled Changes - Toolbar model/snapshot/apply changes often require updates to config toolbar settings, backend toolbar rendering/layout, action metadata, input state, docs, and tests. diff --git a/src/ui/toolbar/apply/layout.rs b/src/ui/toolbar/apply/layout.rs index 452a7350..cb213cdb 100644 --- a/src/ui/toolbar/apply/layout.rs +++ b/src/ui/toolbar/apply/layout.rs @@ -1,6 +1,6 @@ use crate::config::{ToolbarItemId, ToolbarItemOrderGroup, ToolbarLayoutMode}; use crate::input::InputState; -use crate::ui::toolbar::{ToolbarItemCustomizeGroup, ToolbarSideSection}; +use crate::ui::toolbar::ToolbarItemCustomizeGroup; impl InputState { /// Close every open top-strip menu/popover (shapes picker, overflow, and @@ -72,6 +72,9 @@ impl InputState { self.toolbar_session_popover_open = false; self.toolbar_settings_popover_open = false; self.toolbar_canvas_popover_open = false; + self.toolbar_customize_items_open = false; + self.toolbar_customize_items_group = None; + self.toolbar_status_bar_contents_open = false; } self.needs_redraw = true; true @@ -97,15 +100,6 @@ impl InputState { true } - pub(super) fn apply_toolbar_set_side_minimized(&mut self, minimized: bool) -> bool { - if self.toolbar_side_minimized == minimized { - return false; - } - self.toolbar_side_minimized = minimized; - self.needs_redraw = true; - true - } - pub(super) fn apply_toolbar_pin_top_toolbar(&mut self, pin: bool) -> bool { if self.toolbar_top_pinned != pin { self.toolbar_top_pinned = pin; @@ -115,15 +109,6 @@ impl InputState { } } - pub(super) fn apply_toolbar_pin_side_toolbar(&mut self, pin: bool) -> bool { - if self.toolbar_side_pinned != pin { - self.toolbar_side_pinned = pin; - true - } else { - false - } - } - pub(super) fn apply_toolbar_toggle_icon_mode(&mut self, use_icons: bool) -> bool { if self.toolbar_use_icons != use_icons { self.toolbar_use_icons = use_icons; @@ -427,60 +412,6 @@ impl InputState { true } - pub(crate) fn apply_toolbar_set_side_pane( - &mut self, - pane: crate::ui::toolbar::SidePane, - ) -> bool { - let mut changed = false; - if self.toolbar_side_pane != pane { - self.toolbar_side_pane = pane; - changed = true; - } - // Leaving the Settings pane closes its nested sub-panels. - if pane != crate::ui::toolbar::SidePane::Settings - && (self.toolbar_customize_items_open - || self.toolbar_customize_items_group.is_some() - || self.toolbar_status_bar_contents_open) - { - self.toolbar_customize_items_open = false; - self.toolbar_customize_items_group = None; - self.toolbar_status_bar_contents_open = false; - changed = true; - } - if changed { - self.needs_redraw = true; - } - changed - } - - pub(super) fn apply_toolbar_scroll_side_pane(&mut self, offset: f64) -> bool { - let pane = self.toolbar_side_pane.index(); - let offset = offset.max(0.0); - if (self.toolbar_side_scroll[pane] - offset).abs() < 0.5 { - return false; - } - self.toolbar_side_scroll[pane] = offset; - self.needs_redraw = true; - true - } - - pub(super) fn apply_toolbar_toggle_side_section_collapsed( - &mut self, - section: ToolbarSideSection, - collapsed: bool, - ) -> bool { - let changed = if collapsed { - self.toolbar_collapsed_side_sections.insert(section) - } else { - self.toolbar_collapsed_side_sections.remove(§ion) - }; - if !changed { - return false; - } - self.needs_redraw = true; - true - } - /// Applies the layout preset restored from runtime-UI state. /// /// Restoring the mode must not re-run the preset's section defaults: the @@ -556,7 +487,6 @@ impl InputState { if !open { self.toolbar_customize_items_group = None; } - self.toolbar_side_pane = crate::ui::toolbar::SidePane::Settings; self.needs_redraw = true; true } @@ -571,7 +501,6 @@ impl InputState { self.toolbar_customize_items_open = true; self.toolbar_customize_items_group = group; self.toolbar_status_bar_contents_open = false; - self.toolbar_side_pane = crate::ui::toolbar::SidePane::Settings; self.needs_redraw = true; true } @@ -585,7 +514,6 @@ impl InputState { self.toolbar_customize_items_open = false; self.toolbar_customize_items_group = None; } - self.toolbar_side_pane = crate::ui::toolbar::SidePane::Settings; self.needs_redraw = true; true } @@ -659,46 +587,29 @@ mod tests { } #[test] - fn minimize_keeps_bars_visible_and_close_is_an_alias() { + fn minimize_keeps_the_strip_visible_and_close_is_an_alias() { let mut state = make_test_input_state(); - // The side-palette visibility assertion below needs the deprecated - // Panel escape hatch (the struct default is Pill, which retires the - // side surface). - state.init_toolbar_side_layout_from_config(crate::config::ToolbarSideLayout::Panel); - // The deprecated Close events now minimize: the surface stays + // The deprecated Close event now minimizes: the surface stays // visible as a restore tab instead of vanishing. state.apply_toolbar_event(ToolbarEvent::CloseTopToolbar); assert!(state.toolbar_top_minimized); assert!(state.toolbar_top_visible()); - state.apply_toolbar_event(ToolbarEvent::CloseSideToolbar); - assert!(state.toolbar_side_minimized); - assert!(state.toolbar_side_visible()); - state.apply_toolbar_event(ToolbarEvent::SetTopMinimized(false)); - state.apply_toolbar_event(ToolbarEvent::SetSideMinimized(false)); assert!(!state.toolbar_top_minimized); - assert!(!state.toolbar_side_minimized); } #[test] fn pin_application_defers_persistence_confirmation_to_backend() { let mut state = make_test_input_state(); let top_pinned = !state.toolbar_top_pinned; - let side_pinned = !state.toolbar_side_pinned; assert!(state.apply_toolbar_event(ToolbarEvent::PinTopToolbar(top_pinned))); assert!( state.ui_toast.is_none(), "input application does not yet know whether the runtime mutation is durable" ); - - assert!(state.apply_toolbar_event(ToolbarEvent::PinSideToolbar(side_pinned))); - assert!( - state.ui_toast.is_none(), - "the backend emits persistence-aware pin feedback after finishing the mutation" - ); } #[test] diff --git a/src/ui/toolbar/apply/mod.rs b/src/ui/toolbar/apply/mod.rs index a3de0df7..455fe9de 100644 --- a/src/ui/toolbar/apply/mod.rs +++ b/src/ui/toolbar/apply/mod.rs @@ -50,7 +50,6 @@ impl InputState { ToolbarEvent::SetColor(color) => self.apply_toolbar_set_color(color), ToolbarEvent::SetQuickColor { color, .. } => self.apply_toolbar_set_color(color), ToolbarEvent::EditQuickColor { index } => self.apply_toolbar_edit_quick_color(index), - ToolbarEvent::SetColorHsv { h, s, v } => self.apply_toolbar_set_color_hsv(h, s, v), ToolbarEvent::SetThickness(value) => self.apply_toolbar_set_thickness(value), ToolbarEvent::SetMarkerOpacity(value) => self.apply_toolbar_set_marker_opacity(value), ToolbarEvent::SetEraserMode(mode) => self.apply_toolbar_set_eraser_mode(mode), @@ -154,13 +153,8 @@ impl InputState { self.apply_toolbar_set_top_minimized(minimized) } ToolbarEvent::SetTopDisplayMode(mode) => self.apply_toolbar_set_top_display_mode(mode), - ToolbarEvent::SetSideMinimized(minimized) => { - self.apply_toolbar_set_side_minimized(minimized) - } ToolbarEvent::CloseTopToolbar => self.apply_toolbar_set_top_minimized(true), - ToolbarEvent::CloseSideToolbar => self.apply_toolbar_set_side_minimized(true), ToolbarEvent::PinTopToolbar(pin) => self.apply_toolbar_pin_top_toolbar(pin), - ToolbarEvent::PinSideToolbar(pin) => self.apply_toolbar_pin_side_toolbar(pin), ToolbarEvent::ToggleIconMode(use_icons) => { self.apply_toolbar_toggle_icon_mode(use_icons) } @@ -218,11 +212,6 @@ impl InputState { ToolbarEvent::ToggleFloatingBadgeAlways(show) => { self.apply_toolbar_toggle_floating_badge_always(show) } - ToolbarEvent::SetSidePane(pane) => self.apply_toolbar_set_side_pane(pane), - ToolbarEvent::ScrollSidePane(offset) => self.apply_toolbar_scroll_side_pane(offset), - ToolbarEvent::ToggleSideSectionCollapsed(section, collapsed) => { - self.apply_toolbar_toggle_side_section_collapsed(section, collapsed) - } ToolbarEvent::SetToolbarLayoutMode(mode) => self.apply_toolbar_set_layout_mode(mode), ToolbarEvent::SetToolbarItemHidden(id, hidden) => { self.apply_toolbar_set_item_hidden(id, hidden) @@ -263,7 +252,7 @@ impl InputState { | ToolbarEvent::SaveSessionAsCancel | ToolbarEvent::SessionInfo | ToolbarEvent::ClearSession => false, - ToolbarEvent::MoveTopToolbar { .. } | ToolbarEvent::MoveSideToolbar { .. } => false, + ToolbarEvent::MoveTopToolbar { .. } => false, } } } diff --git a/src/ui/toolbar/apply/tools.rs b/src/ui/toolbar/apply/tools.rs index a76e3330..4c6cc087 100644 --- a/src/ui/toolbar/apply/tools.rs +++ b/src/ui/toolbar/apply/tools.rs @@ -67,26 +67,6 @@ impl InputState { self.apply_color_from_ui(color) } - pub(super) fn apply_toolbar_set_color_hsv(&mut self, h: f64, s: f64, v: f64) -> bool { - // `hsv_to_rgb` is always opaque, so carry the live alpha across or the - // inline picker would silently flatten a translucent color every time - // the user nudged its hue or saturation. - let alpha = self.color_for_active_tool().a; - let color = Color { - a: alpha, - ..crate::draw::color::hsv_to_rgb(h, s, v) - }; - let changed = self.apply_color_from_ui(color); - // Remember the picker position even when the color collapses to a - // gray/black RGB value that cannot express hue or saturation. - if self.toolbar_picker_hsv != Some((h, s, v)) { - self.toolbar_picker_hsv = Some((h, s, v)); - self.needs_redraw = true; - return true; - } - changed - } - pub(super) fn apply_toolbar_set_thickness(&mut self, value: f64) -> bool { self.set_thickness_for_active_tool(value) } @@ -226,26 +206,6 @@ mod tests { use crate::input::state::test_support::make_test_input_state; use crate::ui::toolbar::ToolbarEvent; - #[test] - fn set_color_hsv_applies_color_and_remembers_picker_position() { - let mut state = make_test_input_state(); - - // A zero-saturation pick collapses to white in RGB; the remembered - // HSV triple is what keeps the picker's hue from snapping to red. - let changed = state.apply_toolbar_event(ToolbarEvent::SetColorHsv { - h: 0.4, - s: 0.0, - v: 1.0, - }); - - assert!(changed); - assert_eq!(state.toolbar_picker_hsv, Some((0.4, 0.0, 1.0))); - let color = state.current_color; - assert!((color.r - 1.0).abs() < 1e-9); - assert!((color.g - 1.0).abs() < 1e-9); - assert!((color.b - 1.0).abs() < 1e-9); - } - #[test] fn edit_hex_color_opens_popup_with_hex_focused() { let mut state = make_test_input_state(); diff --git a/src/ui/toolbar/events.rs b/src/ui/toolbar/events.rs index c9989c21..6e00766d 100644 --- a/src/ui/toolbar/events.rs +++ b/src/ui/toolbar/events.rs @@ -12,12 +12,10 @@ use super::ToolbarSnapshot; pub enum ToolbarItemCustomizeGroup { TopTools, TopControls, - SideSections, Actions, Pages, Boards, Presets, - ToolOptions, Sessions, } @@ -26,165 +24,15 @@ impl ToolbarItemCustomizeGroup { match self { Self::TopTools => "Top tools", Self::TopControls => "Top controls", - Self::SideSections => "Side sections", Self::Actions => "Actions", Self::Pages => "Pages", Self::Boards => "Boards", Self::Presets => "Presets", - Self::ToolOptions => "Tool options", Self::Sessions => "Sessions", } } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum ToolbarSideSection { - Colors, - Presets, - Thickness, - EraserMode, - PolygonSides, - ArrowLabels, - StepMarkers, - MarkerOpacity, - TextSize, - Font, - Actions, - Boards, - Pages, - StepUndo, - Session, - Settings, -} - -impl ToolbarSideSection { - pub const ALL: [Self; 16] = [ - Self::Colors, - Self::Presets, - Self::Thickness, - Self::EraserMode, - Self::PolygonSides, - Self::ArrowLabels, - Self::StepMarkers, - Self::MarkerOpacity, - Self::TextSize, - Self::Font, - Self::Actions, - Self::Boards, - Self::Pages, - Self::StepUndo, - Self::Session, - Self::Settings, - ]; - - /// Stable id used in config.toml (`ui.toolbar.collapsed_sections`). - pub fn config_id(self) -> &'static str { - match self { - Self::Colors => "colors", - Self::Presets => "presets", - Self::Thickness => "thickness", - Self::EraserMode => "eraser-mode", - Self::PolygonSides => "polygon-sides", - Self::ArrowLabels => "arrow-labels", - Self::StepMarkers => "step-markers", - Self::MarkerOpacity => "marker-opacity", - Self::TextSize => "text-size", - Self::Font => "font", - Self::Actions => "actions", - Self::Boards => "boards", - Self::Pages => "pages", - Self::StepUndo => "step-undo", - Self::Session => "session", - Self::Settings => "settings", - } - } - - pub fn from_config_id(value: &str) -> Option { - let value = value.trim().to_ascii_lowercase(); - Self::ALL - .into_iter() - .find(|section| section.config_id() == value) - } - - pub fn label(self) -> &'static str { - match self { - Self::Colors => "Colors", - Self::Presets => "Presets", - Self::Thickness => "Thickness", - Self::EraserMode => "Eraser mode", - Self::PolygonSides => "Sides", - Self::ArrowLabels => "Arrow labels", - Self::StepMarkers => "Step markers", - Self::MarkerOpacity => "Marker opacity", - Self::TextSize => "Text size", - Self::Font => "Font", - Self::Actions => "Actions", - Self::Boards => "Boards", - Self::Pages => "Pages", - Self::StepUndo => "Step Undo/Redo", - Self::Session => "Session", - Self::Settings => "Settings", - } - } -} - -/// The four fixed side-palette panes, selected by the nav row under the -/// header. Panes replace the old open/closed drawer with four tabs. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum SidePane { - /// Contextual drawing properties for the active tool. - #[default] - Draw, - /// Canvas management: history, zoom, boards, pages. - Canvas, - /// Session persistence. - Session, - /// The single customization surface. - Settings, -} - -impl SidePane { - pub const ALL: [Self; 4] = [Self::Draw, Self::Canvas, Self::Session, Self::Settings]; - - pub fn label(self) -> &'static str { - match self { - Self::Draw => "Draw", - Self::Canvas => "Canvas", - Self::Session => "Session", - Self::Settings => "Settings", - } - } - - /// Stable id used in config.toml (`ui.toolbar.side_active_pane`). - pub fn config_id(self) -> &'static str { - match self { - Self::Draw => "draw", - Self::Canvas => "canvas", - Self::Session => "session", - Self::Settings => "settings", - } - } - - pub fn from_config_id(value: &str) -> Option { - match value.trim().to_ascii_lowercase().as_str() { - "draw" => Some(Self::Draw), - "canvas" => Some(Self::Canvas), - "session" => Some(Self::Session), - "settings" => Some(Self::Settings), - _ => None, - } - } - - pub fn index(self) -> usize { - match self { - Self::Draw => 0, - Self::Canvas => 1, - Self::Session => 2, - Self::Settings => 3, - } - } -} - /// Value targeted by the style pill's precise-entry popup (opened from /// the pill's live numeral buttons). #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -230,17 +78,10 @@ pub enum ToolbarEvent { /// Recolor a quick-color slot: opens the color picker popup bound to that /// palette slot, so accepting it rewrites the swatch (and `config.toml`) /// instead of only the active tool's color. Emitted by secondary-clicking - /// a swatch in the style pill or the side palette. + /// a swatch in the style pill. EditQuickColor { index: usize, }, - /// Set the color from the side palette's HSV picker. Carries the full - /// triple so hue survives gray colors (where RGB alone loses it). - SetColorHsv { - h: f64, - s: f64, - v: f64, - }, SetThickness(f64), NudgeThickness(f64), SetMarkerOpacity(f64), @@ -356,19 +197,12 @@ pub enum ToolbarEvent { /// Set the top strip's display form (full strip / micro chip / hidden). /// The micro chip's click emits `SetTopDisplayMode(Full)`. SetTopDisplayMode(crate::config::TopDisplayMode), - /// Minimize the side palette to a small edge tab, or restore it. - SetSideMinimized(bool), /// Deprecated alias for `SetTopMinimized(true)`; kept so external /// callers and old code paths keep working. #[allow(dead_code)] CloseTopToolbar, - /// Deprecated alias for `SetSideMinimized(true)`. - #[allow(dead_code)] - CloseSideToolbar, /// Pin/unpin the top toolbar (saves to config) PinTopToolbar(bool), - /// Pin/unpin the side toolbar (saves to config) - PinSideToolbar(bool), /// Toggle between icon mode and text mode ToggleIconMode(bool), /// Toggle extended color palette @@ -439,13 +273,6 @@ pub enum ToolbarEvent { /// Toggle the board/page badge when the status bar is visible /// (renamed from TogglePageBadgeWithStatusBar for clarity) ToggleFloatingBadgeAlways(bool), - /// Switch the active side-palette pane - SetSidePane(SidePane), - /// Set the side-palette scroll offset for the active pane (absolute, - /// logical pixels; emitted by the scrollbar drag) - ScrollSidePane(f64), - /// Collapse/expand a section in the side drawer - ToggleSideSectionCollapsed(ToolbarSideSection, bool), /// Set toolbar layout mode SetToolbarLayoutMode(ToolbarLayoutMode), /// Hide or show a known toolbar item override. @@ -483,11 +310,6 @@ pub enum ToolbarEvent { x: f64, y: f64, }, - /// Drag handle for side toolbar (toolbar coords; screen coords when inline toolbars are active) - MoveSideToolbar { - x: f64, - y: f64, - }, } impl ToolbarEvent { @@ -547,47 +369,3 @@ pub(crate) fn action_for_save_preset(slot: usize) -> Option { pub(crate) fn action_for_clear_preset(slot: usize) -> Option { super::model::action_for_clear_preset(slot) } - -#[cfg(test)] -mod tests { - use super::{SidePane, ToolbarSideSection}; - - #[test] - fn side_pane_config_ids_round_trip() { - for pane in SidePane::ALL { - assert_eq!(SidePane::from_config_id(pane.config_id()), Some(pane)); - } - assert_eq!(SidePane::from_config_id(" Canvas "), Some(SidePane::Canvas)); - assert_eq!(SidePane::from_config_id("bogus"), None); - } - - #[test] - fn side_section_config_ids_round_trip() { - let sections = [ - ToolbarSideSection::Colors, - ToolbarSideSection::Presets, - ToolbarSideSection::Thickness, - ToolbarSideSection::EraserMode, - ToolbarSideSection::PolygonSides, - ToolbarSideSection::ArrowLabels, - ToolbarSideSection::StepMarkers, - ToolbarSideSection::MarkerOpacity, - ToolbarSideSection::TextSize, - ToolbarSideSection::Font, - ToolbarSideSection::Actions, - ToolbarSideSection::Boards, - ToolbarSideSection::Pages, - ToolbarSideSection::StepUndo, - ToolbarSideSection::Session, - ToolbarSideSection::Settings, - ]; - for section in sections { - assert_eq!( - ToolbarSideSection::from_config_id(section.config_id()), - Some(section), - "config id round trip for {section:?}" - ); - } - assert_eq!(ToolbarSideSection::from_config_id("bogus"), None); - } -} diff --git a/src/ui/toolbar/mod.rs b/src/ui/toolbar/mod.rs index c2342a5b..e2db8c6d 100644 --- a/src/ui/toolbar/mod.rs +++ b/src/ui/toolbar/mod.rs @@ -2,12 +2,11 @@ mod apply; pub(crate) mod bindings; mod events; pub(crate) mod model; +pub(crate) mod session_format; pub mod snapshot; pub use bindings::ToolbarBindingHints; -pub use events::{ - PrecisionEntryTarget, SidePane, ToolbarEvent, ToolbarItemCustomizeGroup, ToolbarSideSection, -}; +pub use events::{PrecisionEntryTarget, ToolbarEvent, ToolbarItemCustomizeGroup}; #[allow(unused_imports)] pub use snapshot::{ PresetFeedbackSnapshot, PresetSlotSnapshot, RuntimeUiPersistenceMode, diff --git a/src/ui/toolbar/model/actions.rs b/src/ui/toolbar/model/actions.rs index cf8b8699..f1010b8d 100644 --- a/src/ui/toolbar/model/actions.rs +++ b/src/ui/toolbar/model/actions.rs @@ -1,28 +1,6 @@ use crate::config::{ToolbarItemId, toolbar_item_ids as ids}; -use super::super::{ToolbarEvent, ToolbarSideSection, ToolbarSnapshot}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ToolbarCommandGroupKind { - History, - Zoom, - AdvancedActions, - Pages, - Boards, -} - -impl ToolbarCommandGroupKind { - /// Label drawn above the group inside the Canvas pane's Actions card; - /// Pages/Boards carry their own section headers instead. - pub(crate) fn sub_label(self) -> Option<&'static str> { - match self { - Self::History => Some("History"), - Self::Zoom => Some("Zoom"), - Self::AdvancedActions => Some("Advanced"), - Self::Pages | Self::Boards => None, - } - } -} +use super::super::{ToolbarEvent, ToolbarSnapshot}; #[derive(Debug, Clone)] pub(crate) struct ToolbarButtonModel { @@ -58,101 +36,12 @@ impl ToolbarButtonModel { #[derive(Debug, Clone)] pub(crate) struct ToolbarCommandGroup { - pub(crate) kind: ToolbarCommandGroupKind, pub(crate) buttons: Vec, } impl ToolbarCommandGroup { - fn new(kind: ToolbarCommandGroupKind, buttons: Vec) -> Self { - Self { kind, buttons } - } -} - -#[derive(Debug, Clone)] -pub(crate) struct ToolbarActionsModel { - groups: Vec, -} - -impl ToolbarActionsModel { - pub(crate) fn from_snapshot(snapshot: &ToolbarSnapshot) -> Option { - if snapshot.side_section_hidden(ToolbarSideSection::Actions) { - return None; - } - - if !canvas_pane_active(snapshot) { - return None; - } - let show_advanced = snapshot.show_actions_advanced; - let show_view_actions = snapshot.show_zoom_actions - && (snapshot.show_actions_section || snapshot.show_actions_advanced); - let show_actions = snapshot.show_actions_section || show_advanced; - - if !show_actions { - return None; - } - - let mut groups = Vec::with_capacity(3); - if snapshot.show_actions_section { - push_visible_group( - snapshot, - &mut groups, - ToolbarCommandGroupKind::History, - vec![ - ToolbarButtonModel::new(ToolbarEvent::Undo, snapshot.undo_available), - ToolbarButtonModel::new(ToolbarEvent::Redo, snapshot.redo_available), - ToolbarButtonModel::new(ToolbarEvent::ClearCanvas { instant: false }, true), - ], - ); - } - - if show_view_actions { - push_visible_group( - snapshot, - &mut groups, - ToolbarCommandGroupKind::Zoom, - vec![ - ToolbarButtonModel::new(ToolbarEvent::ZoomIn, true), - ToolbarButtonModel::new(ToolbarEvent::ZoomOut, true), - ToolbarButtonModel::new(ToolbarEvent::ResetZoom, snapshot.zoom_active), - ToolbarButtonModel::new(ToolbarEvent::ToggleZoomLock, snapshot.zoom_active), - ], - ); - } - - if show_advanced { - let mut buttons = Vec::with_capacity(5); - buttons.push(ToolbarButtonModel::new( - ToolbarEvent::UndoAll, - snapshot.undo_available, - )); - buttons.push(ToolbarButtonModel::new( - ToolbarEvent::RedoAll, - snapshot.redo_available, - )); - if snapshot.delay_actions_enabled { - buttons.push(ToolbarButtonModel::new( - ToolbarEvent::UndoAllDelayed, - snapshot.undo_available, - )); - buttons.push(ToolbarButtonModel::new( - ToolbarEvent::RedoAllDelayed, - snapshot.redo_available, - )); - } - buttons.push(ToolbarButtonModel::new(ToolbarEvent::ToggleFreeze, true)); - push_visible_group( - snapshot, - &mut groups, - ToolbarCommandGroupKind::AdvancedActions, - buttons, - ); - } - - (!groups.is_empty()).then_some(Self { groups }) - } - - pub(crate) fn groups(&self) -> &[ToolbarCommandGroup] { - &self.groups + fn new(buttons: Vec) -> Self { + Self { buttons } } } @@ -180,6 +69,14 @@ fn boards_buttons(snapshot: &ToolbarSnapshot) -> Vec { ] } +fn basic_action_buttons(snapshot: &ToolbarSnapshot) -> Vec { + vec![ + ToolbarButtonModel::new(ToolbarEvent::Undo, snapshot.undo_available), + ToolbarButtonModel::new(ToolbarEvent::Redo, snapshot.redo_available), + ToolbarButtonModel::new(ToolbarEvent::ClearCanvas { instant: false }, true), + ] +} + fn zoom_buttons(snapshot: &ToolbarSnapshot) -> Vec { vec![ ToolbarButtonModel::new(ToolbarEvent::ZoomIn, true), @@ -213,57 +110,36 @@ fn advanced_buttons(snapshot: &ToolbarSnapshot) -> Vec { buttons } -pub(crate) fn toolbar_pages_model(snapshot: &ToolbarSnapshot) -> Option { - if snapshot.side_section_hidden(ToolbarSideSection::Pages) - || !snapshot.show_pages_section - || !canvas_pane_active(snapshot) - { - return None; - } - - visible_group( - snapshot, - ToolbarCommandGroupKind::Pages, - pages_buttons(snapshot), - ) -} - -pub(crate) fn toolbar_boards_model(snapshot: &ToolbarSnapshot) -> Option { - if snapshot.side_section_hidden(ToolbarSideSection::Boards) - || !snapshot.show_boards_section - || !canvas_pane_active(snapshot) - { +/// Basic Undo/Redo/Clear group for the Canvas popover. The section-level +/// `side.group.actions` compatibility id gates the group; the buttons retain +/// their unified-top `top.utility.*` visibility ids. +pub(crate) fn toolbar_actions_model_for_popover( + snapshot: &ToolbarSnapshot, +) -> Option { + if !snapshot.show_actions_section { return None; } - - visible_group( - snapshot, - ToolbarCommandGroupKind::Boards, - boards_buttons(snapshot), - ) + visible_group(snapshot, basic_action_buttons(snapshot)) } /// Boards command group for the top strip's Canvas popover: gated on the -/// Boards display toggle and per-button hidden overrides only. Like the -/// Session/Settings popovers it ignores the side palette's pane selection, -/// so it renders under `side_layout = "pill"` where no Canvas pane exists. +/// Boards display toggle and per-button hidden overrides only. pub(crate) fn toolbar_boards_model_for_popover( snapshot: &ToolbarSnapshot, ) -> Option { if !snapshot.show_boards_section { return None; } - // The top strip has no side pane, so the Canvas popover is the only place - // the board picker (the full board grid) is reachable from the top bar. It + // The Canvas popover is the only top-bar route to the board picker. It // leads the row as the section's "browse all boards" entry, ahead of the - // linear nav/create buttons; the side palette keeps its own header trigger. + // linear nav/create buttons. let mut buttons = Vec::with_capacity(6); buttons.push(ToolbarButtonModel::new( ToolbarEvent::ToggleBoardPicker, true, )); buttons.extend(boards_buttons(snapshot)); - visible_group(snapshot, ToolbarCommandGroupKind::Boards, buttons) + visible_group(snapshot, buttons) } /// Pages command group for the Canvas popover; see @@ -274,26 +150,18 @@ pub(crate) fn toolbar_pages_model_for_popover( if !snapshot.show_pages_section { return None; } - visible_group( - snapshot, - ToolbarCommandGroupKind::Pages, - pages_buttons(snapshot), - ) + visible_group(snapshot, pages_buttons(snapshot)) } /// Zoom command group for the Canvas popover, gated on the Zoom display -/// toggle only (the popover splits it out from the side pane's Actions card). +/// toggle only. pub(crate) fn toolbar_zoom_group_for_popover( snapshot: &ToolbarSnapshot, ) -> Option { if !snapshot.show_zoom_actions { return None; } - visible_group( - snapshot, - ToolbarCommandGroupKind::Zoom, - zoom_buttons(snapshot), - ) + visible_group(snapshot, zoom_buttons(snapshot)) } /// Advanced-actions command group for the Canvas popover (undo-all, @@ -305,38 +173,18 @@ pub(crate) fn toolbar_advanced_group_for_popover( if !snapshot.show_actions_advanced { return None; } - visible_group( - snapshot, - ToolbarCommandGroupKind::AdvancedActions, - advanced_buttons(snapshot), - ) -} - -fn canvas_pane_active(snapshot: &ToolbarSnapshot) -> bool { - snapshot.active_side_pane == crate::ui::toolbar::SidePane::Canvas -} - -fn push_visible_group( - snapshot: &ToolbarSnapshot, - groups: &mut Vec, - kind: ToolbarCommandGroupKind, - buttons: Vec, -) { - if let Some(group) = visible_group(snapshot, kind, buttons) { - groups.push(group); - } + visible_group(snapshot, advanced_buttons(snapshot)) } fn visible_group( snapshot: &ToolbarSnapshot, - kind: ToolbarCommandGroupKind, buttons: Vec, ) -> Option { let buttons: Vec<_> = buttons .into_iter() .filter(|button| toolbar_button_visible(snapshot, &button.event)) .collect(); - (!buttons.is_empty()).then(|| ToolbarCommandGroup::new(kind, buttons)) + (!buttons.is_empty()).then(|| ToolbarCommandGroup::new(buttons)) } fn toolbar_button_visible(snapshot: &ToolbarSnapshot, event: &ToolbarEvent) -> bool { @@ -345,9 +193,9 @@ fn toolbar_button_visible(snapshot: &ToolbarSnapshot, event: &ToolbarEvent) -> b fn toolbar_button_item_id(event: &ToolbarEvent) -> Option { Some(match event { - ToolbarEvent::Undo => ids::SIDE_ACTIONS_UNDO, - ToolbarEvent::Redo => ids::SIDE_ACTIONS_REDO, - ToolbarEvent::ClearCanvas { .. } => ids::SIDE_ACTIONS_CLEAR_CANVAS, + ToolbarEvent::Undo => ids::TOP_UTILITY_UNDO, + ToolbarEvent::Redo => ids::TOP_UTILITY_REDO, + ToolbarEvent::ClearCanvas { .. } => ids::TOP_UTILITY_CLEAR_CANVAS, ToolbarEvent::ZoomIn => ids::SIDE_ACTIONS_ZOOM_IN, ToolbarEvent::ZoomOut => ids::SIDE_ACTIONS_ZOOM_OUT, ToolbarEvent::ResetZoom => ids::SIDE_ACTIONS_RESET_ZOOM, @@ -368,7 +216,56 @@ fn toolbar_button_item_id(event: &ToolbarEvent) -> Option { ToolbarEvent::BoardNew => ids::SIDE_BOARDS_NEW, ToolbarEvent::BoardDuplicate => ids::SIDE_BOARDS_DUPLICATE, ToolbarEvent::BoardDelete => ids::SIDE_BOARDS_DELETE, - ToolbarEvent::BoardRename => ids::SIDE_BOARDS_RENAME, _ => return None, }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_canvas_command_maps_to_its_retained_item_id() { + let cases = [ + (ToolbarEvent::Undo, ids::TOP_UTILITY_UNDO), + (ToolbarEvent::Redo, ids::TOP_UTILITY_REDO), + ( + ToolbarEvent::ClearCanvas { instant: false }, + ids::TOP_UTILITY_CLEAR_CANVAS, + ), + (ToolbarEvent::ZoomIn, ids::SIDE_ACTIONS_ZOOM_IN), + (ToolbarEvent::ZoomOut, ids::SIDE_ACTIONS_ZOOM_OUT), + (ToolbarEvent::ResetZoom, ids::SIDE_ACTIONS_RESET_ZOOM), + ( + ToolbarEvent::ToggleZoomLock, + ids::SIDE_ACTIONS_TOGGLE_ZOOM_LOCK, + ), + (ToolbarEvent::UndoAll, ids::SIDE_ACTIONS_UNDO_ALL), + (ToolbarEvent::RedoAll, ids::SIDE_ACTIONS_REDO_ALL), + ( + ToolbarEvent::UndoAllDelayed, + ids::SIDE_ACTIONS_UNDO_ALL_DELAYED, + ), + ( + ToolbarEvent::RedoAllDelayed, + ids::SIDE_ACTIONS_REDO_ALL_DELAYED, + ), + (ToolbarEvent::ToggleFreeze, ids::SIDE_ACTIONS_FREEZE), + (ToolbarEvent::PagePrev, ids::SIDE_PAGES_PREVIOUS), + (ToolbarEvent::PageNext, ids::SIDE_PAGES_NEXT), + (ToolbarEvent::PageNew, ids::SIDE_PAGES_NEW), + (ToolbarEvent::PageDuplicate, ids::SIDE_PAGES_DUPLICATE), + (ToolbarEvent::PageDelete, ids::SIDE_PAGES_DELETE), + (ToolbarEvent::ToggleBoardPicker, ids::SIDE_BOARDS_PICKER), + (ToolbarEvent::BoardPrev, ids::SIDE_BOARDS_PREVIOUS), + (ToolbarEvent::BoardNext, ids::SIDE_BOARDS_NEXT), + (ToolbarEvent::BoardNew, ids::SIDE_BOARDS_NEW), + (ToolbarEvent::BoardDuplicate, ids::SIDE_BOARDS_DUPLICATE), + (ToolbarEvent::BoardDelete, ids::SIDE_BOARDS_DELETE), + ]; + + for (event, expected) in cases { + assert_eq!(toolbar_button_item_id(&event), Some(expected), "{event:?}"); + } + } +} diff --git a/src/ui/toolbar/model/activation.rs b/src/ui/toolbar/model/activation.rs index 44c44bf8..467b1caa 100644 --- a/src/ui/toolbar/model/activation.rs +++ b/src/ui/toolbar/model/activation.rs @@ -1,30 +1,16 @@ -// The shared toolbar model is being adopted incrementally by both frontends, -// and this file holds staged shapes no renderer reads yet. Scoped here rather -// than over the whole `model` module so the other ~11.5k lines - the specs, -// snapshot mapping, and settings panes that are fully live - are policed for -// dead code again. -#![allow(dead_code)] - -use crate::draw::Color; use crate::input::state::{MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS}; use super::super::ToolbarEvent; +// Activation payloads are plain `ToolbarEvent` values on model controls. The +// historical module name now groups the IDs and slider math used to construct +// those event-bearing controls; it does not define an activation abstraction. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub(crate) enum ToolbarControlId { - DragTop, - DragSide, - IconModeIcons, - IconModeText, LayoutModeSimple, LayoutModeRegular, LayoutModeAdvanced, - PinTop, - PinSide, - CloseTop, - CloseSide, - DrawerMore, - BoardChip, SettingsContextAwareUi, SettingsIconMode, SettingsTextControls, @@ -70,37 +56,6 @@ pub(crate) enum ToolbarControlId { CancelRuntimeUiRecovery, } -#[derive(Debug, Clone)] -pub(crate) enum ToolbarActivation { - Click(ToolbarEvent), - Drag(ToolbarDragTarget), - Slider(ToolbarSlider), - ColorPicker(ToolbarColorPicker), - None, -} - -impl ToolbarActivation { - pub(crate) fn compatibility_event(&self) -> ToolbarEvent { - match self { - Self::Click(event) => event.clone(), - Self::Drag(ToolbarDragTarget::MoveTopToolbar) => { - ToolbarEvent::MoveTopToolbar { x: 0.0, y: 0.0 } - } - Self::Drag(ToolbarDragTarget::MoveSideToolbar) => { - ToolbarEvent::MoveSideToolbar { x: 0.0, y: 0.0 } - } - Self::Slider(slider) => slider.event_for_value(slider.value), - Self::ColorPicker(_) | Self::None => ToolbarEvent::OpenColorPickerPopup, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ToolbarDragTarget { - MoveTopToolbar, - MoveSideToolbar, -} - #[derive(Debug, Clone, PartialEq)] pub(crate) struct ToolbarSlider { pub(crate) target: ToolbarSliderTarget, @@ -199,6 +154,8 @@ impl ToolbarSliderSpec { self.value_from_t(Self::t_from_pointer_x(pointer_x, hit_x, hit_w)) } + /// Inset travel for the slider geometry contract exercised below. + #[cfg(test)] pub(crate) fn knob_center_x( self, track_x: f64, @@ -215,21 +172,11 @@ impl ToolbarSliderSpec { } } -/// Convert normalized delay slider position [0, 1] to seconds. -pub(crate) fn delay_secs_from_t(t: f64) -> f64 { - ToolbarSliderSpec::DELAY_SECONDS.value_from_t(t) -} - /// Convert a delay in milliseconds to normalized slider position [0, 1]. pub(crate) fn delay_t_from_ms(delay_ms: u64) -> f64 { ToolbarSliderSpec::DELAY_SECONDS.t_from_value(delay_ms as f64 / 1000.0) } -#[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) struct ToolbarColorPicker { - pub(crate) color: Color, -} - #[cfg(test)] mod tests { use super::*; @@ -300,12 +247,18 @@ mod tests { } #[test] - fn delay_helpers_use_delay_slider_spec() { - assert_close(delay_secs_from_t(0.0), ToolbarSliderSpec::DELAY_SECONDS.min); - assert_close(delay_secs_from_t(1.0), ToolbarSliderSpec::DELAY_SECONDS.max); + fn delay_helper_uses_delay_slider_spec() { + assert_close( + ToolbarSliderSpec::DELAY_SECONDS.value_from_t(0.0), + ToolbarSliderSpec::DELAY_SECONDS.min, + ); + assert_close( + ToolbarSliderSpec::DELAY_SECONDS.value_from_t(1.0), + ToolbarSliderSpec::DELAY_SECONDS.max, + ); let t = delay_t_from_ms(2525); - assert_close(delay_secs_from_t(t), 2.525); + assert_close(ToolbarSliderSpec::DELAY_SECONDS.value_from_t(t), 2.525); } #[test] diff --git a/src/ui/toolbar/model/control.rs b/src/ui/toolbar/model/control.rs index cc11d7dc..faef592e 100644 --- a/src/ui/toolbar/model/control.rs +++ b/src/ui/toolbar/model/control.rs @@ -11,7 +11,8 @@ use std::collections::HashSet; use crate::config::Action; use crate::draw::Color; -use super::activation::{ToolbarActivation, ToolbarControlId}; +use super::super::ToolbarEvent; +use super::activation::ToolbarControlId; #[derive(Debug, Clone)] pub(crate) struct ToolbarControl { @@ -30,7 +31,7 @@ pub(crate) enum ToolbarControlKind { #[derive(Debug, Clone)] pub(crate) struct ToolbarSingleControl { - pub(crate) activation: ToolbarActivation, + pub(crate) activation: ToolbarEvent, pub(crate) action: Option, } @@ -88,7 +89,7 @@ impl ToolbarSegmentedControl { pub(crate) struct ToolbarSegment { pub(crate) id: ToolbarControlId, pub(crate) label: Cow<'static, str>, - pub(crate) activation: ToolbarActivation, + pub(crate) activation: ToolbarEvent, pub(crate) action: Option, pub(crate) tooltip: ToolbarTooltip, pub(crate) enabled: bool, diff --git a/src/ui/toolbar/model/event_policy.rs b/src/ui/toolbar/model/event_policy.rs index 52fe2c25..d931ddd3 100644 --- a/src/ui/toolbar/model/event_policy.rs +++ b/src/ui/toolbar/model/event_policy.rs @@ -4,7 +4,7 @@ use crate::config::{ }; use crate::input::Tool; -use super::super::{ToolbarEvent, ToolbarSideSection}; +use super::super::ToolbarEvent; /// The popover an event belongs to, if any. /// @@ -32,7 +32,6 @@ pub(crate) struct ToolbarEventPolicy { /// The popovers this event operates inside, so it does not dismiss them. pub(crate) popovers: &'static [ToolbarPopover], pub(crate) backend_route: ToolbarBackendRoute, - pub(crate) pre_apply_effects: Vec, pub(crate) tablet_thickness_sensitive: bool, } @@ -42,7 +41,6 @@ impl ToolbarEventPolicy { persistence: persistence_for_event(event), popovers: popovers_for_event(event), backend_route: backend_route_for_event(event), - pre_apply_effects: pre_apply_effects_for_event(event), tablet_thickness_sensitive: matches!( event, ToolbarEvent::SetThickness(_) | ToolbarEvent::NudgeThickness(_) @@ -79,11 +77,7 @@ pub(crate) enum ToolbarPersistence { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ToolbarRuntimeUiPersistenceTarget { TopPinned, - SidePinned, TopMinimized, - SideMinimized, - SidePane, - CollapsedSection(ToolbarSideSection), NamedSection(crate::config::ToolbarSectionFlag), LayoutMode, ClickHighlight, @@ -126,12 +120,6 @@ pub(crate) enum ToolbarRuntimeUiPersistenceTarget { pub(crate) enum ToolbarBackendRoute { ApplyToInput, MoveTopToolbar, - MoveSideToolbar, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ToolbarPreApplyEffect { - RecordDrawerHintShown, } pub(crate) fn action_for_event(event: &ToolbarEvent) -> Option { @@ -387,8 +375,6 @@ fn persistence_for_event(event: &ToolbarEvent) -> ToolbarPersistence { use ToolbarRuntimeUiPersistenceTarget as Runtime; match event { ToolbarEvent::PinTopToolbar(_) => ToolbarPersistence::RuntimeUi(Runtime::TopPinned), - ToolbarEvent::PinSideToolbar(_) => ToolbarPersistence::RuntimeUi(Runtime::SidePinned), - ToolbarEvent::SetSidePane(_) => ToolbarPersistence::RuntimeUi(Runtime::SidePane), ToolbarEvent::SetTopMinimized(_) | ToolbarEvent::CloseTopToolbar => { ToolbarPersistence::RuntimeUi(Runtime::TopMinimized) } @@ -465,12 +451,6 @@ fn persistence_for_event(event: &ToolbarEvent) -> ToolbarPersistence { ToolbarEvent::SetStatusBarItemVisible(item, _) => { ToolbarPersistence::RuntimeUi(Runtime::StatusBarItem(*item)) } - ToolbarEvent::SetSideMinimized(_) | ToolbarEvent::CloseSideToolbar => { - ToolbarPersistence::RuntimeUi(Runtime::SideMinimized) - } - ToolbarEvent::ToggleSideSectionCollapsed(section, _) => { - ToolbarPersistence::RuntimeUi(Runtime::CollapsedSection(*section)) - } // Section rows were routed to `NamedSection` above; every other item // keeps its own visibility override. ToolbarEvent::SetToolbarItemHidden(id, hidden) => { @@ -509,7 +489,6 @@ fn persistence_for_event(event: &ToolbarEvent) -> ToolbarPersistence { // Opening the recolor popup persists nothing; accepting it writes the // palette through the popup's own pending-edit path. | ToolbarEvent::EditQuickColor { .. } - | ToolbarEvent::SetColorHsv { .. } | ToolbarEvent::SetThickness(_) | ToolbarEvent::NudgeThickness(_) | ToolbarEvent::SetMarkerOpacity(_) @@ -597,36 +576,22 @@ fn persistence_for_event(event: &ToolbarEvent) -> ToolbarPersistence { | ToolbarEvent::CancelPrecisionEntry | ToolbarEvent::AdjustSelectionProperty { .. } | ToolbarEvent::PickScreenColor - | ToolbarEvent::ScrollSidePane(_) | ToolbarEvent::DragToolbarItemOver { .. } | ToolbarEvent::SetToolbarItemCustomizationOpen(_) | ToolbarEvent::SetToolbarItemCustomizationGroup(_) | ToolbarEvent::SetStatusBarContentsOpen(_) | ToolbarEvent::ToggleShapePicker(_) - | ToolbarEvent::MoveTopToolbar { .. } - | ToolbarEvent::MoveSideToolbar { .. } => ToolbarPersistence::Ephemeral, + | ToolbarEvent::MoveTopToolbar { .. } => ToolbarPersistence::Ephemeral, } } fn backend_route_for_event(event: &ToolbarEvent) -> ToolbarBackendRoute { match event { ToolbarEvent::MoveTopToolbar { .. } => ToolbarBackendRoute::MoveTopToolbar, - ToolbarEvent::MoveSideToolbar { .. } => ToolbarBackendRoute::MoveSideToolbar, _ => ToolbarBackendRoute::ApplyToInput, } } -fn pre_apply_effects_for_event(event: &ToolbarEvent) -> Vec { - if matches!( - event, - ToolbarEvent::SetSidePane(pane) if *pane != crate::ui::toolbar::SidePane::Draw - ) { - vec![ToolbarPreApplyEffect::RecordDrawerHintShown] - } else { - Vec::new() - } -} - #[cfg(test)] mod tests { use super::*; @@ -677,7 +642,6 @@ mod popover_affinity_tests { for group in [ ToolbarItemOrderGroup::TopTools, ToolbarItemOrderGroup::TopControls, - ToolbarItemOrderGroup::SideSections, ] { let event = ToolbarEvent::StartToolbarItemDrag { group, diff --git a/src/ui/toolbar/model/header.rs b/src/ui/toolbar/model/header.rs index f3f2fccf..c65cdb15 100644 --- a/src/ui/toolbar/model/header.rs +++ b/src/ui/toolbar/model/header.rs @@ -2,101 +2,19 @@ use std::borrow::Cow; use crate::config::ToolbarLayoutMode; -use super::super::{ToolbarEvent, ToolbarSnapshot}; -use super::activation::{ToolbarActivation, ToolbarControlId, ToolbarDragTarget}; +use super::super::ToolbarEvent; +use super::activation::ToolbarControlId; use super::control::{ - ToolbarBoardChipPresentation, ToolbarControl, ToolbarControlKind, ToolbarControlPresentation, - ToolbarControlRole, ToolbarIcon, ToolbarPresentationPayload, ToolbarSegment, - ToolbarSegmentedControl, ToolbarSingleControl, ToolbarTooltip, + ToolbarControl, ToolbarControlKind, ToolbarControlPresentation, ToolbarControlRole, + ToolbarPresentationPayload, ToolbarSegment, ToolbarSegmentedControl, ToolbarTooltip, }; -#[derive(Debug, Clone)] -pub(crate) struct SideHeaderModel { - pub(crate) drag: ToolbarControl, - pub(crate) pin: ToolbarControl, - pub(crate) close: ToolbarControl, - pub(crate) board_chip: ToolbarControl, -} - -impl SideHeaderModel { - pub(crate) fn from_snapshot(snapshot: &ToolbarSnapshot) -> Self { - Self { - drag: drag_control( - ToolbarControlId::DragSide, - ToolbarDragTarget::MoveSideToolbar, - ), - pin: single_button( - ToolbarControlId::PinSide, - ToolbarEvent::PinSideToolbar(!snapshot.side_pinned), - if snapshot.side_pinned { - "Pinned (click to unpin)" - } else { - "Pin toolbar" - }, - snapshot.side_pinned, - ToolbarControlRole::Button, - ), - close: single_button( - ToolbarControlId::CloseSide, - ToolbarEvent::SetSideMinimized(true), - "Minimize (leaves a restore tab)", - false, - ToolbarControlRole::Button, - ), - board_chip: board_chip_control(snapshot), - } - } -} - -pub(crate) fn board_chip_label(snapshot: &ToolbarSnapshot) -> String { - let board_index = snapshot.board_index + 1; - let board_count = snapshot.board_count.max(1); - let name = snapshot.board_name.trim(); - let board_label = if board_count > 1 { - if name.is_empty() { - format!("Board {}/{}", board_index, board_count) - } else { - format!("Board {}/{} · {}", board_index, board_count, name) - } - } else if name.is_empty() { - "Board".to_string() - } else { - format!("Board · {}", name) - }; - let pages = snapshot.page_count.max(1); - if pages > 1 { - let page = (snapshot.page_index + 1).min(pages); - format!("{board_label} · p.{page}/{pages}") - } else { - board_label - } -} - -fn drag_control(id: ToolbarControlId, target: ToolbarDragTarget) -> ToolbarControl { - ToolbarControl { - id, - kind: ToolbarControlKind::Single(ToolbarSingleControl { - activation: ToolbarActivation::Drag(target), - action: None, - }), - enabled: true, - active: false, - presentation: ToolbarControlPresentation { - label: Cow::Borrowed("Drag toolbar"), - tooltip: ToolbarTooltip::text("Drag toolbar"), - icon: None, - role: ToolbarControlRole::DragHandle, - payload: ToolbarPresentationPayload::None, - }, - } -} - pub(crate) fn layout_mode_control(mode: ToolbarLayoutMode) -> ToolbarControl { let segment = |id, label: &'static str, target: ToolbarLayoutMode, tooltip: &'static str| { ToolbarSegment { id, label: Cow::Borrowed(label), - activation: ToolbarActivation::Click(ToolbarEvent::SetToolbarLayoutMode(target)), + activation: ToolbarEvent::SetToolbarLayoutMode(target), action: None, tooltip: ToolbarTooltip::text(tooltip), enabled: true, @@ -137,57 +55,6 @@ pub(crate) fn layout_mode_control(mode: ToolbarLayoutMode) -> ToolbarControl { ) } -fn board_chip_control(snapshot: &ToolbarSnapshot) -> ToolbarControl { - let label = board_chip_label(snapshot); - ToolbarControl { - id: ToolbarControlId::BoardChip, - kind: ToolbarControlKind::Single(ToolbarSingleControl { - activation: ToolbarActivation::Click(ToolbarEvent::ToggleBoardPicker), - action: crate::config::Action::BoardPicker.into(), - }), - enabled: true, - active: false, - presentation: ToolbarControlPresentation { - label: Cow::Owned(label.clone()), - tooltip: ToolbarTooltip::text("Board settings"), - icon: Some(ToolbarIcon::Board), - role: ToolbarControlRole::BoardChip, - payload: ToolbarPresentationPayload::BoardChip(ToolbarBoardChipPresentation { - label, - color: snapshot.board_color, - board_index: snapshot.board_index, - board_count: snapshot.board_count, - page_count: snapshot.page_count, - }), - }, - } -} - -fn single_button( - id: ToolbarControlId, - event: ToolbarEvent, - tooltip: &'static str, - active: bool, - role: ToolbarControlRole, -) -> ToolbarControl { - ToolbarControl { - id, - kind: ToolbarControlKind::Single(ToolbarSingleControl { - activation: ToolbarActivation::Click(event), - action: None, - }), - enabled: true, - active, - presentation: ToolbarControlPresentation { - label: Cow::Borrowed(tooltip), - tooltip: ToolbarTooltip::text(tooltip), - icon: None, - role, - payload: ToolbarPresentationPayload::None, - }, - } -} - fn segmented_control( id: ToolbarControlId, active: ToolbarControlId, diff --git a/src/ui/toolbar/model/mod.rs b/src/ui/toolbar/model/mod.rs index ae437e89..a8df1a37 100644 --- a/src/ui/toolbar/model/mod.rs +++ b/src/ui/toolbar/model/mod.rs @@ -11,14 +11,13 @@ mod top_spec; #[allow(unused_imports)] pub(crate) use actions::{ - ToolbarActionsModel, ToolbarButtonModel, ToolbarCommandGroup, ToolbarCommandGroupKind, - toolbar_advanced_group_for_popover, toolbar_boards_model, toolbar_boards_model_for_popover, - toolbar_pages_model, toolbar_pages_model_for_popover, toolbar_zoom_group_for_popover, + ToolbarButtonModel, ToolbarCommandGroup, toolbar_actions_model_for_popover, + toolbar_advanced_group_for_popover, toolbar_boards_model_for_popover, + toolbar_pages_model_for_popover, toolbar_zoom_group_for_popover, }; #[allow(unused_imports)] pub(crate) use activation::{ - ToolbarActivation, ToolbarColorPicker, ToolbarControlId, ToolbarDragTarget, ToolbarSlider, - ToolbarSliderSpec, ToolbarSliderTarget, delay_secs_from_t, delay_t_from_ms, + ToolbarControlId, ToolbarSlider, ToolbarSliderSpec, ToolbarSliderTarget, delay_t_from_ms, }; #[allow(unused_imports)] pub(crate) use control::{ @@ -29,12 +28,12 @@ pub(crate) use control::{ #[allow(unused_imports)] pub(crate) use event_policy::{ ToolbarBackendRoute, ToolbarEventPolicy, ToolbarPersistence, ToolbarPopover, - ToolbarPreApplyEffect, ToolbarRuntimeUiPersistenceTarget, action_for_apply_preset, - action_for_clear_preset, action_for_event, action_for_save_preset, action_for_tool, - popovers_for_event, short_label_for_event, tooltip_label_for_event, + ToolbarRuntimeUiPersistenceTarget, action_for_apply_preset, action_for_clear_preset, + action_for_event, action_for_save_preset, action_for_tool, popovers_for_event, + short_label_for_event, tooltip_label_for_event, }; #[allow(unused_imports)] -pub(crate) use header::{SideHeaderModel, board_chip_label, layout_mode_control}; +pub(crate) use header::layout_mode_control; #[allow(unused_imports)] pub(crate) use session::{ToolbarSessionButton, ToolbarSessionModel, ToolbarSessionRecent}; #[allow(unused_imports)] @@ -50,14 +49,13 @@ pub(crate) use style_pill::{ #[allow(unused_imports)] pub(crate) use tools::{ SemanticToolIcon, TopToolGroup, TopUtilityButton, current_shape_tool, default_drag_hint, - default_polygon_tool, default_shape_tool, is_fill_tool, is_polygon_tool, ordered_pane_sections, - ordered_side_sections, pane_for_section, polygon_tools, semantic_icon_for_tool, shape_tools, - tool_visible, toolbar_item_id_for_tool, toolbar_item_visible, top_clear_canvas_visible, - top_fill_visible, top_highlight_ring_visible, top_highlight_visible, top_screenshot_visible, - top_shape_picker_visible, top_sticky_note_visible, top_text_visible, top_tool_buttons, - top_tool_group, visible_shape_picker_max_row_len, visible_shape_picker_row_count, - visible_shape_picker_rows, visible_tool_count, visible_top_tool_buttons, - visible_top_utility_buttons, + default_polygon_tool, default_shape_tool, is_fill_tool, is_polygon_tool, polygon_tools, + semantic_icon_for_tool, shape_tools, tool_visible, toolbar_item_id_for_tool, + toolbar_item_visible, top_clear_canvas_visible, top_fill_visible, top_highlight_ring_visible, + top_highlight_visible, top_screenshot_visible, top_shape_picker_visible, + top_sticky_note_visible, top_text_visible, top_tool_buttons, top_tool_group, + visible_shape_picker_max_row_len, visible_shape_picker_row_count, visible_shape_picker_rows, + visible_tool_count, visible_top_tool_buttons, visible_top_utility_buttons, }; #[allow(unused_imports)] pub(crate) use top_spec::{ @@ -70,17 +68,18 @@ pub(crate) use top_spec::{ mod tests { use super::*; use crate::config::{ - ToolbarGroupId, ToolbarLayoutMode, toolbar_item_definitions, toolbar_item_ids as ids, + ToolbarGroupId, ToolbarItemOrderConfig, ToolbarItemsConfig, ToolbarLayoutMode, + toolbar_item_definitions, toolbar_item_ids as ids, }; + use crate::input::Tool; use crate::input::state::test_support::make_test_input_state; use crate::ui::toolbar::{ - RuntimeUiPersistenceMode, RuntimeUiPersistenceSnapshot, SidePane, ToolbarBindingHints, - ToolbarEvent, ToolbarSnapshot, + RuntimeUiPersistenceMode, RuntimeUiPersistenceSnapshot, ToolbarBindingHints, ToolbarEvent, + ToolbarSnapshot, }; fn snapshot() -> ToolbarSnapshot { let mut state = make_test_input_state(); - state.toolbar_side_pane = SidePane::Canvas; state.show_actions_section = true; state.show_actions_advanced = false; state.show_zoom_actions = true; @@ -89,43 +88,6 @@ mod tests { ToolbarSnapshot::from_input_with_bindings(&state, ToolbarBindingHints::default()) } - #[test] - fn actions_model_keeps_advanced_actions_in_canvas_pane() { - let mut snapshot = snapshot(); - snapshot.show_actions_section = false; - snapshot.show_actions_advanced = true; - snapshot.delay_actions_enabled = true; - - let model = ToolbarActionsModel::from_snapshot(&snapshot).expect("actions model"); - assert_eq!(model.groups().len(), 2); - assert_eq!(model.groups()[0].kind, ToolbarCommandGroupKind::Zoom); - assert_eq!( - model.groups()[1].kind, - ToolbarCommandGroupKind::AdvancedActions - ); - assert_eq!(model.groups()[1].buttons.len(), 5); - - snapshot.active_side_pane = SidePane::Draw; - assert!(ToolbarActionsModel::from_snapshot(&snapshot).is_none()); - } - - #[test] - fn page_and_board_models_report_disabled_navigation() { - let mut snapshot = snapshot(); - snapshot.page_count = 2; - snapshot.board_count = 1; - snapshot.is_transparent = true; - - let pages = toolbar_pages_model(&snapshot).expect("pages model"); - assert!(!pages.buttons[0].enabled); - assert!(pages.buttons[1].enabled); - - let boards = toolbar_boards_model(&snapshot).expect("boards model"); - assert!(!boards.buttons[0].enabled); - assert!(!boards.buttons[1].enabled); - assert!(!boards.buttons[3].enabled); - } - #[test] fn dynamic_toolbar_labels_live_with_the_event_model() { let mut snapshot = snapshot(); @@ -143,11 +105,11 @@ mod tests { #[test] fn segmented_controls_validate_invariants() { let segment = ToolbarSegment { - id: ToolbarControlId::IconModeIcons, - label: "Ico".into(), - activation: ToolbarActivation::Click(ToolbarEvent::ToggleIconMode(true)), + id: ToolbarControlId::LayoutModeSimple, + label: "Simple".into(), + activation: ToolbarEvent::SetToolbarLayoutMode(ToolbarLayoutMode::Simple), action: None, - tooltip: ToolbarTooltip::text("Icons mode"), + tooltip: ToolbarTooltip::text("Simple layout"), enabled: true, }; @@ -158,50 +120,31 @@ mod tests { assert_eq!( ToolbarSegmentedControl::try_new(None, vec![segment.clone(), segment.clone()]) .unwrap_err(), - ToolbarModelError::DuplicateSegmentId(ToolbarControlId::IconModeIcons) + ToolbarModelError::DuplicateSegmentId(ToolbarControlId::LayoutModeSimple) ); assert_eq!( ToolbarSegmentedControl::try_new( - Some(ToolbarControlId::IconModeText), + Some(ToolbarControlId::LayoutModeRegular), vec![segment.clone()] ) .unwrap_err(), - ToolbarModelError::MissingActiveSegment(ToolbarControlId::IconModeText) + ToolbarModelError::MissingActiveSegment(ToolbarControlId::LayoutModeRegular) ); assert!( - ToolbarSegmentedControl::try_new(Some(ToolbarControlId::IconModeIcons), vec![segment]) - .is_ok() + ToolbarSegmentedControl::try_new( + Some(ToolbarControlId::LayoutModeSimple), + vec![segment] + ) + .is_ok() ); } - #[test] - fn side_header_model_contains_dynamic_board_chip() { - let mut snapshot = snapshot(); - snapshot.board_index = 1; - snapshot.board_count = 3; - snapshot.board_name = "Sprint".to_string(); - snapshot.page_count = 2; - - let header = SideHeaderModel::from_snapshot(&snapshot); - let ToolbarPresentationPayload::BoardChip(chip) = &header.board_chip.presentation.payload - else { - panic!("board chip payload"); - }; - - assert_eq!(chip.label, "Board 2/3 · Sprint · p.1/2"); - assert_eq!(chip.board_index, 1); - assert_eq!(chip.board_count, 3); - assert_eq!(chip.page_count, 2); - } - #[test] fn settings_model_includes_context_ui_and_simple_mode_hides_advanced_toggles() { let mut snapshot = snapshot(); - snapshot.active_side_pane = SidePane::Settings; snapshot.layout_mode = ToolbarLayoutMode::Simple; - snapshot.show_settings_section = true; - let model = ToolbarSettingsModel::from_snapshot(&snapshot).expect("settings"); + let model = ToolbarSettingsModel::for_popover(&snapshot).expect("settings"); assert_eq!( model.toggles()[0].id, ToolbarControlId::SettingsContextAwareUi @@ -214,26 +157,21 @@ mod tests { ); snapshot.layout_mode = ToolbarLayoutMode::Regular; - let model = ToolbarSettingsModel::from_snapshot(&snapshot).expect("settings"); + let model = ToolbarSettingsModel::for_popover(&snapshot).expect("settings"); assert!( model .toggles() .iter() .any(|toggle| toggle.id == ToolbarControlId::SettingsAdvancedActions) ); - - snapshot.active_side_pane = SidePane::Draw; - assert!(ToolbarSettingsModel::from_snapshot(&snapshot).is_none()); } #[test] fn long_settings_toggles_take_a_full_row() { let mut snapshot = snapshot(); - snapshot.active_side_pane = SidePane::Settings; snapshot.layout_mode = ToolbarLayoutMode::Regular; - snapshot.show_settings_section = true; - let model = ToolbarSettingsModel::from_snapshot(&snapshot).expect("settings"); + let model = ToolbarSettingsModel::for_popover(&snapshot).expect("settings"); let rows = model.toggle_rows(); assert_eq!( rows.iter().map(|row| row.len()).sum::(), @@ -257,8 +195,6 @@ mod tests { #[test] fn settings_model_moves_hidden_item_overrides_into_customization_panel() { let mut snapshot = snapshot(); - snapshot.active_side_pane = SidePane::Settings; - snapshot.show_settings_section = true; snapshot.resolved_toolbar_items = crate::config::ToolbarItemsConfig { hidden: vec![ids::TOP_TOOL_PEN.as_str().to_string()], shown: Vec::new(), @@ -266,7 +202,7 @@ mod tests { } .resolved(); - let model = ToolbarSettingsModel::from_snapshot(&snapshot).expect("settings"); + let model = ToolbarSettingsModel::for_popover(&snapshot).expect("settings"); assert!(model.item_overrides().is_empty()); assert!(model.buttons().iter().any(|button| { matches!( @@ -282,7 +218,7 @@ mod tests { ); snapshot.customize_items_open = true; - let model = ToolbarSettingsModel::from_snapshot(&snapshot).expect("settings"); + let model = ToolbarSettingsModel::for_popover(&snapshot).expect("settings"); assert!(model.item_overrides().is_empty()); assert!( model @@ -293,7 +229,7 @@ mod tests { snapshot.customize_items_group = Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::TopTools); - let model = ToolbarSettingsModel::from_snapshot(&snapshot).expect("settings"); + let model = ToolbarSettingsModel::for_popover(&snapshot).expect("settings"); assert!(model.groups().is_empty()); assert!( model @@ -323,8 +259,6 @@ mod tests { #[test] fn factory_visibility_reset_button_tracks_only_eligible_tri_state_differences() { - use crate::config::ToolbarItemsConfig; - let mut snapshot = snapshot(); snapshot.resolved_toolbar_items = ToolbarItemsConfig::default().resolved(); assert!(snapshot.toolbar_item_hidden(ids::TOP_UTILITY_SCREENSHOT)); @@ -352,7 +286,7 @@ mod tests { let mut chrome_only = ToolbarItemsConfig::default(); chrome_only.set_hidden(ids::TOP_CHROME_OVERFLOW, true); - chrome_only.set_hidden(ids::SIDE_GROUP_SETTINGS, true); + chrome_only.set_hidden(ids::SIDE_SETTINGS_ABOUT, true); snapshot.resolved_toolbar_items = chrome_only.resolved(); assert!(!has_visibility_reset_button(&snapshot)); @@ -367,6 +301,37 @@ mod tests { assert!(has_visibility_reset_button(&snapshot)); } + #[test] + fn active_order_groups_control_shared_top_models() { + let mut snapshot = snapshot(); + snapshot.resolved_toolbar_items = ToolbarItemsConfig { + hidden: Vec::new(), + shown: Vec::new(), + order: ToolbarItemOrderConfig { + top_tools: vec![ + ids::TOP_TOOL_MARKER.as_str().to_string(), + ids::TOP_TOOL_PEN.as_str().to_string(), + ], + top_controls: vec![ + ids::TOP_UTILITY_CLEAR_CANVAS.as_str().to_string(), + ids::TOP_UTILITY_TEXT.as_str().to_string(), + ], + }, + } + .resolved(); + + assert_eq!( + visible_top_tool_buttons(false, &snapshot) + .take(2) + .collect::>(), + vec![Tool::Marker, Tool::Pen] + ); + assert_eq!( + &visible_top_utility_buttons(&snapshot, false, true)[..2], + &[TopUtilityButton::ClearCanvas, TopUtilityButton::Text] + ); + } + #[test] fn factory_visibility_reset_button_uses_unambiguous_wording() { let mut snapshot = snapshot(); @@ -511,7 +476,7 @@ mod tests { } #[test] - fn event_policy_classifies_persistence_and_pre_apply_effects() { + fn event_policy_classifies_persistence() { // Chrome the user arranges from the overlay survives a restart as a // runtime override. assert_eq!( @@ -527,38 +492,10 @@ mod tests { ToolbarEventPolicy::for_event(&ToolbarEvent::SetThickness(2.0)).persistence, ToolbarPersistence::Ephemeral ); - assert_eq!( - ToolbarEventPolicy::for_event(&ToolbarEvent::SetSidePane(SidePane::Canvas)).persistence, - ToolbarPersistence::RuntimeUi(ToolbarRuntimeUiPersistenceTarget::SidePane) - ); assert_eq!( ToolbarEventPolicy::for_event(&ToolbarEvent::RetryRuntimeUiPersistence).persistence, ToolbarPersistence::Ephemeral ); - assert_eq!( - ToolbarEventPolicy::for_event(&ToolbarEvent::ToggleSideSectionCollapsed( - crate::ui::toolbar::ToolbarSideSection::Colors, - true, - )) - .persistence, - ToolbarPersistence::RuntimeUi(ToolbarRuntimeUiPersistenceTarget::CollapsedSection( - crate::ui::toolbar::ToolbarSideSection::Colors, - )) - ); - assert_eq!( - ToolbarEventPolicy::for_event(&ToolbarEvent::ScrollSidePane(12.0)).persistence, - ToolbarPersistence::Ephemeral - ); - assert_eq!( - ToolbarEventPolicy::for_event(&ToolbarEvent::SetSidePane(SidePane::Canvas)) - .pre_apply_effects, - vec![ToolbarPreApplyEffect::RecordDrawerHintShown] - ); - assert!( - ToolbarEventPolicy::for_event(&ToolbarEvent::SetSidePane(SidePane::Draw)) - .pre_apply_effects - .is_empty() - ); } #[test] @@ -577,17 +514,6 @@ mod tests { assert_eq!(picker.short_label(&snapshot, "Board"), "Picker"); assert_eq!(picker.tooltip_label(&snapshot, "Board"), "Board Picker"); - // The side-palette boards group keeps its legacy header trigger and is - // not given the picker button. - let side = toolbar_boards_model(&snapshot).expect("side boards group"); - assert!( - !side - .buttons - .iter() - .any(|button| button.event == ToolbarEvent::ToggleBoardPicker), - "the side palette boards row is unchanged" - ); - // Hiding side.boards.picker drops the button (config plumbing), while // the rest of the boards row survives. snapshot.resolved_toolbar_items = crate::config::ToolbarItemsConfig { @@ -613,4 +539,151 @@ mod tests { "hiding the picker leaves the rest of the boards row intact" ); } + + #[test] + fn basic_actions_are_canvas_popover_controls_with_top_item_ids() { + let mut snapshot = snapshot(); + snapshot.show_actions_section = true; + snapshot.undo_available = true; + snapshot.redo_available = true; + + let events = toolbar_actions_model_for_popover(&snapshot) + .expect("actions") + .buttons + .into_iter() + .map(|button| button.event) + .collect::>(); + assert_eq!( + events, + [ + ToolbarEvent::Undo, + ToolbarEvent::Redo, + ToolbarEvent::ClearCanvas { instant: false }, + ] + ); + + snapshot + .resolved_toolbar_items + .hidden + .insert(ids::TOP_UTILITY_UNDO); + let hidden = toolbar_actions_model_for_popover(&snapshot).expect("remaining actions"); + assert!( + !hidden + .buttons + .iter() + .any(|button| button.event == ToolbarEvent::Undo) + ); + assert!( + hidden + .buttons + .iter() + .any(|button| button.event == ToolbarEvent::Redo) + ); + + snapshot.show_actions_section = false; + assert!(toolbar_actions_model_for_popover(&snapshot).is_none()); + } + + #[test] + fn retained_side_ids_still_control_unified_top_models() { + fn hide(snapshot: &mut ToolbarSnapshot, id: crate::config::ToolbarItemId) { + snapshot.resolved_toolbar_items = crate::config::ToolbarItemsConfig { + hidden: vec![id.as_str().to_string()], + shown: Vec::new(), + order: crate::config::ToolbarItemOrderConfig::default(), + } + .resolved(); + } + + let mut snapshot = snapshot(); + snapshot.show_actions_advanced = true; + snapshot.delay_actions_enabled = true; + + assert!( + toolbar_zoom_group_for_popover(&snapshot) + .expect("zoom") + .buttons + .iter() + .any(|button| button.event == ToolbarEvent::ZoomIn) + ); + hide(&mut snapshot, ids::SIDE_ACTIONS_ZOOM_IN); + assert!( + !toolbar_zoom_group_for_popover(&snapshot) + .expect("zoom") + .buttons + .iter() + .any(|button| button.event == ToolbarEvent::ZoomIn), + "hiding side.actions.zoom-in removes the Canvas zoom-in control" + ); + + snapshot = self::snapshot(); + assert!( + toolbar_pages_model_for_popover(&snapshot) + .expect("pages") + .buttons + .iter() + .any(|button| button.event == ToolbarEvent::PageDelete) + ); + hide(&mut snapshot, ids::SIDE_PAGES_DELETE); + assert!( + !toolbar_pages_model_for_popover(&snapshot) + .expect("pages") + .buttons + .iter() + .any(|button| button.event == ToolbarEvent::PageDelete), + "hiding side.pages.delete removes the Canvas page-delete control" + ); + + snapshot = self::snapshot(); + snapshot.show_actions_advanced = true; + assert!( + toolbar_advanced_group_for_popover(&snapshot) + .expect("advanced") + .buttons + .iter() + .any(|button| button.event == ToolbarEvent::ToggleFreeze) + ); + hide(&mut snapshot, ids::SIDE_ACTIONS_FREEZE); + assert!( + !toolbar_advanced_group_for_popover(&snapshot) + .expect("advanced") + .buttons + .iter() + .any(|button| button.event == ToolbarEvent::ToggleFreeze), + "hiding side.actions.freeze removes the Canvas freeze control" + ); + + snapshot = self::snapshot(); + assert!( + ToolbarSessionModel::for_popover(&snapshot) + .expect("session") + .buttons + .iter() + .any(|button| button.event == ToolbarEvent::SessionInfo) + ); + hide(&mut snapshot, ids::SIDE_SESSION_INFO); + assert!( + !ToolbarSessionModel::for_popover(&snapshot) + .expect("session") + .buttons + .iter() + .any(|button| button.event == ToolbarEvent::SessionInfo), + "hiding side.session.info removes the Session Info control" + ); + + snapshot = self::snapshot(); + let about_visible = |snapshot: &ToolbarSnapshot| { + ToolbarSettingsModel::for_popover(snapshot) + .expect("settings") + .buttons() + .iter() + .any(|button| matches!(button.event, ToolbarEvent::OpenAbout)) + }; + assert!(about_visible(&snapshot)); + hide(&mut snapshot, ids::SIDE_SETTINGS_ABOUT); + assert!( + !about_visible(&snapshot), + "hiding side.settings.about removes the Settings About control" + ); + } } diff --git a/src/ui/toolbar/model/session.rs b/src/ui/toolbar/model/session.rs index 2435bf83..24631ebb 100644 --- a/src/ui/toolbar/model/session.rs +++ b/src/ui/toolbar/model/session.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use crate::config::{ToolbarItemId, toolbar_item_ids as ids}; -use super::super::{SessionRecentSnapshot, ToolbarEvent, ToolbarSideSection, ToolbarSnapshot}; +use super::super::{SessionRecentSnapshot, ToolbarEvent, ToolbarSnapshot}; const MAX_RECENT_SESSIONS: usize = 5; pub(crate) const SESSION_BUTTON_COLUMNS: usize = 3; @@ -17,20 +17,8 @@ pub(crate) struct ToolbarSessionModel { } impl ToolbarSessionModel { - pub(crate) fn from_snapshot(snapshot: &ToolbarSnapshot) -> Option { - if snapshot.side_section_hidden(ToolbarSideSection::Session) - || snapshot.active_side_pane != crate::ui::toolbar::SidePane::Session - { - return None; - } - Self::build(snapshot) - } - - /// The same model for the top strip's Session popover. The popover is - /// navigation like the Settings pane — it ignores the side palette's - /// pane selection and the hideable `side.group.session` section flag - /// (under `side_layout = "pill"` it is the only Session surface) while - /// keeping the per-button hidden overrides. + /// The model for the top strip's unconditionally reachable Session + /// popover. Historical per-button hidden overrides remain active. pub(crate) fn for_popover(snapshot: &ToolbarSnapshot) -> Option { Self::build(snapshot) } @@ -83,17 +71,9 @@ impl ToolbarSessionModel { }) } - pub(crate) fn has_recent_sessions(&self) -> bool { - !self.recents.is_empty() - } - pub(crate) fn button_columns(&self) -> usize { self.buttons.len().clamp(1, SESSION_BUTTON_COLUMNS) } - - pub(crate) fn button_rows(&self) -> usize { - self.buttons.len().div_ceil(self.button_columns()) - } } #[derive(Debug, Clone)] @@ -174,14 +154,26 @@ fn session_button_item_id(event: &ToolbarEvent) -> Option { mod tests { use super::*; use crate::input::state::test_support::make_test_input_state; - use crate::ui::toolbar::{SidePane, ToolbarBindingHints}; + use crate::ui::toolbar::ToolbarBindingHints; fn app_snapshot() -> ToolbarSnapshot { - let mut state = make_test_input_state(); - state.toolbar_side_pane = SidePane::Session; + let state = make_test_input_state(); ToolbarSnapshot::from_input_with_bindings(&state, ToolbarBindingHints::default()) } + #[test] + fn every_session_command_maps_to_its_retained_item_id() { + for (event, expected) in [ + (ToolbarEvent::OpenSession, ids::SIDE_SESSION_OPEN), + (ToolbarEvent::SaveSessionAs, ids::SIDE_SESSION_SAVE_AS), + (ToolbarEvent::SessionInfo, ids::SIDE_SESSION_INFO), + (ToolbarEvent::ClearSession, ids::SIDE_SESSION_CLEAR), + (ToolbarEvent::OpenConfigurator, ids::SIDE_SESSION_MANAGER), + ] { + assert_eq!(session_button_item_id(&event), Some(expected), "{event:?}"); + } + } + #[test] fn session_model_shows_active_target_controls_and_caps_recents() { let mut snapshot = app_snapshot(); @@ -194,13 +186,12 @@ mod tests { }) .collect(); - let model = ToolbarSessionModel::from_snapshot(&snapshot).expect("session model"); + let model = ToolbarSessionModel::for_popover(&snapshot).expect("session model"); assert_eq!(model.active_name, "lecture.wayscriber-session"); assert!(model.active_path_label.contains("/tmp/lecture")); assert_eq!(model.buttons.len(), 5); assert_eq!(model.button_columns(), SESSION_BUTTON_COLUMNS); - assert_eq!(model.button_rows(), 2); assert!(model.buttons.iter().all(|button| button.enabled)); assert_eq!(model.recents.len(), MAX_RECENT_SESSIONS); assert!(model.overwrite_confirmation.is_none()); @@ -214,7 +205,7 @@ mod tests { path: PathBuf::from("/tmp/recent.wayscriber-session"), }]; - let model = ToolbarSessionModel::from_snapshot(&snapshot).expect("session model"); + let model = ToolbarSessionModel::for_popover(&snapshot).expect("session model"); assert!(!model.buttons[0].enabled); assert!(!model.buttons[1].enabled); @@ -236,7 +227,7 @@ mod tests { snapshot.active_session_path = Some(PathBuf::from("/tmp/current.wayscriber-session")); snapshot.pending_save_as_overwrite_path = Some(target.clone()); - let model = ToolbarSessionModel::from_snapshot(&snapshot).expect("session model"); + let model = ToolbarSessionModel::for_popover(&snapshot).expect("session model"); let confirmation = model .overwrite_confirmation .as_ref() @@ -252,14 +243,4 @@ mod tests { ToolbarEvent::SaveSessionAsCancel )); } - - #[test] - fn session_model_is_hidden_outside_session_pane() { - let mut snapshot = app_snapshot(); - snapshot.active_side_pane = SidePane::Canvas; - assert!(ToolbarSessionModel::from_snapshot(&snapshot).is_none()); - - snapshot.active_side_pane = SidePane::Draw; - assert!(ToolbarSessionModel::from_snapshot(&snapshot).is_none()); - } } diff --git a/src/ui/toolbar/model/settings.rs b/src/ui/toolbar/model/settings.rs index 452ad6a7..9a9e3f94 100644 --- a/src/ui/toolbar/model/settings.rs +++ b/src/ui/toolbar/model/settings.rs @@ -9,7 +9,7 @@ use crate::config::{ }; use super::super::{ToolbarEvent, ToolbarItemCustomizeGroup, ToolbarSnapshot}; -use super::activation::{ToolbarActivation, ToolbarControlId}; +use super::activation::ToolbarControlId; use super::control::{ToolbarIcon, ToolbarTooltip}; mod helpers; @@ -29,18 +29,9 @@ pub(crate) struct ToolbarSettingsModel { } impl ToolbarSettingsModel { - pub(crate) fn from_snapshot(snapshot: &ToolbarSnapshot) -> Option { - // The Settings pane is navigation, not a hideable section: it is the - // single customization surface, so it must always be reachable. - if snapshot.active_side_pane != crate::ui::toolbar::SidePane::Settings { - return None; - } - Self::build(snapshot) - } - - /// The same model for the top strip's Settings popover, which ignores - /// the side palette's pane selection (under `side_layout = "pill"` the - /// popover is the only Settings surface). + /// The model for the top strip's Settings popover. Settings is + /// navigation, not a hideable section: it is the single customization + /// surface, so it must always be reachable. pub(crate) fn for_popover(snapshot: &ToolbarSnapshot) -> Option { Self::build(snapshot) } @@ -368,7 +359,7 @@ pub(crate) struct ToolbarSettingsItemOverride { pub(crate) id: ToolbarItemId, pub(crate) label: Cow<'static, str>, pub(crate) shown: bool, - pub(crate) activation: ToolbarActivation, + pub(crate) activation: ToolbarEvent, pub(crate) tooltip: ToolbarTooltip, pub(crate) order: Option, } @@ -397,23 +388,23 @@ impl ToolbarSettingsItemOverride { index, can_move_up: index > 0, can_move_down: index + 1 < len, - move_up: ToolbarActivation::Click(ToolbarEvent::MoveToolbarItem { + move_up: ToolbarEvent::MoveToolbarItem { group: order_group, id, delta: -1, - }), - move_down: ToolbarActivation::Click(ToolbarEvent::MoveToolbarItem { + }, + move_down: ToolbarEvent::MoveToolbarItem { group: order_group, id, delta: 1, - }), + }, }) }); Self { id, label: Cow::Borrowed(definition.label), shown: !hidden, - activation: ToolbarActivation::Click(ToolbarEvent::SetToolbarItemHidden(id, !hidden)), + activation: ToolbarEvent::SetToolbarItemHidden(id, !hidden), tooltip: ToolbarTooltip::text(format!("{}: uncheck to hide", definition.label)), order, } @@ -426,8 +417,8 @@ pub(crate) struct ToolbarSettingsItemOrder { pub(crate) index: usize, pub(crate) can_move_up: bool, pub(crate) can_move_down: bool, - pub(crate) move_up: ToolbarActivation, - pub(crate) move_down: ToolbarActivation, + pub(crate) move_up: ToolbarEvent, + pub(crate) move_down: ToolbarEvent, } #[derive(Debug, Clone)] @@ -435,7 +426,7 @@ pub(crate) struct ToolbarSettingsToggle { pub(crate) id: ToolbarControlId, pub(crate) label: Cow<'static, str>, pub(crate) checked: bool, - pub(crate) activation: ToolbarActivation, + pub(crate) activation: ToolbarEvent, pub(crate) tooltip: ToolbarTooltip, /// Label too long for a half-width cell: the toggle takes a full row. pub(crate) wide: bool, @@ -453,7 +444,7 @@ impl ToolbarSettingsToggle { id, label: Cow::Borrowed(label), checked, - activation: ToolbarActivation::Click(event), + activation: event, tooltip: ToolbarTooltip::text(tooltip), wide: false, } @@ -470,6 +461,8 @@ pub(crate) struct ToolbarSettingsButton { pub(crate) id: ToolbarControlId, pub(crate) label: Cow<'static, str>, pub(crate) event: ToolbarEvent, + /// Consumed by the GTK settings popover; the built-in popover is text-only. + #[cfg_attr(not(feature = "toolbar-gtk"), allow(dead_code))] pub(crate) icon: ToolbarIcon, pub(crate) tooltip: ToolbarTooltip, } @@ -631,9 +624,7 @@ mod tests { .iter() .skip(1) .filter_map(|toggle| match &toggle.activation { - ToolbarActivation::Click(ToolbarEvent::SetStatusBarItemVisible(item, _)) => { - Some(*item) - } + ToolbarEvent::SetStatusBarItemVisible(item, _) => Some(*item), _ => None, }) .collect(); diff --git a/src/ui/toolbar/model/settings/helpers.rs b/src/ui/toolbar/model/settings/helpers.rs index a1d7d628..2b9d2a6b 100644 --- a/src/ui/toolbar/model/settings/helpers.rs +++ b/src/ui/toolbar/model/settings/helpers.rs @@ -236,12 +236,10 @@ pub(super) fn customize_groups() -> Vec { [ ToolbarItemCustomizeGroup::TopTools, ToolbarItemCustomizeGroup::TopControls, - ToolbarItemCustomizeGroup::SideSections, ToolbarItemCustomizeGroup::Actions, ToolbarItemCustomizeGroup::Pages, ToolbarItemCustomizeGroup::Boards, ToolbarItemCustomizeGroup::Presets, - ToolbarItemCustomizeGroup::ToolOptions, ToolbarItemCustomizeGroup::Sessions, ] .into_iter() @@ -266,16 +264,10 @@ pub(super) fn customize_group_contains( definition.surface == ToolbarItemSurface::Top && definition.category != ToolbarItemCategory::Tool } - ToolbarItemCustomizeGroup::SideSections => { - definition.category == ToolbarItemCategory::Group - } ToolbarItemCustomizeGroup::Actions => definition.category == ToolbarItemCategory::Action, ToolbarItemCustomizeGroup::Pages => definition.category == ToolbarItemCategory::Page, ToolbarItemCustomizeGroup::Boards => definition.category == ToolbarItemCategory::Board, ToolbarItemCustomizeGroup::Presets => definition.group == Some(ToolbarGroupId::Presets), - ToolbarItemCustomizeGroup::ToolOptions => { - definition.category == ToolbarItemCategory::ToolOption - } ToolbarItemCustomizeGroup::Sessions => definition.category == ToolbarItemCategory::Session, } } @@ -305,7 +297,6 @@ fn customize_order_group(group: ToolbarItemCustomizeGroup) -> Option Some(ToolbarItemOrderGroup::TopTools), ToolbarItemCustomizeGroup::TopControls => Some(ToolbarItemOrderGroup::TopControls), - ToolbarItemCustomizeGroup::SideSections => Some(ToolbarItemOrderGroup::SideSections), _ => None, } } @@ -357,3 +348,79 @@ fn control_item_id(id: ToolbarControlId) -> Option { _ => return None, }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_settings_control_maps_to_its_retained_item_id() { + for (control, expected) in [ + ( + ToolbarControlId::SettingsContextAwareUi, + ids::SIDE_SETTINGS_CONTEXT_AWARE_UI, + ), + ( + ToolbarControlId::SettingsTextControls, + ids::SIDE_SETTINGS_TEXT_CONTROLS, + ), + ( + ToolbarControlId::SettingsStatusBar, + ids::SIDE_SETTINGS_STATUS_BAR, + ), + ( + ToolbarControlId::SettingsStatusBoardBadge, + ids::SIDE_SETTINGS_STATUS_BOARD_BADGE, + ), + ( + ToolbarControlId::SettingsStatusPageBadge, + ids::SIDE_SETTINGS_STATUS_PAGE_BADGE, + ), + ( + ToolbarControlId::SettingsFloatingBadgeAlways, + ids::SIDE_SETTINGS_FLOATING_BADGE_ALWAYS, + ), + ( + ToolbarControlId::SettingsPresetToasts, + ids::SIDE_SETTINGS_PRESET_TOASTS, + ), + ( + ToolbarControlId::SettingsPresets, + ids::SIDE_SETTINGS_PRESETS, + ), + ( + ToolbarControlId::SettingsActions, + ids::SIDE_SETTINGS_ACTIONS, + ), + ( + ToolbarControlId::SettingsZoomActions, + ids::SIDE_SETTINGS_ZOOM_ACTIONS, + ), + ( + ToolbarControlId::SettingsAdvancedActions, + ids::SIDE_SETTINGS_ADVANCED_ACTIONS, + ), + (ToolbarControlId::SettingsBoards, ids::SIDE_SETTINGS_BOARDS), + (ToolbarControlId::SettingsPages, ids::SIDE_SETTINGS_PAGES), + ( + ToolbarControlId::SettingsStepControls, + ids::SIDE_SETTINGS_STEP_CONTROLS, + ), + ( + ToolbarControlId::OpenCommandPalette, + ids::SIDE_SETTINGS_COMMAND_PALETTE, + ), + ( + ToolbarControlId::OpenConfigurator, + ids::SIDE_SETTINGS_CONFIGURATOR, + ), + ( + ToolbarControlId::OpenConfigFile, + ids::SIDE_SETTINGS_CONFIG_FILE, + ), + (ToolbarControlId::OpenAbout, ids::SIDE_SETTINGS_ABOUT), + ] { + assert_eq!(control_item_id(control), Some(expected), "{control:?}"); + } + } +} diff --git a/src/ui/toolbar/model/tools.rs b/src/ui/toolbar/model/tools.rs index 539111df..09a1a63d 100644 --- a/src/ui/toolbar/model/tools.rs +++ b/src/ui/toolbar/model/tools.rs @@ -7,7 +7,7 @@ use crate::config::{ToolbarItemId, ToolbarItemOrderGroup, toolbar_item_ids as ids}; use crate::input::Tool; -use crate::ui::toolbar::{ToolbarSideSection, ToolbarSnapshot}; +use crate::ui::toolbar::ToolbarSnapshot; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum SemanticToolIcon { @@ -85,40 +85,6 @@ const POLYGON_TOOLS: [Tool; 5] = [ Tool::FreeformPolygon, ]; -/// Which pane a side section belongs to. The Draw pane holds per-tool -/// drawing properties; Canvas holds canvas management; Session and Settings -/// are their own panes. -pub(crate) fn pane_for_section( - section: crate::ui::toolbar::ToolbarSideSection, -) -> crate::ui::toolbar::SidePane { - use crate::ui::toolbar::{SidePane, ToolbarSideSection as S}; - match section { - S::Colors - | S::Presets - | S::Thickness - | S::EraserMode - | S::PolygonSides - | S::ArrowLabels - | S::StepMarkers - | S::MarkerOpacity - | S::TextSize - | S::Font => SidePane::Draw, - S::Actions | S::Boards | S::Pages | S::StepUndo => SidePane::Canvas, - S::Session => SidePane::Session, - S::Settings => SidePane::Settings, - } -} - -/// User-ordered side sections filtered to the active pane. -pub(crate) fn ordered_pane_sections( - snapshot: &ToolbarSnapshot, -) -> Vec { - ordered_side_sections(snapshot) - .into_iter() - .filter(|section| pane_for_section(*section) == snapshot.active_side_pane) - .collect() -} - pub(crate) fn top_tool_buttons(simple: bool) -> &'static [Tool] { if simple { &SIMPLE_TOOL_BUTTONS @@ -454,51 +420,6 @@ pub(crate) fn is_fill_tool(tool: Tool) -> bool { ) } -pub(crate) fn ordered_side_sections(snapshot: &ToolbarSnapshot) -> Vec { - snapshot - .resolved_toolbar_items - .order - .ordered_ids(ToolbarItemOrderGroup::SideSections) - .iter() - .filter_map(|id| side_section_for_toolbar_item_id(*id)) - .collect() -} - -fn side_section_for_toolbar_item_id(id: ToolbarItemId) -> Option { - [ - (ids::SIDE_GROUP_COLORS, ToolbarSideSection::Colors), - (ids::SIDE_GROUP_PRESETS, ToolbarSideSection::Presets), - (ids::SIDE_GROUP_THICKNESS, ToolbarSideSection::Thickness), - (ids::SIDE_GROUP_ERASER_MODE, ToolbarSideSection::EraserMode), - ( - ids::SIDE_GROUP_POLYGON_SIDES, - ToolbarSideSection::PolygonSides, - ), - ( - ids::SIDE_GROUP_ARROW_LABELS, - ToolbarSideSection::ArrowLabels, - ), - ( - ids::SIDE_GROUP_STEP_MARKERS, - ToolbarSideSection::StepMarkers, - ), - ( - ids::SIDE_GROUP_MARKER_OPACITY, - ToolbarSideSection::MarkerOpacity, - ), - (ids::SIDE_GROUP_TEXT_SIZE, ToolbarSideSection::TextSize), - (ids::SIDE_GROUP_FONT, ToolbarSideSection::Font), - (ids::SIDE_GROUP_ACTIONS, ToolbarSideSection::Actions), - (ids::SIDE_GROUP_BOARDS, ToolbarSideSection::Boards), - (ids::SIDE_GROUP_PAGES, ToolbarSideSection::Pages), - (ids::SIDE_GROUP_STEP_UNDO, ToolbarSideSection::StepUndo), - (ids::SIDE_GROUP_SESSION, ToolbarSideSection::Session), - (ids::SIDE_GROUP_SETTINGS, ToolbarSideSection::Settings), - ] - .into_iter() - .find_map(|(candidate, section)| (candidate == id).then_some(section)) -} - pub(crate) fn current_shape_tool(active_tool: Tool, tool_override: Option) -> Option { tool_override .filter(|tool| is_shape_tool(*tool)) diff --git a/src/ui/toolbar/model/top_spec/control.rs b/src/ui/toolbar/model/top_spec/control.rs index e282d3fb..5858718d 100644 --- a/src/ui/toolbar/model/top_spec/control.rs +++ b/src/ui/toolbar/model/top_spec/control.rs @@ -72,8 +72,8 @@ pub(crate) enum TopToolbarControl { Utility(TopToolbarUtility), /// One saved-preset slot (0-based index) in the presets island. Left /// click applies the saved preset when the slot is filled, or saves the - /// current setup into it when the slot is empty (the side-palette - /// convention). Renders as a compact button showing the saved tool glyph + /// current setup into it when the slot is empty. Renders as a compact + /// button showing the saved tool glyph /// in the neutral foreground with the preset color as a separate corner /// swatch, or the 1-based slot number when empty. Preset(usize), @@ -141,8 +141,8 @@ impl TopToolbarControl { Self::Tool(tool) => ToolbarEvent::SelectTool(tool), Self::ShapePicker => ToolbarEvent::ToggleShapePicker(!snapshot.shape_picker_open), Self::Utility(utility) => utility_event(utility, snapshot), - // Filled slots apply; empty slots save the current setup, reusing - // the side-palette click convention (the slot is 1-based). + // Filled slots apply; empty slots save the current setup (the slot + // is 1-based). Self::Preset(index) => { let slot = index + 1; if preset_slot(snapshot, index).is_some() { diff --git a/src/ui/toolbar/model/top_spec/spec.rs b/src/ui/toolbar/model/top_spec/spec.rs index 504b9842..b8c50beb 100644 --- a/src/ui/toolbar/model/top_spec/spec.rs +++ b/src/ui/toolbar/model/top_spec/spec.rs @@ -236,13 +236,10 @@ impl TopToolbarSpec { /// Overflow menu content, in menu order: the destructive Clear first, /// then the width-dropped tools and utilities in configured order, then - /// the Canvas/Session/Settings popover entries. The three menu entries - /// appear in every layout (`side_layout = "panel"` included): under - /// `pill` they are the only surface hosting those functions, and under - /// `panel` the popovers are transient quick surfaces that cannot be - /// confused with the pinned side panes — so they are unconditional rather - /// than pill-gated. Like the restore controls they are not hideable - /// items: under `pill` they must always be reachable. + /// the Canvas/Session/Settings popover entries. Those three popover hosts + /// are the only surfaces for those functions on the unified top toolbar, + /// so they are unconditional rather than width-gated. Like the restore + /// controls they are not hideable items and must always be reachable. fn overflow_controls( clear_visible: bool, plan: &TopStripPlan, diff --git a/src/ui/toolbar/model/top_spec/tests/behavior.rs b/src/ui/toolbar/model/top_spec/tests/behavior.rs index 2148ba9b..80afce8b 100644 --- a/src/ui/toolbar/model/top_spec/tests/behavior.rs +++ b/src/ui/toolbar/model/top_spec/tests/behavior.rs @@ -327,7 +327,7 @@ fn presets_island_hosts_the_saved_slots() { let filled = TopToolbarControl::Preset(0); let empty = TopToolbarControl::Preset(1); // The filled slot applies preset 1 and reads active (the applied - // slot); the empty slot reuses the side-palette save convention. + // slot); an empty slot saves the current setup. assert_eq!(filled.event(&snapshot), ToolbarEvent::ApplyPreset(1)); assert!(filled.active(&snapshot)); assert_eq!( diff --git a/src/ui/toolbar/model/top_spec/tests/structure.rs b/src/ui/toolbar/model/top_spec/tests/structure.rs index 70f7f853..e53c817c 100644 --- a/src/ui/toolbar/model/top_spec/tests/structure.rs +++ b/src/ui/toolbar/model/top_spec/tests/structure.rs @@ -271,10 +271,8 @@ fn narrow_spec_moves_dropped_controls_to_one_ordered_overflow() { #[test] fn overflow_hosts_canvas_session_and_settings_entries_in_every_layout() { - // Decision (M4-B3, extended in M8): the three popover entries are - // unconditional — they show under both `side_layout` values and are - // not hideable items, because under `pill` they are the only surface - // hosting Canvas/Session/Settings. + // The three popover entries are unconditional and not hideable items; + // they are the only surface hosting Canvas/Session/Settings. for layout_mode in [ToolbarLayoutMode::Regular, ToolbarLayoutMode::Simple] { let mut snapshot = snapshot(); snapshot.layout_mode = layout_mode; diff --git a/src/ui/toolbar/session_format.rs b/src/ui/toolbar/session_format.rs new file mode 100644 index 00000000..b0db907e --- /dev/null +++ b/src/ui/toolbar/session_format.rs @@ -0,0 +1,78 @@ +//! Renderer-neutral label formatting for session names and paths. +//! +//! Both adapters share session-extension stripping. The built-in Session +//! popover also uses the fixed character-count truncators below, while GTK +//! delegates visible ellipsization to Pango. + +/// Middle-ellipsize so both the head and the distinguishing tail survive. +/// Tail truncation made e.g. two different "lecture-05-…" files render +/// identically in the recents list. +pub fn truncate_middle(value: &str, max_chars: usize) -> String { + let count = value.chars().count(); + if count <= max_chars { + return value.to_string(); + } + let keep = max_chars.saturating_sub(1).max(2); + let head = keep.div_ceil(2); + let tail = keep - head; + let mut truncated: String = value.chars().take(head).collect(); + truncated.push('…'); + truncated.extend(value.chars().skip(count - tail)); + truncated +} + +/// Keep the tail of a path — the leading directories are the least +/// informative part of a session path. +pub fn truncate_start(value: &str, max_chars: usize) -> String { + let count = value.chars().count(); + if count <= max_chars { + return value.to_string(); + } + let keep = max_chars.saturating_sub(1); + let mut truncated = String::from("…"); + truncated.extend(value.chars().skip(count - keep)); + truncated +} + +/// Drop the constant session-file extension in list rows; it costs the +/// characters that distinguish one session from another. +pub fn strip_session_extension(value: &str) -> &str { + value + .strip_suffix(".wayscriber-session") + .or_else(|| value.strip_suffix(".wayscriber")) + .unwrap_or(value) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn truncate_middle_keeps_head_and_tail() { + assert_eq!(truncate_middle("short", 10), "short"); + let truncated = truncate_middle("lecture-05-quantum-mechanics-part-two", 20); + assert_eq!(truncated.chars().count(), 20); + assert!(truncated.starts_with("lecture-05")); + assert!(truncated.ends_with("part-two")); + assert!(truncated.contains('…')); + } + + #[test] + fn truncate_start_keeps_path_tail() { + assert_eq!(truncate_start("/tmp/x", 10), "/tmp/x"); + let truncated = truncate_start("/home/user/.local/share/wayscriber/sessions", 20); + assert_eq!(truncated.chars().count(), 20); + assert!(truncated.starts_with('…')); + assert!(truncated.ends_with("wayscriber/sessions")); + } + + #[test] + fn strip_session_extension_drops_known_suffixes() { + assert_eq!( + strip_session_extension("lecture.wayscriber-session"), + "lecture" + ); + assert_eq!(strip_session_extension("lecture.wayscriber"), "lecture"); + assert_eq!(strip_session_extension("lecture.txt"), "lecture.txt"); + } +} diff --git a/src/ui/toolbar/snapshot/build.rs b/src/ui/toolbar/snapshot/build.rs index 06e57f38..86cce0da 100644 --- a/src/ui/toolbar/snapshot/build.rs +++ b/src/ui/toolbar/snapshot/build.rs @@ -10,21 +10,12 @@ use super::types::{PresetFeedbackSnapshot, PresetSlotSnapshot, ToolbarSnapshot}; impl ToolbarSnapshot { #[allow(dead_code)] pub fn from_input(state: &InputState) -> Self { - Self::from_input_with_options(state, ToolbarBindingHints::default(), false) + Self::from_input_with_bindings(state, ToolbarBindingHints::default()) } - #[allow(dead_code)] pub fn from_input_with_bindings( state: &InputState, binding_hints: ToolbarBindingHints, - ) -> Self { - Self::from_input_with_options(state, binding_hints, false) - } - - pub fn from_input_with_options( - state: &InputState, - binding_hints: ToolbarBindingHints, - show_drawer_hint: bool, ) -> Self { let frame = state.boards.active_frame(); let active_tool = state.active_tool(); @@ -99,8 +90,6 @@ impl ToolbarSnapshot { }) }) .collect(); - let active_side_pane = state.toolbar_side_pane; - let side_scroll = state.toolbar_side_scroll[state.toolbar_side_pane.index()]; let customize_items_open = state.toolbar_customize_items_open; let customize_items_group = state.toolbar_customize_items_group; let status_bar_contents_open = state.toolbar_status_bar_contents_open; @@ -109,14 +98,12 @@ impl ToolbarSnapshot { let show_pages_section = state.show_pages_section; let show_boards_section = state.show_boards_section; let show_step_section = state.show_step_section; - let show_settings_section = state.show_settings_section; let delay_actions_enabled = state.show_step_section && state.show_delay_sliders; Self { active_tool, tool_override: state.tool_override(), color: state.color_for_tool(active_tool), - picker_hsv: state.toolbar_picker_hsv, quick_colors: state.quick_colors.clone(), thickness: thickness_value, eraser_size: state.eraser_size, @@ -160,7 +147,6 @@ impl ToolbarSnapshot { custom_undo_steps: state.custom_undo_steps, custom_redo_steps: state.custom_redo_steps, top_pinned: state.toolbar_top_pinned, - side_pinned: state.toolbar_side_pinned, use_icons: state.toolbar_use_icons, toolbar_scale: state.toolbar_scale, layout_mode: state.toolbar_layout_mode, @@ -177,8 +163,6 @@ impl ToolbarSnapshot { show_step_section, show_text_controls: state.show_text_controls, context_aware_ui: state.context_aware_ui, - show_settings_section, - collapsed_side_sections: state.toolbar_collapsed_side_sections.clone(), show_tool_preview: state.show_tool_preview, show_status_bar: state.show_status_bar, status_bar_interactive: state.status_bar_interactive, @@ -209,22 +193,17 @@ impl ToolbarSnapshot { // Fade is owned by the backend engine; renderers see 1.0 until // the backend publishes the animated value. top_fade: 1.0, - side_minimized: state.toolbar_side_minimized, selection_properties: if active_tool == crate::input::Tool::Select { state.selection_pill_entries() } else { Vec::new() }, top_viewport_max: None, - active_side_pane, - side_scroll, - side_viewport_max: None, top_available_height: None, customize_items_open, customize_items_group, status_bar_contents_open, binding_hints, - show_drawer_hint, is_transparent: state.board_is_transparent(), render_profile_generation: state.render_profile_generation(), active_session_name: None, diff --git a/src/ui/toolbar/snapshot/types.rs b/src/ui/toolbar/snapshot/types.rs index dc734996..2b30a596 100644 --- a/src/ui/toolbar/snapshot/types.rs +++ b/src/ui/toolbar/snapshot/types.rs @@ -1,8 +1,5 @@ use crate::config::QuickColorPalette; -use crate::config::{ - ResolvedToolbarItems, ToolbarGroupId, ToolbarItemId, ToolbarLayoutMode, TopDisplayMode, - toolbar_item_ids as ids, -}; +use crate::config::{ResolvedToolbarItems, ToolbarItemId, ToolbarLayoutMode, TopDisplayMode}; use crate::draw::{Color, EraserKind, FontDescriptor}; use crate::input::state::PresetFeedbackKind; use crate::input::tool::{ToolControlGroup, ToolProfile}; @@ -10,10 +7,8 @@ use crate::input::{EraserMode, Tool}; use std::path::PathBuf; use super::super::bindings::ToolbarBindingHints; -use super::super::events::ToolbarSideSection; -use std::collections::BTreeSet; -/// The kind of tool-specific options to display in the side panel. +/// The kind of tool-specific options to display. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ToolOptionsKind { /// No tool-specific options (e.g., Select tool) @@ -249,9 +244,6 @@ pub struct ToolbarSnapshot { pub active_tool: Tool, pub tool_override: Option, pub color: Color, - /// Last HSV triple committed from the palette color picker, when it - /// still matches `color`; keeps hue/saturation stable across grays. - pub picker_hsv: Option<(f64, f64, f64)>, pub quick_colors: QuickColorPalette, pub thickness: f64, pub eraser_size: f64, @@ -299,8 +291,6 @@ pub struct ToolbarSnapshot { pub custom_redo_steps: usize, /// Whether the top toolbar is pinned (opens at startup) pub top_pinned: bool, - /// Whether the side toolbar is pinned (opens at startup) - pub side_pinned: bool, /// Whether to use icons instead of text labels pub use_icons: bool, /// Scale factor for toolbar UI (icons + layout) @@ -325,7 +315,7 @@ pub struct ToolbarSnapshot { pub show_marker_opacity_section: bool, /// Whether to show preset action toasts pub show_preset_toasts: bool, - /// Whether to show presets in the side toolbar + /// Whether to show the Presets section pub show_presets: bool, /// Whether to show the Step Undo/Redo section pub show_step_section: bool, @@ -333,10 +323,6 @@ pub struct ToolbarSnapshot { pub show_text_controls: bool, /// Whether to enable context-aware UI that shows/hides controls based on active tool pub context_aware_ui: bool, - /// Whether to show the Settings section - pub show_settings_section: bool, - /// Side drawer sections whose body content is collapsed. - pub collapsed_side_sections: BTreeSet, pub show_tool_preview: bool, pub show_status_bar: bool, pub status_bar_interactive: bool, @@ -374,18 +360,9 @@ pub struct ToolbarSnapshot { /// `fade::TOP_STRIP_DIM_LEVEL` = dimmed, values between while a fade /// transition is in flight. Owned by the backend fade engine. pub top_fade: f64, - /// Whether the side palette is minimized to its edge restore tab - pub side_minimized: bool, /// Width available to the top strip in pre-scale spec units, when /// known; content past this degrades into the overflow menu. pub top_viewport_max: Option, - /// Active side-palette pane - pub active_side_pane: super::super::events::SidePane, - /// Scroll offset of the active pane (logical pixels, clamped at render) - pub side_scroll: f64, - /// Height available to the side palette in pre-scale spec units, when - /// known; the pane scrolls instead of growing the surface past this. - pub side_viewport_max: Option, /// Height available from the top toolbar surface origin to the output /// bottom, in pre-scale spec units, when known. The open /// Canvas/Session/Settings popover grows to fill the room actually below @@ -410,7 +387,6 @@ pub struct ToolbarSnapshot { /// Binding hints for tooltips pub binding_hints: ToolbarBindingHints, /// Whether to show the drawer onboarding hint (first-time users) - pub show_drawer_hint: bool, /// Whether the current board is the transparent overlay pub is_transparent: bool, /// Changes whenever final-render color profile preview changes. @@ -459,54 +435,4 @@ impl ToolbarSnapshot { pub fn toolbar_item_hidden(&self, item: ToolbarItemId) -> bool { self.resolved_toolbar_items.is_hidden(item) } - - pub fn toolbar_group_hidden(&self, group: ToolbarGroupId) -> bool { - self.toolbar_item_hidden(group.toolbar_item_id()) - } - - pub fn side_section_hidden(&self, section: ToolbarSideSection) -> bool { - let group = match section { - ToolbarSideSection::Colors => ToolbarGroupId::Colors, - ToolbarSideSection::Presets => ToolbarGroupId::Presets, - ToolbarSideSection::Thickness => ToolbarGroupId::Thickness, - ToolbarSideSection::EraserMode => ToolbarGroupId::EraserMode, - ToolbarSideSection::PolygonSides => ToolbarGroupId::PolygonSides, - ToolbarSideSection::ArrowLabels => ToolbarGroupId::ArrowLabels, - ToolbarSideSection::StepMarkers => ToolbarGroupId::StepMarkers, - ToolbarSideSection::MarkerOpacity => ToolbarGroupId::MarkerOpacity, - ToolbarSideSection::TextSize => ToolbarGroupId::TextSize, - ToolbarSideSection::Font => ToolbarGroupId::Font, - ToolbarSideSection::Actions => ToolbarGroupId::Actions, - ToolbarSideSection::Boards => ToolbarGroupId::Boards, - ToolbarSideSection::Pages => ToolbarGroupId::Pages, - ToolbarSideSection::StepUndo => ToolbarGroupId::StepUndo, - ToolbarSideSection::Session => ToolbarGroupId::Session, - ToolbarSideSection::Settings => ToolbarGroupId::Settings, - }; - let legacy_item = match section { - ToolbarSideSection::Colors => Some(ids::SIDE_TOOL_OPTIONS_COLOR), - ToolbarSideSection::Thickness => Some(ids::SIDE_TOOL_OPTIONS_THICKNESS), - ToolbarSideSection::EraserMode => Some(ids::SIDE_TOOL_OPTIONS_ERASER_MODE), - ToolbarSideSection::PolygonSides => Some(ids::SIDE_TOOL_OPTIONS_POLYGON_SIDES), - ToolbarSideSection::ArrowLabels => Some(ids::SIDE_TOOL_OPTIONS_ARROW_LABELS), - ToolbarSideSection::StepMarkers => Some(ids::SIDE_TOOL_OPTIONS_STEP_MARKER_RESET), - ToolbarSideSection::MarkerOpacity => Some(ids::SIDE_TOOL_OPTIONS_MARKER_OPACITY), - ToolbarSideSection::TextSize => Some(ids::SIDE_TOOL_OPTIONS_FONT_SIZE), - ToolbarSideSection::Font => Some(ids::SIDE_TOOL_OPTIONS_FONT_FAMILY), - ToolbarSideSection::Presets - | ToolbarSideSection::Actions - | ToolbarSideSection::Boards - | ToolbarSideSection::Pages - | ToolbarSideSection::StepUndo - | ToolbarSideSection::Session - | ToolbarSideSection::Settings => None, - }; - - self.toolbar_group_hidden(group) - || legacy_item.is_some_and(|item| self.toolbar_item_hidden(item)) - } - - pub fn side_section_collapsed(&self, section: ToolbarSideSection) -> bool { - self.collapsed_side_sections.contains(§ion) - } } From d48259d0c86f75e8aac1302e3ceea57bec23ebe9 Mon Sep 17 00:00:00 2001 From: devmobasa <4170275+devmobasa@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:42:04 +0200 Subject: [PATCH 2/2] fix(toolbar): preserve interaction state --- src/backend/wayland/runtime_ui_state.rs | 1 + src/backend/wayland/runtime_ui_state/board.rs | 2 ++ .../wayland/runtime_ui_state/coordinator.rs | 3 +++ .../wayland/runtime_ui_state/positions.rs | 5 +++- .../wayland/runtime_ui_state/rollback.rs | 18 ++++--------- src/backend/wayland/runtime_ui_state/tests.rs | 1 + .../tests/preference_actions.rs | 5 +++- .../runtime_ui_state/tests/visibility.rs | 27 +++++++++++++++++++ src/runtime_ui_state/model.rs | 5 ++++ src/ui/toolbar/model/event_policy.rs | 14 +++++++++- 10 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/backend/wayland/runtime_ui_state.rs b/src/backend/wayland/runtime_ui_state.rs index 64a56455..734f340f 100644 --- a/src/backend/wayland/runtime_ui_state.rs +++ b/src/backend/wayland/runtime_ui_state.rs @@ -133,6 +133,7 @@ impl UnavailablePersistencePreviews { }; self.item_drag = Some(PreviewRollbackSnapshot { values: values.values().clone(), + derive_toolbar_visibility_from_pins: false, }); true } diff --git a/src/backend/wayland/runtime_ui_state/board.rs b/src/backend/wayland/runtime_ui_state/board.rs index 3c8eb11e..87ea86d7 100644 --- a/src/backend/wayland/runtime_ui_state/board.rs +++ b/src/backend/wayland/runtime_ui_state/board.rs @@ -119,6 +119,7 @@ impl ToolbarRuntimeState { values: [(target.clone(), InteractionSeedValue::Bool(current))] .into_iter() .collect(), + derive_toolbar_visibility_from_pins: false, }; let session = match self .controller @@ -211,6 +212,7 @@ impl ToolbarRuntimeState { values: [(target.clone(), InteractionSeedValue::Bool(current))] .into_iter() .collect(), + derive_toolbar_visibility_from_pins: false, }; let session = match self .controller diff --git a/src/backend/wayland/runtime_ui_state/coordinator.rs b/src/backend/wayland/runtime_ui_state/coordinator.rs index feb54fc1..1e0da6e6 100644 --- a/src/backend/wayland/runtime_ui_state/coordinator.rs +++ b/src/backend/wayland/runtime_ui_state/coordinator.rs @@ -118,6 +118,8 @@ impl ToolbarRuntimeState { ) -> Option { let rollback = PreviewRollbackSnapshot { values: rollback_values.values().clone(), + derive_toolbar_visibility_from_pins: target + == ToolbarRuntimeUiPersistenceTarget::ToolbarVisibility, }; let scope = RuntimeUiMutationScope::batch(rollback_values.targets()); match self.controller.begin_runtime_preview(scope, rollback) { @@ -168,6 +170,7 @@ impl ToolbarRuntimeState { }; let rollback = PreviewRollbackSnapshot { values: values.values().clone(), + derive_toolbar_visibility_from_pins: false, }; let scope = RuntimeUiMutationScope::one(InteractionSeedTarget::ItemOrder(group)); match self.controller.begin_runtime_preview(scope, rollback) { diff --git a/src/backend/wayland/runtime_ui_state/positions.rs b/src/backend/wayland/runtime_ui_state/positions.rs index ff38d7ed..6b633f65 100644 --- a/src/backend/wayland/runtime_ui_state/positions.rs +++ b/src/backend/wayland/runtime_ui_state/positions.rs @@ -56,7 +56,10 @@ pub(super) fn position_rollback( ); } } - PreviewRollbackSnapshot { values } + PreviewRollbackSnapshot { + values, + derive_toolbar_visibility_from_pins: false, + } } /// The committed values for a finished drag, or `None` when any guarded offset diff --git a/src/backend/wayland/runtime_ui_state/rollback.rs b/src/backend/wayland/runtime_ui_state/rollback.rs index 09bdf1de..6196e919 100644 --- a/src/backend/wayland/runtime_ui_state/rollback.rs +++ b/src/backend/wayland/runtime_ui_state/rollback.rs @@ -80,19 +80,11 @@ pub(in crate::backend::wayland) fn apply_toolbar_runtime_rollback( } } } - // The visibility toggle persists the top pin; the pin button persists the - // same key but is deliberately decoupled from live visibility, so a - // rolled-back toggle must re-derive the live visibility flag — otherwise - // the screen disagrees with the restored pin (and with the next start). - // The toggle arm skips no-op pin writes, so every snapshot that lands here - // crossed a genuine pin transition and the derived flag is exactly the - // startup (pin-derived) form of the pre-toggle screen. The display mode is - // deliberately not restored: the persisted type has no `Hidden`, and none - // is needed — after a hide-rollback the still-`Hidden` live mode - // reproduces the pre-toggle screen, after a show-rollback the strip is - // hidden either way, and any future show unfolds `Hidden` to `Full` - // regardless. - if rollback.values.contains_key(&Target::TopPinned) { + // The keyboard visibility toggle persists through the top pin and changes + // the current screen; the pin button persists the same key but deliberately + // leaves live visibility alone. The preview records which path created the + // rollback so this cannot be guessed incorrectly from `TopPinned` alone. + if rollback.derive_toolbar_visibility_from_pins { input.derive_toolbar_visibility_from_pins(); } input.needs_redraw = true; diff --git a/src/backend/wayland/runtime_ui_state/tests.rs b/src/backend/wayland/runtime_ui_state/tests.rs index 6998b910..272590b7 100644 --- a/src/backend/wayland/runtime_ui_state/tests.rs +++ b/src/backend/wayland/runtime_ui_state/tests.rs @@ -234,6 +234,7 @@ fn pins_rollback(top: bool) -> PreviewRollbackSnapshot { InteractionSeedTarget::TopPinned, InteractionSeedValue::Bool(top), )]), + derive_toolbar_visibility_from_pins: true, } } 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 9b882e94..9cbf9a2d 100644 --- a/src/backend/wayland/runtime_ui_state/tests/preference_actions.rs +++ b/src/backend/wayland/runtime_ui_state/tests/preference_actions.rs @@ -262,7 +262,10 @@ fn a_rollback_restores_every_durable_chrome_preference() { apply_toolbar_runtime_rollback( &mut input, &mut positions, - &PreviewRollbackSnapshot { values }, + &PreviewRollbackSnapshot { + values, + derive_toolbar_visibility_from_pins: false, + }, ); let restarted = input_from_config(&config); diff --git a/src/backend/wayland/runtime_ui_state/tests/visibility.rs b/src/backend/wayland/runtime_ui_state/tests/visibility.rs index 521625e9..b5eec7d1 100644 --- a/src/backend/wayland/runtime_ui_state/tests/visibility.rs +++ b/src/backend/wayland/runtime_ui_state/tests/visibility.rs @@ -110,6 +110,33 @@ fn a_rolled_back_show_toggle_re_hides_the_toolbar() { ); } +/// The pin button changes restart policy, not current visibility. Rolling its +/// preview back must restore only the pin and leave the visible screen alone. +#[test] +fn a_rolled_back_pin_button_keeps_a_visible_unpinned_toolbar_visible() { + let config = Config::default(); + let mut input = input_from_config(&config); + let mut positions = config_positions(&config); + assert!(input.toolbar_visible && input.toolbar_top_visible); + // Post-click state: the button pinned an already-visible toolbar. + input.toolbar_top_pinned = true; + let rollback = PreviewRollbackSnapshot { + values: BTreeMap::from([( + InteractionSeedTarget::TopPinned, + InteractionSeedValue::Bool(false), + )]), + derive_toolbar_visibility_from_pins: false, + }; + + apply_toolbar_runtime_rollback(&mut input, &mut positions, &rollback); + + assert!(!input.toolbar_top_pinned); + assert!( + input.toolbar_visible && input.toolbar_top_visible, + "pin rollback must not derive live visibility from the restored pin" + ); +} + /// End-to-end through the store's forced-rollback path: a visibility toggle /// abandoned behind a failed reset barrier resolves to a drained rollback, /// and applying it restores the pre-toggle screen, not just the pins. diff --git a/src/runtime_ui_state/model.rs b/src/runtime_ui_state/model.rs index a21df115..3632d2bf 100644 --- a/src/runtime_ui_state/model.rs +++ b/src/runtime_ui_state/model.rs @@ -286,4 +286,9 @@ impl RuntimeUiMutationPermit { #[derive(Debug, Clone, PartialEq, Eq, Default)] pub(crate) struct PreviewRollbackSnapshot { pub(crate) values: BTreeMap, + /// Keyboard visibility toggles persist through the pin target but also + /// change the current screen. Pin-button previews use the same value + /// target without changing live visibility, so the rollback must carry + /// which behavior created it instead of inferring that from `TopPinned`. + pub(crate) derive_toolbar_visibility_from_pins: bool, } diff --git a/src/ui/toolbar/model/event_policy.rs b/src/ui/toolbar/model/event_policy.rs index d931ddd3..00861e40 100644 --- a/src/ui/toolbar/model/event_policy.rs +++ b/src/ui/toolbar/model/event_policy.rs @@ -299,6 +299,9 @@ pub(crate) fn popovers_for_event(event: &ToolbarEvent) -> &'static [ToolbarPopov | ToolbarEvent::ZoomOut | ToolbarEvent::ResetZoom | ToolbarEvent::ToggleZoomLock + | ToolbarEvent::Undo + | ToolbarEvent::Redo + | ToolbarEvent::ClearCanvas { .. } | ToolbarEvent::UndoAll | ToolbarEvent::RedoAll | ToolbarEvent::UndoAllDelayed @@ -662,6 +665,16 @@ mod popover_affinity_tests { // Hosted controls. assert!(popovers_for_event(&ToolbarEvent::ZoomIn).contains(&P::Canvas)); + for event in [ + ToolbarEvent::Undo, + ToolbarEvent::Redo, + ToolbarEvent::ClearCanvas { instant: false }, + ] { + assert!( + popovers_for_event(&event).contains(&P::Canvas), + "Canvas action {event:?} must keep its owning popover open" + ); + } assert!(popovers_for_event(&ToolbarEvent::SessionInfo).contains(&P::Session)); assert!(popovers_for_event(&ToolbarEvent::ToggleIconMode(true)).contains(&P::Settings)); assert!(popovers_for_event(&ToolbarEvent::ToggleFill(true)).contains(&P::ShapePicker)); @@ -669,7 +682,6 @@ mod popover_affinity_tests { // Foreign controls close it. assert!(!popovers_for_event(&ToolbarEvent::ZoomIn).contains(&P::Settings)); assert!(!popovers_for_event(&ToolbarEvent::SessionInfo).contains(&P::Canvas)); - assert!(popovers_for_event(&ToolbarEvent::Undo).is_empty()); // The pairs that genuinely belong to two popovers. let configurator = popovers_for_event(&ToolbarEvent::OpenConfigurator);