Skip to main content

openlogi_core/binding/
button.rs

1//! Rebindable mouse/keyboard button identifiers.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7/// One of the user-rebindable hotspots on a Logi mouse. The order matches the
8/// physical layout from front to side; [`ButtonId::ALL`] is consumed by the
9/// default-binding generator and the popover trigger list.
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
11pub enum ButtonId {
12    /// The primary button. Rebindable in the config schema, but the OS hook
13    /// never suppresses it — see [`ButtonId::is_os_hook_button`].
14    LeftClick,
15    /// The secondary button. Like [`ButtonId::LeftClick`], it always passes
16    /// through the OS hook.
17    RightClick,
18    /// The wheel click — one of the three buttons the OS hook remaps.
19    MiddleClick,
20    /// The thumb-side "back" button (mouse button 4), remapped by the OS hook.
21    Back,
22    /// The thumb-side "forward" button (mouse button 5), remapped by the OS hook.
23    Forward,
24    /// The "ModeShift" button under the wheel — typically used for SmartShift /
25    /// DPI cycle. Named `DpiToggle` for historical reasons.
26    DpiToggle,
27    /// The horizontal thumb wheel's click. Kept in [`ButtonId::ALL`] so its
28    /// default still seeds and dispatches when the wheel is diverted, even
29    /// though the mouse model surfaces one paired rotation control instead of
30    /// the click (see `mouse_model::geometry`).
31    Thumbwheel,
32    /// Rotating the thumb wheel "up" (positive rotation). Bound, by default, to
33    /// continuous horizontal scroll; see the agent-core `watchers`-side dispatch.
34    ThumbwheelScrollUp,
35    /// Rotating the thumb wheel "down" (negative rotation).
36    ThumbwheelScrollDown,
37    /// The HID++ gesture button on MX-line devices. The press itself
38    /// fires the bound action; swipe directions are P1.5 territory.
39    GestureButton,
40    /// Keyboard F-row "Search" control (`0x1b04` CID `0x00d4`,
41    /// `MultiPlatform_Search`) — F4 on the Signature series.
42    KeySearch,
43    /// Keyboard "Dictation" control (CID `0x0103`) — F5 on the Signature series.
44    KeyDictation,
45    /// Keyboard "Emoji" control (CID `0x0108`) — F6 on the Signature series.
46    KeyEmoji,
47    /// Keyboard "Screen Capture" control (CID `0x010a`) — F7 on the Signature
48    /// series.
49    KeyScreenCapture,
50    /// Keyboard "Mute Microphone" control (CID `0x011c`) — F8 on the Signature
51    /// series.
52    KeyMicMute,
53    /// Keyboard "Play/Pause" control (CID `0x00e5`) — F9 on the Signature series.
54    KeyPlayPause,
55    /// Keyboard "Mute" control (CID `0x00e7`) — F10 on the Signature series.
56    KeyMute,
57    /// Keyboard "Volume Down" control (CID `0x00e8`) — F11 on the Signature
58    /// series.
59    KeyVolumeDown,
60    /// Keyboard "Volume Up" control (CID `0x00e9`) — F12 on the Signature
61    /// series.
62    KeyVolumeUp,
63    /// The MX Master 4 Haptic Sense Panel — the touch-sensitive thumb rest
64    /// (Logi metadata slot `ASSIGNMENT_NAME_SHOW_RADIAL_MENU`, HID++ CID
65    /// `0x01a0`). A separate physical control from [`ButtonId::GestureButton`];
66    /// captured over HID++ like it, and eligible as the gesture owner.
67    HapticPanel,
68    /// Tilting the main wheel left — `0x1b04` CID `0x005b` ("Left Scroll"),
69    /// Logi metadata slot `SLOT_NAME_LEFT_SCROLL_BUTTON`. A distinct control
70    /// from the thumb wheel: it is a plain divertable button, not a rotation,
71    /// and it lives on the main wheel of mice like the MX Anywhere 2S.
72    WheelTiltLeft,
73    /// Tilting the main wheel right — `0x1b04` CID `0x005d` ("Right Scroll"),
74    /// Logi metadata slot `SLOT_NAME_RIGHT_SCROLL_BUTTON`. Counterpart to
75    /// [`ButtonId::WheelTiltLeft`].
76    ///
77    /// Declared last: the TOML config and any serialized form encode the
78    /// variant identifier / index, so new buttons are append-only.
79    WheelTiltRight,
80}
81
82impl ButtonId {
83    /// Every rebindable button in declaration (physical front-to-side) order —
84    /// the iteration source for default-binding seeding and the popover
85    /// trigger list.
86    pub const ALL: [ButtonId; 13] = [
87        ButtonId::LeftClick,
88        ButtonId::RightClick,
89        ButtonId::MiddleClick,
90        ButtonId::WheelTiltLeft,
91        ButtonId::WheelTiltRight,
92        ButtonId::Back,
93        ButtonId::Forward,
94        ButtonId::DpiToggle,
95        ButtonId::Thumbwheel,
96        ButtonId::ThumbwheelScrollUp,
97        ButtonId::ThumbwheelScrollDown,
98        ButtonId::GestureButton,
99        ButtonId::HapticPanel,
100    ];
101
102    /// The divertable keyboard F-row controls, in F-row order. Kept out of
103    /// [`ButtonId::ALL`]: that array seeds mouse defaults and the mouse
104    /// popover trigger list, while keyboard keys stay native unless the user
105    /// binds them (an unbound key is never diverted).
106    pub const KEYBOARD_KEYS: [ButtonId; 9] = [
107        ButtonId::KeySearch,
108        ButtonId::KeyDictation,
109        ButtonId::KeyEmoji,
110        ButtonId::KeyScreenCapture,
111        ButtonId::KeyMicMute,
112        ButtonId::KeyPlayPause,
113        ButtonId::KeyMute,
114        ButtonId::KeyVolumeDown,
115        ButtonId::KeyVolumeUp,
116    ];
117
118    /// Whether this button is one the OS hook (macOS `CGEventTap` / Linux evdev)
119    /// remaps: Middle, Back, or Forward. The primary L/R clicks always pass
120    /// through (suppressing them would brick the mouse), and the DPI / thumb /
121    /// dedicated gesture controls aren't visible to the OS hook at all (they're
122    /// captured over HID++). Stored gesture bindings also project through this
123    /// set so Middle Click configurations written by v0.8.0 keep working;
124    /// [`Self::supports_gesture_mode`] separately controls which buttons may be
125    /// newly promoted by the UI.
126    #[must_use]
127    pub fn is_os_hook_button(self) -> bool {
128        matches!(
129            self,
130            ButtonId::MiddleClick | ButtonId::Back | ButtonId::Forward
131        )
132    }
133
134    /// Whether this button may use the OS hook's hold-and-swipe gesture path.
135    /// Back and Forward are the only eligible controls: Middle Click belongs
136    /// to the main wheel, whose controls intentionally remain single-action.
137    #[must_use]
138    pub fn is_os_hook_gesture_source(self) -> bool {
139        matches!(self, ButtonId::Back | ButtonId::Forward)
140    }
141
142    /// Whether this button is a HID++ gesture source — a control that is
143    /// captured over HID++ raw-XY diversion (never the OS hook) and can
144    /// therefore own the gesture role with swipe directions: DPI/ModeShift,
145    /// the dedicated gesture button, or the MX Master 4 haptic panel. The
146    /// capture layer maps each to its control ID and checks the device's
147    /// advertised raw-XY capability before arming it.
148    #[must_use]
149    pub fn is_hidpp_gesture_source(self) -> bool {
150        matches!(
151            self,
152            ButtonId::DpiToggle | ButtonId::GestureButton | ButtonId::HapticPanel
153        )
154    }
155
156    /// Whether OpenLogi offers gesture mode for this logical control. Wheel
157    /// controls and the primary clicks stay single-action by product policy;
158    /// device-specific HID++ capability checks may further narrow this set at
159    /// capture time.
160    #[must_use]
161    pub fn supports_gesture_mode(self) -> bool {
162        self.is_os_hook_gesture_source() || self.is_hidpp_gesture_source()
163    }
164
165    /// Human-readable label for popovers and tooltips.
166    #[must_use]
167    pub fn label(self) -> &'static str {
168        match self {
169            ButtonId::LeftClick => "Left Click",
170            ButtonId::RightClick => "Right Click",
171            ButtonId::MiddleClick => "Middle Click",
172            ButtonId::WheelTiltLeft => "Tilt Left",
173            ButtonId::WheelTiltRight => "Tilt Right",
174            ButtonId::Back => "Back",
175            ButtonId::Forward => "Forward",
176            ButtonId::DpiToggle => "DPI Toggle",
177            ButtonId::Thumbwheel => "Thumb Wheel",
178            ButtonId::ThumbwheelScrollUp => "Thumb Wheel Up",
179            ButtonId::ThumbwheelScrollDown => "Thumb Wheel Down",
180            ButtonId::GestureButton => "Gesture Button",
181            ButtonId::KeySearch => "Search Key",
182            ButtonId::KeyDictation => "Dictation Key",
183            ButtonId::KeyEmoji => "Emoji Key",
184            ButtonId::KeyScreenCapture => "Screen Capture Key",
185            ButtonId::KeyMicMute => "Mic Mute Key",
186            ButtonId::KeyPlayPause => "Play/Pause Key",
187            ButtonId::KeyMute => "Mute Key",
188            ButtonId::KeyVolumeDown => "Volume Down Key",
189            ButtonId::KeyVolumeUp => "Volume Up Key",
190            ButtonId::HapticPanel => "Haptic Panel",
191        }
192    }
193
194    /// Stable catalog key for the localized button label.
195    #[must_use]
196    pub fn translation_key(self) -> &'static str {
197        match self {
198            ButtonId::LeftClick => "actions.left_click",
199            ButtonId::RightClick => "actions.right_click",
200            ButtonId::MiddleClick => "actions.middle_click",
201            ButtonId::WheelTiltLeft => "actions.tilt_left",
202            ButtonId::WheelTiltRight => "actions.tilt_right",
203            ButtonId::Back => "actions.back",
204            ButtonId::Forward => "actions.forward",
205            ButtonId::DpiToggle => "actions.dpi_toggle",
206            ButtonId::Thumbwheel => "pointer.thumb_wheel",
207            ButtonId::ThumbwheelScrollUp => "pointer.thumb_wheel_up",
208            ButtonId::ThumbwheelScrollDown => "pointer.thumb_wheel_down",
209            ButtonId::GestureButton => "actions.gesture_button",
210            ButtonId::KeySearch => "keyboard.search_key",
211            ButtonId::KeyDictation => "keyboard.dictation_key",
212            ButtonId::KeyEmoji => "keyboard.emoji_key",
213            ButtonId::KeyScreenCapture => "keyboard.screen_capture_key",
214            ButtonId::KeyMicMute => "keyboard.mic_mute_key",
215            ButtonId::KeyPlayPause => "keyboard.play_pause_key",
216            ButtonId::KeyMute => "keyboard.mute_key",
217            ButtonId::KeyVolumeDown => "keyboard.volume_down_key",
218            ButtonId::KeyVolumeUp => "keyboard.volume_up_key",
219            ButtonId::HapticPanel => "actions.haptic_panel",
220        }
221    }
222}
223
224impl fmt::Display for ButtonId {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        f.write_str(self.label())
227    }
228}