Skip to main content

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}