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}