docs/plans/2026-04-30-mos-mouse-scroll-action-design.md
Add a Mos-owned "Mouse Scroll" action group to the Buttons preferences action menu. The group appears below "Mouse Buttons" and exposes the same three scroll function roles as the Scrolling preferences panel: dash, toggle, and block.
The feature lets users bind multiple physical buttons, including Logi HID++ buttons, to the same scroll role without changing the existing single-key persisted Scrolling preferences format.
[Mos] Mouse Scroll action group below the mouse-button action group.[Mos] with a reusable tag renderer using the Mos logo-inspired blue/purple gradient and subtle inner highlight.dash, toggle, and block persistence compatibility for old users.ScrollHotkey? to arrays in this change.RecordedEvent.displayComponents storage shape.The Buttons preferences action menu is built by ShortcutManager.buildShortcutMenu(...), with shortcut definitions and categories in SystemShortcut. ButtonTableCellView configures the popup and uses ActionDisplayResolver plus ActionDisplayRenderer for selected-action presentation.
Button execution flows through InputProcessor, which resolves a ButtonBinding into ResolvedAction and stores stateful actions in activeBindings until the matching up event arrives.
Scroll hotkeys currently use OPTIONS_SCROLL_DEFAULT.dash, .toggle, and .block, each as one ScrollHotkey?. ScrollCore also tracks one held state per role. That model stays intact for existing Scrolling preferences and is supplemented by a new button-action activation source.
Logi HID++ button events already enter the same ButtonCore pipeline through LogiIntegrationBridge.dispatchLogiButtonEvent(...). Logi usage/divert registration for button bindings already registers enabled mouse trigger codes, so a Logi trigger bound to a Mos scroll action is covered by the existing path.
Keep the existing BrandTag rendering utility but make its model semantically reusable for Mos tags:
BrandTagConfig.mos.tag instead of Logi-specific brand semantics.BrandTag.createTagImage(...), createTagView(...), and createPrefixedImage(...) as the shared rendering functions.The final Mos tag style is:
MosThis keeps the current Logi tag output stable while giving future tags one public rendering path.
Add three predefined action identifiers:
mosScrollDashmosScrollTogglemosScrollBlockGroup them under a new category:
categoryMosMouseScrollThe category should be inserted after SystemShortcut.mouseButtonsCategory and before the conditional Logi category in ShortcutManager.
The category menu item uses the Mos tag image and localized title "Mouse Scroll". The submenu contains the three role actions.
Add a ResolvedAction.mosScroll(role: ScrollRole) case. It is stateful, like mouse-button and custom-key actions.
ShortcutExecutor.resolveAction(...) maps the three mosScroll* identifiers to the corresponding ScrollRole. ShortcutExecutor.execute(...) forwards down/up to a ScrollCore API dedicated to button-driven scroll actions.
Add per-role active counts for Mos scroll actions. A down event increments the count, and an up event decrements it. The role stays active while its count is above zero.
ScrollCore then derives the final role state from all active sources:
For dash, the amplification is 5.0 while any dash source is active and 1.0 otherwise.
This is the important safety rule: if two buttons are both bound to mosScrollDash, pressing both and releasing one keeps dash active until the second is released.
The existing Scrolling preferences fields stay as:
dash: ScrollHotkey?toggle: ScrollHotkey?block: ScrollHotkey?The new multi-binding behavior comes from multiple ButtonBinding records pointing to the same Mos scroll action identifier. No old UserDefaults or app-level scroll configuration needs migration.
Focused tests should cover:
mosScrollDash resolves to a stateful Mos scroll action.Verification should run focused tests first, then the full Debug test plan when the implementation is stable.