Skip to main content

kbd/
layer.rs

1//! Layers — named, stackable collections of bindings.
2//!
3//! Layers are the organizational unit. When active, a layer's bindings
4//! participate in matching. Layers stack: most recently activated is
5//! checked first. Global bindings act as an always-active base layer.
6//!
7//! [`LayerName`] is the identifier type used everywhere a layer is
8//! referenced — in [`Action::PushLayer`],
9//! in the dispatcher's stack, and in introspection snapshots.
10//!
11//! [`Layer`] is a builder — construct with `Layer::new("name")`, add bindings
12//! with `.bind()`, configure with `.oneshot()` / `.swallow()` / `.timeout()`,
13//! then hand to [`Dispatcher::define_layer`](crate::dispatcher::Dispatcher::define_layer).
14
15use std::time::Duration;
16
17use crate::action::Action;
18use crate::binding::KeyPropagation;
19use crate::hotkey::Hotkey;
20
21/// Layer identifier.
22///
23/// Used by layer-control actions ([`Action::PushLayer`],
24/// [`Action::ToggleLayer`]), the dispatcher's
25/// layer stack, and introspection snapshots.
26///
27/// Converts from `&str` and `String` for convenience.
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30#[cfg_attr(feature = "serde", serde(transparent))]
31pub struct LayerName(Box<str>);
32
33impl LayerName {
34    /// Create a new layer name.
35    #[must_use]
36    pub fn new(value: impl Into<Box<str>>) -> Self {
37        Self(value.into())
38    }
39
40    /// Return the name as a string slice.
41    #[must_use]
42    pub fn as_str(&self) -> &str {
43        &self.0
44    }
45}
46
47impl From<&str> for LayerName {
48    fn from(value: &str) -> Self {
49        Self::new(value)
50    }
51}
52
53impl From<String> for LayerName {
54    fn from(value: String) -> Self {
55        Self::new(value)
56    }
57}
58
59impl std::fmt::Display for LayerName {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.write_str(self.as_str())
62    }
63}
64
65/// Whether unmatched keys in an active layer fall through to lower layers.
66///
67/// # Examples
68///
69/// ```
70/// use kbd::action::Action;
71/// use kbd::key::Key;
72/// use kbd::layer::{Layer, UnmatchedKeys};
73///
74/// // A navigation layer that only captures H/J/K/L.
75/// // Other keys (like Ctrl+S) still reach global bindings.
76/// let nav = Layer::new("nav")
77///     .bind(Key::H, Action::Suppress)
78///     .bind(Key::J, Action::Suppress);
79/// assert_eq!(nav.options().unmatched(), UnmatchedKeys::Fallthrough);
80///
81/// // A modal layer that captures ALL keys — nothing falls through.
82/// // Useful for insert-mode or game-input modes.
83/// let modal = Layer::new("modal")
84///     .bind(Key::H, Action::Suppress)
85///     .swallow();
86/// assert_eq!(modal.options().unmatched(), UnmatchedKeys::Swallow);
87/// ```
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
89#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
90#[non_exhaustive]
91pub enum UnmatchedKeys {
92    /// Unmatched keys pass to the next layer down the stack.
93    #[default]
94    Fallthrough,
95    /// Unmatched keys are consumed (swallowed) by this layer.
96    Swallow,
97}
98
99/// Per-layer behavioral options.
100#[derive(Debug, Clone, PartialEq, Eq, Default)]
101#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
102pub struct LayerOptions {
103    /// If set, automatically pop the layer after this many keypresses.
104    oneshot: Option<usize>,
105    /// Whether unmatched keys are consumed or fall through.
106    unmatched: UnmatchedKeys,
107    /// If set, automatically pop the layer after this duration of inactivity.
108    timeout: Option<Duration>,
109    /// Human-readable label for this layer, used for overlay grouping.
110    description: Option<Box<str>>,
111}
112
113impl LayerOptions {
114    /// If set, automatically pop the layer after this many keypresses.
115    #[must_use]
116    pub const fn oneshot(&self) -> Option<usize> {
117        self.oneshot
118    }
119
120    /// Whether unmatched keys are consumed or fall through.
121    #[must_use]
122    pub const fn unmatched(&self) -> UnmatchedKeys {
123        self.unmatched
124    }
125
126    /// If set, automatically pop the layer after this duration of inactivity.
127    #[must_use]
128    pub const fn timeout(&self) -> Option<Duration> {
129        self.timeout
130    }
131
132    /// Human-readable label for this layer, used for overlay grouping.
133    #[must_use]
134    pub fn description(&self) -> Option<&str> {
135        self.description.as_deref()
136    }
137
138    /// Set unmatched key behavior.
139    #[must_use]
140    pub const fn with_unmatched(mut self, behavior: UnmatchedKeys) -> Self {
141        self.unmatched = behavior;
142        self
143    }
144}
145
146/// A single binding within a layer.
147#[derive(Debug)]
148pub(crate) struct LayerBinding {
149    pub(crate) hotkey: Hotkey,
150    pub(crate) action: Action,
151    pub(crate) propagation: KeyPropagation,
152}
153
154/// Engine-internal representation of a stored layer definition.
155pub(crate) struct StoredLayer {
156    pub(crate) bindings: Vec<LayerBinding>,
157    pub(crate) options: LayerOptions,
158}
159
160impl std::fmt::Debug for StoredLayer {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        f.debug_struct("StoredLayer")
163            .field("bindings", &self.bindings.len())
164            .field("options", &self.options)
165            .finish()
166    }
167}
168
169/// A named collection of bindings that can be activated and deactivated.
170///
171/// Construct via the builder pattern, then register with
172/// [`Dispatcher::define_layer`](crate::dispatcher::Dispatcher::define_layer).
173///
174/// # Examples
175///
176/// Basic layer with vim-style navigation:
177///
178/// ```
179/// use kbd::action::Action;
180/// use kbd::key::Key;
181/// use kbd::layer::Layer;
182///
183/// let nav = Layer::new("nav")
184///     .bind(Key::H, Action::Suppress)
185///     .bind(Key::J, Action::Suppress)
186///     .bind(Key::K, Action::Suppress)
187///     .bind(Key::L, Action::Suppress)
188///     .description("Vim navigation keys")
189///     .swallow();
190///
191/// assert_eq!(nav.name().as_str(), "nav");
192/// assert_eq!(nav.binding_count(), 4);
193/// ```
194///
195/// Oneshot layer that auto-pops after one keypress:
196///
197/// ```
198/// use kbd::action::Action;
199/// use kbd::key::Key;
200/// use kbd::layer::Layer;
201///
202/// let leader = Layer::new("leader")
203///     .bind(Key::F, Action::Suppress)
204///     .bind(Key::B, Action::Suppress)
205///     .oneshot(1);
206/// ```
207///
208/// Layer with a timeout that auto-pops after inactivity:
209///
210/// ```
211/// use std::time::Duration;
212/// use kbd::action::Action;
213/// use kbd::key::Key;
214/// use kbd::layer::Layer;
215///
216/// let timed = Layer::new("quick-nav")
217///     .bind(Key::N, Action::Suppress)
218///     .bind(Key::P, Action::Suppress)
219///     .timeout(Duration::from_secs(2));
220/// ```
221pub struct Layer {
222    name: LayerName,
223    bindings: Vec<LayerBinding>,
224    options: LayerOptions,
225}
226
227impl Layer {
228    /// Create a new layer with the given name.
229    #[must_use]
230    pub fn new(name: impl Into<LayerName>) -> Self {
231        Self {
232            name: name.into(),
233            bindings: Vec::new(),
234            options: LayerOptions::default(),
235        }
236    }
237
238    /// Add a binding to this layer.
239    #[must_use]
240    pub fn bind(mut self, hotkey: impl Into<Hotkey>, action: impl Into<Action>) -> Self {
241        self.bindings.push(LayerBinding {
242            hotkey: hotkey.into(),
243            action: action.into(),
244            propagation: KeyPropagation::default(),
245        });
246        self
247    }
248
249    /// Set the layer to swallow unmatched keys (consume instead of fallthrough).
250    #[must_use]
251    pub fn swallow(mut self) -> Self {
252        self.options.unmatched = UnmatchedKeys::Swallow;
253        self
254    }
255
256    /// Set the layer to auto-pop after `depth` keypresses (oneshot mode).
257    #[must_use]
258    pub fn oneshot(mut self, depth: usize) -> Self {
259        self.options.oneshot = Some(depth);
260        self
261    }
262
263    /// Set the layer to auto-pop after `duration` of inactivity.
264    #[must_use]
265    pub fn timeout(mut self, duration: Duration) -> Self {
266        self.options.timeout = Some(duration);
267        self
268    }
269
270    /// Set a human-readable description for this layer.
271    ///
272    /// Used for overlay grouping and help screen display.
273    #[must_use]
274    pub fn description(mut self, description: impl Into<Box<str>>) -> Self {
275        self.options.description = Some(description.into());
276        self
277    }
278
279    /// The layer's name.
280    #[must_use]
281    pub fn name(&self) -> &LayerName {
282        &self.name
283    }
284
285    /// The layer's options.
286    #[must_use]
287    pub fn options(&self) -> &LayerOptions {
288        &self.options
289    }
290
291    /// The number of bindings in this layer.
292    #[must_use]
293    pub fn binding_count(&self) -> usize {
294        self.bindings.len()
295    }
296
297    /// Consume this layer and return its constituent parts.
298    #[must_use]
299    pub(crate) fn into_parts(self) -> (LayerName, Vec<LayerBinding>, LayerOptions) {
300        (self.name, self.bindings, self.options)
301    }
302}
303
304impl std::fmt::Debug for Layer {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        f.debug_struct("Layer")
307            .field("name", &self.name)
308            .field("bindings", &self.bindings.len())
309            .field("options", &self.options)
310            .finish()
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use std::time::Duration;
317
318    use super::*;
319    use crate::action::Action;
320    use crate::hotkey::Modifier;
321    use crate::key::Key;
322
323    #[test]
324    fn layer_new_creates_with_name() {
325        let layer = Layer::new("nav");
326        assert_eq!(layer.name().as_str(), "nav");
327    }
328
329    #[test]
330    fn layer_new_has_empty_bindings() {
331        let layer = Layer::new("test");
332        assert_eq!(layer.binding_count(), 0);
333    }
334
335    #[test]
336    fn layer_new_has_default_options() {
337        let layer = Layer::new("test");
338        assert_eq!(*layer.options(), LayerOptions::default());
339    }
340
341    #[test]
342    fn layer_bind_adds_binding() {
343        let layer = Layer::new("nav").bind(Key::H, Action::Suppress);
344        assert_eq!(layer.binding_count(), 1);
345    }
346
347    #[test]
348    fn layer_bind_multiple_bindings() {
349        let layer = Layer::new("nav")
350            .bind(Key::H, Action::Suppress)
351            .bind(Key::J, Action::Suppress)
352            .bind(Key::K, Action::Suppress)
353            .bind(Key::L, Action::Suppress);
354        assert_eq!(layer.binding_count(), 4);
355    }
356
357    #[test]
358    fn layer_bind_preserves_hotkey() {
359        let layer = Layer::new("nav").bind(
360            Hotkey::new(Key::H).modifier(Modifier::Ctrl),
361            Action::Suppress,
362        );
363        let (_, bindings, _) = layer.into_parts();
364        assert_eq!(bindings.len(), 1);
365        assert_eq!(bindings[0].hotkey.key(), Key::H);
366        assert_eq!(bindings[0].hotkey.modifiers(), &[Modifier::Ctrl]);
367    }
368
369    #[test]
370    fn layer_bind_accepts_closure() {
371        let layer = Layer::new("test").bind(Key::A, || println!("fired"));
372        assert_eq!(layer.binding_count(), 1);
373    }
374
375    #[test]
376    fn layer_swallow_sets_option() {
377        let layer = Layer::new("test").swallow();
378        assert_eq!(layer.options().unmatched(), UnmatchedKeys::Swallow);
379    }
380
381    #[test]
382    fn layer_oneshot_sets_depth() {
383        let layer = Layer::new("test").oneshot(3);
384        assert_eq!(layer.options().oneshot(), Some(3));
385    }
386
387    #[test]
388    fn layer_timeout_sets_duration() {
389        let duration = Duration::from_secs(5);
390        let layer = Layer::new("test").timeout(duration);
391        assert_eq!(layer.options().timeout(), Some(duration));
392    }
393
394    #[test]
395    fn layer_builder_chains_all_options() {
396        let layer = Layer::new("nav")
397            .bind(Key::H, Action::Suppress)
398            .bind(Key::J, Action::Suppress)
399            .description("Navigation keys")
400            .swallow()
401            .oneshot(1)
402            .timeout(Duration::from_millis(500));
403
404        assert_eq!(layer.name().as_str(), "nav");
405        assert_eq!(layer.binding_count(), 2);
406        assert_eq!(layer.options().description(), Some("Navigation keys"));
407        assert_eq!(layer.options().unmatched(), UnmatchedKeys::Swallow);
408        assert_eq!(layer.options().oneshot(), Some(1));
409        assert_eq!(layer.options().timeout(), Some(Duration::from_millis(500)));
410    }
411
412    #[test]
413    fn layer_options_default_is_fallthrough_no_oneshot_no_timeout_no_description() {
414        let options = LayerOptions::default();
415        assert_eq!(options.oneshot(), None);
416        assert_eq!(options.unmatched(), UnmatchedKeys::Fallthrough);
417        assert_eq!(options.timeout(), None);
418        assert_eq!(options.description(), None);
419    }
420
421    #[test]
422    fn layer_name_from_string() {
423        let layer = Layer::new(String::from("dynamic"));
424        assert_eq!(layer.name().as_str(), "dynamic");
425    }
426
427    #[test]
428    fn layer_into_parts_decomposes() {
429        let layer = Layer::new("nav").bind(Key::H, Action::Suppress).swallow();
430
431        let (name, bindings, options) = layer.into_parts();
432        assert_eq!(name.as_str(), "nav");
433        assert_eq!(bindings.len(), 1);
434        assert_eq!(options.unmatched(), UnmatchedKeys::Swallow);
435    }
436
437    #[test]
438    fn layer_description_sets_label() {
439        let layer = Layer::new("nav").description("Navigation keys");
440        assert_eq!(layer.options().description(), Some("Navigation keys"));
441    }
442
443    #[test]
444    fn layer_description_preserved_in_into_parts() {
445        let layer = Layer::new("nav")
446            .bind(Key::H, Action::Suppress)
447            .description("Navigation keys");
448
449        let (_, _, options) = layer.into_parts();
450        assert_eq!(options.description(), Some("Navigation keys"));
451    }
452}