Skip to main content

openlogi_core/binding/
value.rs

1//! Single-action vs per-direction gesture bindings.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use super::action::Action;
8use super::defaults::default_gesture_binding;
9use super::gesture::GestureDirection;
10
11/// What a single rebindable [`ButtonId`](crate::binding::ButtonId) does: either
12/// one [`Action`], or — for a raw-XY-capable button placed in gesture mode — a
13/// per-[`GestureDirection`] map (hold + swipe up/down/left/right, or a plain
14/// click).
15///
16/// There has only ever been one binding map per device; a gesture binding is
17/// just a binding whose payload is a direction map instead of a single action.
18///
19/// # Serialization
20///
21/// `#[serde(untagged)]`: [`Single`](Binding::Single) serializes exactly as the
22/// bare [`Action`] did before (a string `"BrowserBack"`, or a single-key table
23/// for the payload variants), and [`Gesture`](Binding::Gesture) serializes as a
24/// table keyed by [`GestureDirection`] names (`Up`/`Down`/`Left`/`Right`/
25/// `Click`).
26///
27/// The two arms are disambiguated by the **zero overlap** between [`Action`]
28/// variant names and [`GestureDirection`] variant names — untagged tries
29/// `Single(Action)` first, and a table keyed by `Up` etc. cannot parse as an
30/// externally-tagged `Action`, so it falls through to `Gesture`. A payload
31/// action like `{ SetDpiPreset = 2 }` is a valid externally-tagged `Action`, so
32/// it stays `Single` and never reaches the `Gesture` arm. This invariant is the
33/// entire safety basis for untagged routing; the `binding_untagged_*` tests
34/// guard it (a future `Action` named `Up`/`Down`/`Left`/`Right`/`Click` would
35/// silently mis-route, and those tests would fail).
36#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(untagged)]
38pub enum Binding {
39    /// One action, fired on press. The shape every non-gesture button uses.
40    Single(Action),
41    /// Per-direction sub-bindings for a button in gesture mode. Keyed by the
42    /// committed swipe direction, with [`GestureDirection::Click`] holding the
43    /// plain-click (no-swipe) action.
44    Gesture(BTreeMap<GestureDirection, Action>),
45}
46
47impl Binding {
48    /// The plain-click action for this binding: the [`Single`](Binding::Single)
49    /// action, or the [`Gesture`](Binding::Gesture) map's
50    /// [`Click`](GestureDirection::Click) entry. Falls back to [`Action::None`]
51    /// when a gesture binding has no explicit `Click`.
52    ///
53    /// Lets the click-dispatch path stay binding-shape-agnostic.
54    #[must_use]
55    pub fn click_action(&self) -> Action {
56        match self {
57            Binding::Single(action) => action.clone(),
58            Binding::Gesture(map) => map
59                .get(&GestureDirection::Click)
60                .cloned()
61                .unwrap_or(Action::None),
62        }
63    }
64
65    /// The action bound to `direction`, if this is a gesture binding.
66    /// [`Single`](Binding::Single) has no directions and returns `None`.
67    #[must_use]
68    pub fn direction_action(&self, direction: GestureDirection) -> Option<&Action> {
69        match self {
70            Binding::Single(_) => None,
71            Binding::Gesture(map) => map.get(&direction),
72        }
73    }
74
75    /// Whether this binding drives raw-XY swipe capture (the
76    /// [`Gesture`](Binding::Gesture) arm).
77    #[must_use]
78    pub fn is_gesture(&self) -> bool {
79        matches!(self, Binding::Gesture(_))
80    }
81
82    /// Promote a [`Single`](Binding::Single) binding in place to a
83    /// [`Gesture`](Binding::Gesture), keeping its action as the
84    /// [`GestureDirection::Click`] entry and leaving the swipe arms unbound.
85    /// A no-op when this is already a [`Gesture`](Binding::Gesture).
86    pub fn upgrade_to_gesture(&mut self) {
87        if let Binding::Single(action) = self {
88            let mut map = BTreeMap::new();
89            map.insert(GestureDirection::Click, action.clone());
90            *self = Binding::Gesture(map);
91        }
92    }
93
94    /// Demote a [`Gesture`](Binding::Gesture) binding in place to a
95    /// [`Single`](Binding::Single) of its [`Click`](GestureDirection::Click)
96    /// entry, falling back to `fallback` when the map has no explicit `Click` —
97    /// the inverse of [`Self::upgrade_to_gesture`]. A no-op on a
98    /// [`Single`](Binding::Single).
99    pub fn demote_to_single(&mut self, fallback: Action) {
100        if let Binding::Gesture(map) = self {
101            let click = map
102                .get(&GestureDirection::Click)
103                .cloned()
104                .unwrap_or(fallback);
105            *self = Binding::Single(click);
106        }
107    }
108
109    /// Fill any unbound directions of a [`Gesture`](Binding::Gesture) binding
110    /// with their canonical [`default_gesture_binding`], so a button promoted to
111    /// the gesture role always exposes the full five-direction set — rather than
112    /// leaving swipe arms the GUI renders as defaults but the runtime never
113    /// dispatches. A no-op on [`Single`](Binding::Single) and on directions
114    /// already bound (existing user choices are preserved).
115    pub fn fill_gesture_defaults(&mut self) {
116        if let Binding::Gesture(map) = self {
117            for dir in GestureDirection::ALL {
118                map.entry(dir)
119                    .or_insert_with(|| default_gesture_binding(dir));
120            }
121        }
122    }
123}
124
125impl From<Action> for Binding {
126    fn from(action: Action) -> Self {
127        Binding::Single(action)
128    }
129}