openlogi_core/binding/value.rs
1//! Single-action vs per-direction gesture bindings.
2
3use std::collections::BTreeMap;
4use std::time::Duration;
5
6use serde::{Deserialize, Serialize};
7
8use super::action::Action;
9use super::defaults::default_gesture_binding;
10use super::gesture::GestureDirection;
11
12/// How long a physical button must remain down before its independent long
13/// action fires.
14pub const LONG_PRESS_THRESHOLD: Duration = Duration::from_millis(500);
15
16/// The mutually exclusive actions of a threshold-based button binding.
17///
18/// `short` fires only on an ordinary release before the threshold. `long`
19/// fires once when the threshold elapses and suppresses `short` for that press.
20#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct LongPressBinding {
23 short: Action,
24 long: Action,
25}
26
27impl LongPressBinding {
28 /// Pair the release-before-threshold action with the threshold action.
29 #[must_use]
30 pub const fn new(short: Action, long: Action) -> Self {
31 Self { short, long }
32 }
33
34 /// Action fired by a normal release before the threshold.
35 #[must_use]
36 pub const fn short(&self) -> &Action {
37 &self.short
38 }
39
40 /// Action fired once when the threshold is reached.
41 #[must_use]
42 pub const fn long(&self) -> &Action {
43 &self.long
44 }
45}
46
47/// What a single rebindable [`ButtonId`](crate::binding::ButtonId) does: one
48/// immediate [`Action`], an independent short/long action pair, or — for a
49/// raw-XY-capable button placed in gesture mode — a per-[`GestureDirection`]
50/// map (hold + swipe up/down/left/right, or a plain click).
51///
52/// There has only ever been one binding map per device; a gesture binding is
53/// just a binding whose payload is a direction map instead of a single action.
54///
55/// # Serialization
56///
57/// `#[serde(untagged)]`: [`Single`](Binding::Single) serializes exactly as the
58/// bare [`Action`] did before (a string `"BrowserBack"`, or a single-key table
59/// for the payload variants), [`Gesture`](Binding::Gesture) serializes as a
60/// table keyed by [`GestureDirection`] names (`Up`/`Down`/`Left`/`Right`/
61/// `Click`), and [`LongPress`](Binding::LongPress) as the structurally distinct
62/// `{ short = ..., long = ... }` table.
63///
64/// The arms are disambiguated structurally: action variant names and gesture
65/// direction names have zero overlap, while a long press requires both
66/// lowercase `short` and `long` fields and rejects unknown fields. The
67/// `binding_untagged_*` tests guard these routing invariants.
68#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(untagged)]
70pub enum Binding {
71 /// One action, fired on press. The shape every non-gesture button uses.
72 Single(Action),
73 /// Per-direction sub-bindings for a button in gesture mode. Keyed by the
74 /// committed swipe direction, with [`GestureDirection::Click`] holding the
75 /// plain-click (no-swipe) action.
76 Gesture(BTreeMap<GestureDirection, Action>),
77 /// Independent release-before-threshold and threshold actions.
78 LongPress(LongPressBinding),
79}
80
81impl Binding {
82 /// The plain-click action for this binding: the [`Single`](Binding::Single)
83 /// action, the [`Gesture`](Binding::Gesture) map's
84 /// [`Click`](GestureDirection::Click) entry, or a
85 /// [`LongPress`](Binding::LongPress) binding's short action. Falls back to
86 /// [`Action::None`] when a gesture binding has no explicit `Click`.
87 ///
88 /// Lets the click-dispatch path stay binding-shape-agnostic.
89 #[must_use]
90 pub fn click_action(&self) -> Action {
91 match self {
92 Binding::Single(action) => action.clone(),
93 Binding::Gesture(map) => map
94 .get(&GestureDirection::Click)
95 .cloned()
96 .unwrap_or(Action::None),
97 Binding::LongPress(binding) => binding.short().clone(),
98 }
99 }
100
101 /// The action bound to `direction`, if this is a gesture binding.
102 /// [`Single`](Binding::Single) has no directions and returns `None`.
103 #[must_use]
104 pub fn direction_action(&self, direction: GestureDirection) -> Option<&Action> {
105 match self {
106 Binding::Single(_) | Binding::LongPress(_) => None,
107 Binding::Gesture(map) => map.get(&direction),
108 }
109 }
110
111 /// Whether this binding drives raw-XY swipe capture (the
112 /// [`Gesture`](Binding::Gesture) arm).
113 #[must_use]
114 pub fn is_gesture(&self) -> bool {
115 matches!(self, Binding::Gesture(_))
116 }
117
118 /// Promote a [`Single`](Binding::Single) binding in place to a
119 /// [`Gesture`](Binding::Gesture), keeping its action as the
120 /// [`GestureDirection::Click`] entry and leaving the swipe arms unbound.
121 /// A long-press binding keeps its short action as `Click`; its long action
122 /// is discarded because gesture and threshold modes are mutually exclusive.
123 /// A no-op when this is already a [`Gesture`](Binding::Gesture).
124 pub fn upgrade_to_gesture(&mut self) {
125 let click = match self {
126 Binding::Single(action) => action.clone(),
127 Binding::LongPress(binding) => binding.short().clone(),
128 Binding::Gesture(_) => return,
129 };
130 *self = Binding::Gesture(BTreeMap::from([(GestureDirection::Click, click)]));
131 }
132
133 /// Demote a [`Gesture`](Binding::Gesture) binding in place to a
134 /// [`Single`](Binding::Single) of its [`Click`](GestureDirection::Click)
135 /// entry, falling back to `fallback` when the map has no explicit `Click` —
136 /// the inverse of [`Self::upgrade_to_gesture`]. A no-op on a
137 /// [`Single`](Binding::Single) or [`LongPress`](Binding::LongPress).
138 pub fn demote_to_single(&mut self, fallback: Action) {
139 if let Binding::Gesture(map) = self {
140 let click = map
141 .get(&GestureDirection::Click)
142 .cloned()
143 .unwrap_or(fallback);
144 *self = Binding::Single(click);
145 }
146 }
147
148 /// Fill any unbound directions of a [`Gesture`](Binding::Gesture) binding
149 /// with their canonical [`default_gesture_binding`], so a button promoted to
150 /// the gesture role always exposes the full five-direction set — rather than
151 /// leaving swipe arms the GUI renders as defaults but the runtime never
152 /// dispatches. A no-op on [`Single`](Binding::Single) and on directions
153 /// already bound (existing user choices are preserved).
154 pub fn fill_gesture_defaults(&mut self) {
155 if let Binding::Gesture(map) = self {
156 for dir in GestureDirection::ALL {
157 map.entry(dir)
158 .or_insert_with(|| default_gesture_binding(dir));
159 }
160 }
161 }
162}
163
164impl From<Action> for Binding {
165 fn from(action: Action) -> Self {
166 Binding::Single(action)
167 }
168}