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}