Skip to main content

openlogi_core/binding/
defaults.rs

1//! Default bindings for a fresh device / gesture map.
2
3use super::action::Action;
4use super::button::ButtonId;
5use super::gesture::GestureDirection;
6use super::value::Binding;
7
8/// Sensible defaults for a fresh device so the panel isn't empty on first run.
9///
10/// `GestureButton` matches what Logi Options+ ships for MX-line devices:
11/// gesture button → Mission Control, captured per-direction (see
12/// [`default_gesture_binding`]).
13///
14/// `GestureButton`'s entry here is vestigial: in the merged [`Binding`] model
15/// the gesture button defaults to [`Binding::Gesture`] (see
16/// [`default_binding_for`]), so this single-action value is never the source of
17/// truth for it. It is retained only so the per-button-`Action` callers (the
18/// hook map, scroll defaults, labels) stay total.
19///
20/// [`ButtonId::Thumbwheel`] — the wheel's capacitive tap — is deliberately
21/// inert. The mouse model surfaces the wheel as one paired rotation control,
22/// so the tap has no GUI hotspot to discover or clear it, while the firmware
23/// reports a tap from incidental thumb contact (including mid-roll, from the
24/// ridges alone). Seeding it with a real action therefore fired that action
25/// for users who only changed the rotation bindings or the sensitivity — the
26/// two settings that divert the wheel over `0x2150` in the first place. A tap
27/// bound explicitly in the config still dispatches; only the seed is inert.
28#[must_use]
29pub fn default_binding(button: ButtonId) -> Action {
30    match button {
31        ButtonId::LeftClick => Action::LeftClick,
32        ButtonId::RightClick => Action::RightClick,
33        ButtonId::MiddleClick => Action::MiddleClick,
34        ButtonId::Back => Action::BrowserBack,
35        ButtonId::Forward => Action::BrowserForward,
36        ButtonId::DpiToggle => Action::CycleDpiPresets,
37        #[expect(
38            clippy::match_same_arms,
39            reason = "the tap is inert because its captured events are noise (see above), \
40                      not because the control stays native like the keyboard arm below"
41        )]
42        ButtonId::Thumbwheel => Action::None,
43        // The thumb wheel scrolls horizontally by default: rotating it produces
44        // continuous horizontal scroll, with "up" → right and "down" → left.
45        // The wheel watcher renders these two actions as smooth, sensitivity-
46        // scaled scrolling rather than the discrete per-press burst a button
47        // would get (see `watchers::gesture`).
48        ButtonId::ThumbwheelScrollUp => Action::HorizontalScrollRight,
49        ButtonId::ThumbwheelScrollDown => Action::HorizontalScrollLeft,
50        ButtonId::GestureButton => Action::MissionControl,
51        ButtonId::HapticPanel => Action::ShowActionsRing,
52        // Keyboard keys stay on their native firmware function until the user
53        // explicitly binds them; an unbound key is never diverted, so a
54        // `None` default keeps the projection total without capturing anything.
55        ButtonId::KeySearch
56        | ButtonId::KeyDictation
57        | ButtonId::KeyEmoji
58        | ButtonId::KeyScreenCapture
59        | ButtonId::KeyMicMute
60        | ButtonId::KeyPlayPause
61        | ButtonId::KeyMute
62        | ButtonId::KeyVolumeDown
63        | ButtonId::KeyVolumeUp => Action::None,
64    }
65}
66
67/// Per-direction defaults for the gesture button. These are captured live over
68/// HID++ `0x1b04` (raw-XY diversion) and dispatched like any other binding; the
69/// defaults give the picker something sensible to show on first run.
70#[must_use]
71pub fn default_gesture_binding(direction: GestureDirection) -> Action {
72    match direction {
73        GestureDirection::Up => Action::MissionControl,
74        GestureDirection::Down => Action::ShowDesktop,
75        GestureDirection::Left => Action::PrevTab,
76        GestureDirection::Right => Action::NextTab,
77        GestureDirection::Click => Action::AppExpose,
78    }
79}
80
81/// The canonical default [`Binding`] for a fresh button in the merged model.
82///
83/// [`ButtonId::GestureButton`] defaults to [`Binding::Gesture`] populated from
84/// [`default_gesture_binding`] — preserving the existing per-direction swipe
85/// behavior — so the GUI mode toggle and the runtime agree it starts in gesture
86/// mode. Every other button defaults to [`Binding::Single`] of its
87/// [`default_binding`].
88///
89/// This is the seed when a button is first promoted to a gesture binding (see
90/// [`Config::set_gesture_direction`](crate::config::Config::set_gesture_direction)),
91/// so a freshly-customized gesture button always carries a full default
92/// direction map — including a [`GestureDirection::Click`] — rather than a sparse
93/// map whose click would project to a no-op [`Action::None`].
94#[must_use]
95pub fn default_binding_for(button: ButtonId) -> Binding {
96    match button {
97        ButtonId::GestureButton => Binding::Gesture(
98            GestureDirection::ALL
99                .into_iter()
100                .map(|d| (d, default_gesture_binding(d)))
101                .collect(),
102        ),
103        other => Binding::Single(default_binding(other)),
104    }
105}