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