Skip to main content

smart_keymap_core/key/
chorded.rs

1use core::fmt::Debug;
2use core::marker::PhantomData;
3use core::ops::Index;
4
5use serde::Deserialize;
6
7use crate::{input, key, keymap, slice::Slice};
8
9/// Reference for a chorded key.
10#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
11pub enum Ref {
12    /// Ref for [Key].
13    Chorded(u8),
14    /// Ref for [AuxiliaryKey].
15    Auxiliary(u8),
16}
17
18/// A chord identifier.
19pub type ChordId = u8;
20
21/// Chords are defined by an (unordered) set of keymap indices into the keymap.
22#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
23#[serde(from = "heapless::Vec<u16, MAX_CHORD_SIZE>")]
24pub struct ChordIndices<const MAX_CHORD_SIZE: usize> {
25    /// A slice of keymap indices.
26    indices: Slice<u16, MAX_CHORD_SIZE>,
27}
28
29impl<const MAX_CHORD_SIZE: usize> ChordIndices<MAX_CHORD_SIZE> {
30    /// Constructs a new [ChordIndices] value from the given slice.
31    ///
32    /// The given slice must be less than `MAX_CHORD_SIZE` in length.
33    pub const fn from_slice(indices: &[u16]) -> ChordIndices<MAX_CHORD_SIZE> {
34        ChordIndices {
35            indices: Slice::from_slice(indices),
36        }
37    }
38
39    /// The chord indices as a slice.
40    pub const fn as_slice(&self) -> &[u16] {
41        self.indices.as_slice()
42    }
43
44    /// Whether the given index is part of the chord.
45    pub fn has_index(&self, index: u16) -> bool {
46        self.as_slice().contains(&index)
47    }
48
49    /// Whether the chord is satisfied by the given indices.
50    pub fn is_satisfied_by(&self, indices: &[u16]) -> bool {
51        self.as_slice().iter().all(|&i| indices.contains(&i))
52    }
53}
54
55impl<const MAX_CHORD_SIZE: usize> From<heapless::Vec<u16, MAX_CHORD_SIZE>>
56    for ChordIndices<MAX_CHORD_SIZE>
57{
58    fn from(v: heapless::Vec<u16, MAX_CHORD_SIZE>) -> Self {
59        ChordIndices::from_slice(&v)
60    }
61}
62
63/// Chord definitions.
64#[derive(Deserialize, Clone, Copy, PartialEq)]
65pub struct Config<const MAX_CHORDS: usize, const MAX_CHORD_SIZE: usize> {
66    /// The timeout (in number of milliseconds) for a chorded key to resolve.
67    ///
68    /// (Resolves as passthrough key if no chord is satisfied).
69    ///
70    /// This is the outer decision clock for a chorded position.
71    /// An inner pending key (e.g. tap-hold) then uses time remaining
72    ///  from this same physical press; the two timeouts do not add.
73    #[serde(default = "default_timeout")]
74    pub timeout: u16,
75
76    /// The keymap chords.
77    pub chords: Slice<ChordIndices<MAX_CHORD_SIZE>, MAX_CHORDS>,
78
79    /// Amount of time (in milliseconds) the keymap must have been idle
80    ///  in order for chorded key to activate.
81    ///
82    /// This reduces disruption from unexpected chord resolutions
83    ///  when typing quickly.
84    pub required_idle_time: Option<u16>,
85}
86
87impl<const MAX_CHORDS: usize, const MAX_CHORD_SIZE: usize> core::fmt::Debug
88    for Config<MAX_CHORDS, MAX_CHORD_SIZE>
89{
90    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
91        f.debug_struct("Config")
92            .field("timeout", &self.timeout)
93            .field("chords", &self.chords.as_slice())
94            .field("required_idle_time", &self.required_idle_time)
95            .finish()
96    }
97}
98
99/// The default timeout.
100pub const DEFAULT_TIMEOUT: u16 = 200;
101
102const fn default_timeout() -> u16 {
103    DEFAULT_TIMEOUT
104}
105
106impl<const MAX_CHORDS: usize, const MAX_CHORD_SIZE: usize> Config<MAX_CHORDS, MAX_CHORD_SIZE> {
107    /// Constructs a new config.
108    pub const fn new() -> Self {
109        Self {
110            timeout: DEFAULT_TIMEOUT,
111            chords: Slice::from_slice(&[]),
112            required_idle_time: None,
113        }
114    }
115}
116
117impl<const MAX_CHORDS: usize, const MAX_CHORD_SIZE: usize> Default
118    for Config<MAX_CHORDS, MAX_CHORD_SIZE>
119{
120    /// Returns the default context.
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126/// State for a key chord.
127#[derive(Debug, Clone, PartialEq)]
128pub struct ChordState<const MAX_CHORD_SIZE: usize> {
129    /// The chord index in the chorded config.
130    pub index: usize,
131    /// The chord's indices.
132    pub chord: ChordIndices<MAX_CHORD_SIZE>,
133    /// Whether the chord is satisfied by the pressed indices.
134    pub is_satisfied: bool,
135}
136
137struct PressedIndicesDebugHelper<'a, const MAX_PRESSED_INDICES: usize> {
138    pressed_indices: &'a [Option<u16>; MAX_PRESSED_INDICES],
139}
140
141impl<const MAX_PRESSED_INDICES: usize> core::fmt::Debug
142    for PressedIndicesDebugHelper<'_, MAX_PRESSED_INDICES>
143{
144    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
145        // Reverse-find the last non-empty pressed index to avoid printing large arrays.
146        let last_non_empty_pi_pos = self
147            .pressed_indices
148            .iter()
149            .rposition(|pi| pi.is_some())
150            .map_or(0, |pos| pos + 1);
151        if last_non_empty_pi_pos < MAX_PRESSED_INDICES {
152            f.debug_list()
153                .entries(&self.pressed_indices[..last_non_empty_pi_pos])
154                .finish_non_exhaustive()
155        } else {
156            f.debug_list().entries(&self.pressed_indices[..]).finish()
157        }
158    }
159}
160
161struct PressedChordsDebugHelper<'a, const MAX_CHORDS: usize> {
162    pressed_chords: &'a [bool; MAX_CHORDS],
163}
164
165impl<const MAX_CHORDS: usize> core::fmt::Debug for PressedChordsDebugHelper<'_, MAX_CHORDS> {
166    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
167        // Reverse-find the last true pressed chord to avoid printing large arrays.
168        let last_true_pc_pos = self
169            .pressed_chords
170            .iter()
171            .rposition(|&pc| pc)
172            .map_or(0, |pos| pos + 1);
173        if last_true_pc_pos < MAX_CHORDS {
174            f.debug_list()
175                .entries(&self.pressed_chords[..last_true_pc_pos])
176                .finish_non_exhaustive()
177        } else {
178            f.debug_list().entries(&self.pressed_chords[..]).finish()
179        }
180    }
181}
182
183/// Chord definitions.
184#[derive(Clone, Copy, PartialEq)]
185pub struct Context<
186    const MAX_CHORDS: usize,
187    const MAX_CHORD_SIZE: usize,
188    const MAX_PRESSED_INDICES: usize,
189> {
190    config: Config<MAX_CHORDS, MAX_CHORD_SIZE>,
191    pressed_indices: [Option<u16>; MAX_PRESSED_INDICES],
192    pressed_chords: [bool; MAX_CHORDS],
193    idle_time_ms: u32,
194    ignore_idle_time: bool,
195    latest_resolved_chord: Option<ChordId>,
196    /// Index whose press completed the latest chord; nests the chord `Ref`.
197    activating_index: Option<u16>,
198}
199
200impl<const MAX_CHORDS: usize, const MAX_CHORD_SIZE: usize, const MAX_PRESSED_INDICES: usize> Debug
201    for Context<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>
202{
203    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
204        f.debug_struct("Context")
205            .field("config", &self.config)
206            .field(
207                "pressed_indices",
208                &PressedIndicesDebugHelper {
209                    pressed_indices: &self.pressed_indices,
210                },
211            )
212            .field(
213                "pressed_chords",
214                &PressedChordsDebugHelper {
215                    pressed_chords: &self.pressed_chords,
216                },
217            )
218            .field("idle_time_ms", &self.idle_time_ms)
219            .field("ignore_idle_time", &self.ignore_idle_time)
220            .field("latest_resolved_chord", &self.latest_resolved_chord)
221            .field("activating_index", &self.activating_index)
222            .finish()
223    }
224}
225
226impl<const MAX_CHORDS: usize, const MAX_CHORD_SIZE: usize, const MAX_PRESSED_INDICES: usize>
227    Context<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>
228{
229    /// Constructs a context from the given config
230    pub const fn from_config(config: Config<MAX_CHORDS, MAX_CHORD_SIZE>) -> Self {
231        let pressed_indices = [None; MAX_PRESSED_INDICES];
232        Context {
233            config,
234            pressed_indices,
235            pressed_chords: [false; MAX_CHORDS],
236            idle_time_ms: 0,
237            ignore_idle_time: false,
238            latest_resolved_chord: None,
239            activating_index: None,
240        }
241    }
242
243    /// Re-construct from context's [Config], clearing pressed/chord runtime state.
244    pub fn reset(&mut self) {
245        *self = Self::from_config(self.config);
246    }
247
248    /// Updates the context with the given keymap context.
249    pub fn update_keymap_context(
250        &mut self,
251        keymap::KeymapContext { idle_time_ms, .. }: &keymap::KeymapContext,
252    ) {
253        self.idle_time_ms = *idle_time_ms;
254    }
255
256    /// Whether `keymap_index` completed the latest resolved chord.
257    pub fn is_activator(&self, keymap_index: u16) -> bool {
258        self.activating_index == Some(keymap_index)
259    }
260
261    fn sufficient_idle_time(&self) -> bool {
262        let sufficient_idle_time =
263            self.idle_time_ms >= self.config.required_idle_time.unwrap_or(0) as u32;
264
265        sufficient_idle_time || self.ignore_idle_time
266    }
267
268    fn pressed_chord_with_index(&self, keymap_index: u16) -> Option<ChordState<MAX_CHORD_SIZE>> {
269        self.pressed_chords
270            .iter()
271            .enumerate()
272            .filter_map(|(index, &is_pressed)| {
273                if is_pressed {
274                    Some(ChordState {
275                        index,
276                        chord: self.config.chords[index],
277                        is_satisfied: true,
278                    })
279                } else {
280                    None
281                }
282            })
283            .find(|ChordState { chord, .. }| chord.has_index(keymap_index))
284    }
285
286    // Span of indices of pressed chords.
287    fn pressed_chords_indices_span(&self) -> heapless::Vec<u16, MAX_PRESSED_INDICES> {
288        let mut res: heapless::Vec<u16, MAX_PRESSED_INDICES> = heapless::Vec::new();
289
290        let pressed_chords =
291            self.pressed_chords
292                .iter()
293                .enumerate()
294                .filter_map(|(index, &is_pressed)| {
295                    if is_pressed {
296                        Some(&self.config.chords[index])
297                    } else {
298                        None
299                    }
300                });
301
302        pressed_chords.for_each(|&chord| {
303            for &i in chord.as_slice() {
304                if let Err(pos) = res.binary_search(&i) {
305                    let _ = res.insert(pos, i);
306                }
307            }
308        });
309
310        res
311    }
312
313    /// Returns the chords for the given keymap index.
314    ///
315    /// - If a chord with that index is resolved as active, return a vec with only that chord.
316    /// - Otherwise, return a vec with all the chords which include the keymap index
317    ///   and could be satisfied. (i.e. chords which do not overlap with resolved active chords).
318    pub fn chords_for_keymap_index(
319        &self,
320        keymap_index: u16,
321    ) -> heapless::Vec<ChordState<MAX_CHORD_SIZE>, { MAX_CHORDS }> {
322        match self.pressed_chord_with_index(keymap_index) {
323            Some(chord_state) => {
324                let mut chords = heapless::Vec::new();
325                let _ = chords.push(chord_state);
326                chords
327            }
328            None => {
329                let chords_indices_span = self.pressed_chords_indices_span();
330                self.config
331                    .chords
332                    .iter()
333                    .enumerate()
334                    // filter: satisfiable chords
335                    .filter(|&(_index, chord)| chord.has_index(keymap_index))
336                    .filter(|&(_index, chord)| {
337                        // Filter out chords which overlap with resolved active chords.
338                        chords_indices_span.is_empty()
339                            || chord.indices.iter().all(|&i| {
340                                // The chord index is not part of the pressed chords indices span.
341                                chords_indices_span.binary_search(&i).is_err()
342                            })
343                    })
344                    .map(|(index, &chord)| ChordState {
345                        index,
346                        chord,
347                        is_satisfied: false,
348                    })
349                    .collect()
350            }
351        }
352    }
353
354    fn insert_pressed_index(&mut self, pos: usize, index: u16) {
355        if self.pressed_indices.is_empty() {
356            return;
357        }
358
359        let mut i = self.pressed_indices.len() - 1;
360        while i > pos {
361            self.pressed_indices[i] = self.pressed_indices[i - 1];
362            i -= 1;
363        }
364
365        self.pressed_indices[pos] = Some(index);
366    }
367
368    fn remove_pressed_index(&mut self, pos: usize) {
369        if self.pressed_indices.is_empty() {
370            return;
371        }
372
373        let mut i = pos;
374        while i < self.pressed_indices.len() - 1 {
375            self.pressed_indices[i] = self.pressed_indices[i + 1];
376            i += 1;
377        }
378
379        self.pressed_indices[self.pressed_indices.len() - 1] = None;
380    }
381
382    fn press_index(&mut self, index: u16) {
383        match self
384            .pressed_indices
385            .binary_search_by_key(&index, |&k| k.unwrap_or(u16::MAX))
386        {
387            Ok(_) => {}
388            Err(pos) => self.insert_pressed_index(pos, index),
389        }
390    }
391
392    fn release_index(&mut self, index: u16) {
393        if let Ok(pos) = self
394            .pressed_indices
395            .binary_search_by_key(&index, |&k| k.unwrap_or(u16::MAX))
396        {
397            self.remove_pressed_index(pos)
398        }
399    }
400
401    /// Updates the context for the given key event.
402    fn handle_event(&mut self, event: key::Event<Event>) {
403        match event {
404            key::Event::Input(input::Event::Press { keymap_index }) => {
405                self.press_index(keymap_index);
406
407                // Consider whether the key press supports
408                //  ignoring required idle time for a chorded key,
409                //  or supports quickly re-tapping a chorded key.
410                let span = self.pressed_chords_indices_span();
411                if span.contains(&keymap_index) {
412                    // Key presses of an active chord ignore required idle time.
413                    self.ignore_idle_time = true;
414                } else {
415                    // Otherwise, check against the latest resolved chord.
416                    if let Some(chord_id) = self.latest_resolved_chord {
417                        let chord_indices = self.config.chords[chord_id as usize];
418                        self.ignore_idle_time = chord_indices.has_index(keymap_index);
419                    } else {
420                        self.ignore_idle_time = false;
421
422                        // Chords not active, and this press was outside the latest active chord,
423                        //  so clear the latest resolved chord.
424                        self.latest_resolved_chord = None;
425                    }
426                }
427            }
428            key::Event::Input(input::Event::Release { keymap_index }) => {
429                self.release_index(keymap_index);
430
431                // Ensure every chord which includes this keymap index
432                //  is not marked as 'pressed'.
433                self.config
434                    .chords
435                    .iter()
436                    .enumerate()
437                    .for_each(|(chord_id, chord_indices)| {
438                        if chord_indices.has_index(keymap_index) {
439                            self.pressed_chords[chord_id] = false;
440                        }
441                    });
442                self.activating_index = None;
443            }
444            key::Event::Key {
445                keymap_index: _,
446                key_event: Event::ChordResolved(ChordResolution::Chord(chord_id)),
447            } => {
448                self.pressed_chords[chord_id as usize] = true;
449                self.latest_resolved_chord = Some(chord_id);
450            }
451            key::Event::Key {
452                key_event: Event::ChordActivated { keymap_index },
453                ..
454            } => {
455                self.activating_index = Some(keymap_index);
456            }
457            _ => {}
458        }
459    }
460}
461
462impl<const MAX_CHORDS: usize, const MAX_CHORD_SIZE: usize, const MAX_PRESSED_INDICES: usize>
463    key::Context for Context<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>
464{
465    type Event = Event;
466
467    fn handle_event(&mut self, event: key::Event<Self::Event>) -> key::KeyEvents<Self::Event> {
468        self.handle_event(event);
469        key::KeyEvents::no_events()
470    }
471
472    fn reset(&mut self) {
473        Context::reset(self);
474    }
475}
476
477/// Primary Chorded key (with a passthrough key).
478///
479/// The primary key is the key with the lowest index in the chord,
480///  and has the key used for the resolved chord.
481#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
482pub struct Key<
483    R: Copy,
484    const MAX_CHORDS: usize,
485    const MAX_CHORD_SIZE: usize,
486    const MAX_OVERLAPPING_CHORD_SIZE: usize,
487    const MAX_PRESSED_INDICES: usize,
488> {
489    /// The chorded key
490    pub chords: Slice<(ChordId, R), MAX_OVERLAPPING_CHORD_SIZE>,
491    /// The passthrough key
492    pub passthrough: R,
493    #[serde(default)]
494    marker: PhantomData<(
495        [(); MAX_CHORDS],
496        [(); MAX_CHORD_SIZE],
497        [(); MAX_PRESSED_INDICES],
498    )>,
499}
500
501impl<
502        R: Copy,
503        const MAX_CHORDS: usize,
504        const MAX_CHORD_SIZE: usize,
505        const MAX_OVERLAPPING_CHORD_SIZE: usize,
506        const MAX_PRESSED_INDICES: usize,
507    > Key<R, MAX_CHORDS, MAX_CHORD_SIZE, MAX_OVERLAPPING_CHORD_SIZE, MAX_PRESSED_INDICES>
508{
509    /// Constructs new pressed key.
510    pub fn new_pressed_key(
511        &self,
512        context: &Context<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>,
513        keymap_index: u16,
514        lookup: impl Fn(ChordId) -> Option<R>,
515    ) -> (
516        key::PressedKeyResult<
517            R,
518            PendingKeyState<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>,
519            KeyState,
520        >,
521        key::KeyEvents<Event>,
522    ) {
523        let pks = PendingKeyState::new(context, keymap_index);
524
525        let chord_resolution = if context.sufficient_idle_time() {
526            pks.check_resolution()
527        } else {
528            PendingChordState::Resolved(ChordResolution::Passthrough)
529        };
530
531        if let PendingChordState::Resolved(resolution) = chord_resolution {
532            let maybe_new_key_ref = match resolution {
533                ChordResolution::Chord(resolved_chord_id) => {
534                    if context.is_activator(keymap_index) {
535                        lookup(resolved_chord_id)
536                    } else {
537                        None
538                    }
539                }
540                ChordResolution::Passthrough => Some(self.passthrough),
541            };
542
543            if let Some(new_key_ref) = maybe_new_key_ref {
544                let pkr =
545                    key::PressedKeyResult::NewPressedKey(key::NewPressedKey::key(new_key_ref));
546                let pke = key::KeyEvents::no_events();
547
548                (pkr, pke)
549            } else {
550                let pkr = key::PressedKeyResult::NewPressedKey(key::NewPressedKey::NoOp);
551                let pke = key::KeyEvents::no_events();
552                (pkr, pke)
553            }
554        } else {
555            let pkr = key::PressedKeyResult::Pending(pks);
556
557            let timeout_ev = Event::Timeout;
558            let sch_ev = key::ScheduledEvent::after(
559                context.config.timeout,
560                key::Event::key_event(keymap_index, timeout_ev),
561            );
562            let pke = key::KeyEvents::scheduled_event(sch_ev);
563
564            (pkr, pke)
565        }
566    }
567
568    /// Constructs new chorded key.
569    pub const fn new(chords: &[(ChordId, R)], passthrough: R) -> Self {
570        let chords = Slice::from_slice(chords);
571        Key {
572            chords,
573            passthrough,
574            marker: PhantomData,
575        }
576    }
577
578    /// Bound key for a chord id stored on this primary key, if any.
579    pub fn binding_for(&self, id: ChordId) -> Option<R> {
580        self.chords
581            .iter()
582            .find(|(ch_id, _)| *ch_id == id)
583            .map(|(_, r)| *r)
584    }
585
586    fn update_pending_state(
587        &self,
588        pending_state: &mut PendingKeyState<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>,
589        keymap_index: u16,
590        context: &Context<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>,
591        event: key::Event<Event>,
592        lookup: impl Fn(ChordId) -> Option<R>,
593    ) -> (Option<key::NewPressedKey<R>>, key::KeyEvents<Event>) {
594        let ch_state = pending_state.handle_event(keymap_index, event);
595
596        if let Some(ch_state) = ch_state {
597            let (maybe_new_key_ref, pke) = resolved_chord_npk(
598                keymap_index,
599                context,
600                pending_state,
601                event,
602                ch_state,
603                &lookup,
604                Some(self.passthrough),
605            );
606            (maybe_new_key_ref, pke)
607        } else {
608            (None, key::KeyEvents::no_events())
609        }
610    }
611}
612
613/// Auxiliary chorded key (with a passthrough key).
614///
615/// The auxiliary keys are chorded keys,
616///  but don't store the resolved chord key.
617/// (i.e. After te primary chorded key, the remaining keys
618///  in the chord are defined with auxiliary chorded keys).
619#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
620pub struct AuxiliaryKey<
621    R,
622    const MAX_CHORDS: usize,
623    const MAX_CHORD_SIZE: usize,
624    const MAX_PRESSED_INDICES: usize,
625> {
626    /// The passthrough key
627    pub passthrough: R,
628    #[serde(default)]
629    marker: PhantomData<(
630        [(); MAX_CHORDS],
631        [(); MAX_CHORD_SIZE],
632        [(); MAX_PRESSED_INDICES],
633    )>,
634}
635
636impl<
637        R: Copy,
638        const MAX_CHORDS: usize,
639        const MAX_CHORD_SIZE: usize,
640        const MAX_PRESSED_INDICES: usize,
641    > AuxiliaryKey<R, MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>
642{
643    /// Constructs new pressed key.
644    pub fn new_pressed_key(
645        &self,
646        context: &Context<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>,
647        keymap_index: u16,
648        lookup: impl Fn(ChordId) -> Option<R>,
649    ) -> (
650        key::PressedKeyResult<
651            R,
652            PendingKeyState<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>,
653            KeyState,
654        >,
655        key::KeyEvents<Event>,
656    ) {
657        let pks = PendingKeyState::new(context, keymap_index);
658
659        let chord_resolution = if context.sufficient_idle_time() {
660            pks.check_resolution()
661        } else {
662            PendingChordState::Resolved(ChordResolution::Passthrough)
663        };
664
665        if let PendingChordState::Resolved(resolution) = chord_resolution {
666            match resolution {
667                ChordResolution::Chord(resolved_chord_id) => {
668                    let pkr = if context.is_activator(keymap_index) {
669                        match lookup(resolved_chord_id) {
670                            Some(r) => {
671                                key::PressedKeyResult::NewPressedKey(key::NewPressedKey::key(r))
672                            }
673                            None => key::PressedKeyResult::NewPressedKey(key::NewPressedKey::NoOp),
674                        }
675                    } else {
676                        key::PressedKeyResult::NewPressedKey(key::NewPressedKey::NoOp)
677                    };
678                    let pke = key::KeyEvents::no_events();
679
680                    (pkr, pke)
681                }
682                ChordResolution::Passthrough => {
683                    let new_key_ref = self.passthrough;
684                    let pkr =
685                        key::PressedKeyResult::NewPressedKey(key::NewPressedKey::key(new_key_ref));
686                    let pke = key::KeyEvents::no_events();
687                    (pkr, pke)
688                }
689            }
690        } else {
691            let pkr = key::PressedKeyResult::Pending(pks);
692
693            let timeout_ev = Event::Timeout;
694            let sch_ev = key::ScheduledEvent::after(
695                context.config.timeout,
696                key::Event::key_event(keymap_index, timeout_ev),
697            );
698            let pke = key::KeyEvents::scheduled_event(sch_ev);
699
700            (pkr, pke)
701        }
702    }
703
704    /// Constructs new auxiliary chorded key.
705    pub const fn new(passthrough: R) -> Self {
706        AuxiliaryKey {
707            passthrough,
708            marker: PhantomData,
709        }
710    }
711
712    fn update_pending_state(
713        &self,
714        pending_state: &mut PendingKeyState<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>,
715        keymap_index: u16,
716        context: &Context<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>,
717        event: key::Event<Event>,
718        lookup: impl Fn(ChordId) -> Option<R>,
719    ) -> (Option<key::NewPressedKey<R>>, key::KeyEvents<Event>) {
720        let ch_state = pending_state.handle_event(keymap_index, event);
721        if let Some(ch_state) = ch_state {
722            resolved_chord_npk(
723                keymap_index,
724                context,
725                pending_state,
726                event,
727                ch_state,
728                &lookup,
729                Some(self.passthrough),
730            )
731        } else {
732            (None, key::KeyEvents::no_events())
733        }
734    }
735}
736
737/// Events for chorded keys.
738#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
739pub enum Event {
740    /// The chorded key was resolved.
741    ChordResolved(ChordResolution),
742
743    /// The press at this index completed the chord; it should nest the chord `Ref`.
744    ChordActivated {
745        /// Keymap index of the completing press.
746        keymap_index: u16,
747    },
748
749    /// Timed out waiting for chord to be satisfied.
750    Timeout,
751}
752
753fn activator_index<
754    const MAX_CHORDS: usize,
755    const MAX_CHORD_SIZE: usize,
756    const MAX_PRESSED_INDICES: usize,
757>(
758    event: key::Event<Event>,
759    pending_state: &PendingKeyState<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>,
760    context: &Context<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>,
761    resolving_index: u16,
762    chord_id: ChordId,
763) -> u16 {
764    let chord = context.config.chords.get(chord_id as usize);
765    let in_chord = |i: u16| chord.is_some_and(|c| c.has_index(i));
766    match event {
767        key::Event::Input(input::Event::Press { keymap_index }) if in_chord(keymap_index) => {
768            keymap_index
769        }
770        _ => pending_state
771            .last_foreign_press
772            .filter(|&i| in_chord(i))
773            .unwrap_or(resolving_index),
774    }
775}
776
777fn resolved_chord_npk<
778    R: Copy,
779    const MAX_CHORDS: usize,
780    const MAX_CHORD_SIZE: usize,
781    const MAX_PRESSED_INDICES: usize,
782>(
783    keymap_index: u16,
784    context: &Context<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>,
785    pending_state: &PendingKeyState<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>,
786    event: key::Event<Event>,
787    ch_state: ChordResolution,
788    lookup: impl Fn(ChordId) -> Option<R>,
789    passthrough: Option<R>,
790) -> (Option<key::NewPressedKey<R>>, key::KeyEvents<Event>) {
791    let ch_r_ev = Event::ChordResolved(ch_state);
792    let mut pke = key::KeyEvents::event(key::Event::key_event(keymap_index, ch_r_ev));
793
794    let maybe_new_key_ref = match ch_state {
795        ChordResolution::Chord(chord_id) => {
796            let activator = activator_index(event, pending_state, context, keymap_index, chord_id);
797            pke.add_event(key::Event::key_event(
798                activator,
799                Event::ChordActivated {
800                    keymap_index: activator,
801                },
802            ));
803            if keymap_index == activator {
804                lookup(chord_id)
805            } else {
806                None
807            }
808        }
809        ChordResolution::Passthrough => passthrough,
810    };
811
812    if let Some(new_key_ref) = maybe_new_key_ref {
813        (Some(key::NewPressedKey::key(new_key_ref)), pke)
814    } else {
815        (Some(key::NewPressedKey::no_op()), pke)
816    }
817}
818
819/// Whether the pressed key state has resolved to a chord or not.
820#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
821pub enum ChordResolution {
822    /// Resolved as chord.
823    Chord(ChordId),
824    /// Resolved as passthrough key.
825    Passthrough,
826}
827
828/// The resolution state of a chorded key.
829#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
830pub enum PendingChordState {
831    /// The key state is resolved (as chord or as passthrough).
832    Resolved(ChordResolution),
833    /// The key chord state is pending.
834    ///
835    /// The chord may be pending with the ID of a satisfied chord.
836    Pending(Option<ChordId>),
837}
838
839/// State for pressed keys.
840#[derive(Debug, Clone, PartialEq)]
841pub struct PendingKeyState<
842    const MAX_CHORDS: usize,
843    const MAX_CHORD_SIZE: usize,
844    const MAX_PRESSED_INDICES: usize,
845> {
846    /// The keymap indices which have been pressed while the key is pending.
847    pressed_indices: heapless::Vec<u16, { MAX_CHORD_SIZE }>,
848    /// The chords which this pending key could resolve to.
849    possible_chords: heapless::Vec<ChordState<MAX_CHORD_SIZE>, { MAX_CHORDS }>,
850    /// Most recent other-key press while pending (completes the chord on timeout).
851    last_foreign_press: Option<u16>,
852    marker: PhantomData<[(); MAX_PRESSED_INDICES]>,
853}
854
855impl<const MAX_CHORDS: usize, const MAX_CHORD_SIZE: usize, const MAX_PRESSED_INDICES: usize>
856    PendingKeyState<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>
857{
858    /// Constructs a new [PendingKeyState].
859    pub fn new(
860        context: &Context<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>,
861        keymap_index: u16,
862    ) -> Self {
863        let mut pressed_indices = heapless::Vec::new();
864        let _ = pressed_indices.push(keymap_index);
865        let possible_chords = context.chords_for_keymap_index(keymap_index);
866
867        Self {
868            pressed_indices,
869            possible_chords,
870            last_foreign_press: None,
871            marker: PhantomData,
872        }
873    }
874
875    /// Finds the chord state amongst possible_chords which is satisfied (if it exists).
876    fn satisfied_chord(&self) -> Option<&ChordState<MAX_CHORD_SIZE>> {
877        self.possible_chords
878            .iter()
879            .find(|&ChordState { is_satisfied, .. }| *is_satisfied)
880    }
881
882    fn check_resolution(&self) -> PendingChordState {
883        match self.possible_chords.as_slice() {
884            [ChordState {
885                index,
886                is_satisfied,
887                ..
888            }] if *is_satisfied => {
889                // Only one chord is satisfied by pressed indices.
890                //
891                // This resolves the chord.
892                PendingChordState::Resolved(ChordResolution::Chord(*index as u8))
893            }
894            [] => {
895                // Otherwise, this key state resolves to "Passthrough",
896                //  since it has been interrupted by an unrelated key press.
897                PendingChordState::Resolved(ChordResolution::Passthrough)
898            }
899            satisfiable_chords => {
900                // Overlapping chords.
901                PendingChordState::Pending(
902                    satisfiable_chords
903                        .iter()
904                        .find(|&ChordState { is_satisfied, .. }| *is_satisfied)
905                        .map(|&ChordState { index, .. }| index as u8),
906                )
907            }
908        }
909    }
910
911    /// Handle PKS for primary chorded key.
912    pub fn handle_event(
913        &mut self,
914        keymap_index: u16,
915        event: key::Event<Event>,
916    ) -> Option<ChordResolution> {
917        match event {
918            key::Event::Key {
919                keymap_index: _ev_idx,
920                key_event: Event::Timeout,
921            } => {
922                // Timed out before chord unambiguously resolved.
923                let maybe_satisfied_chord_id = self
924                    .satisfied_chord()
925                    .map(|chord_state| chord_state.index as u8);
926                match maybe_satisfied_chord_id {
927                    Some(satisfied_chord_id) => Some(ChordResolution::Chord(satisfied_chord_id)),
928                    _ => Some(ChordResolution::Passthrough),
929                }
930            }
931            key::Event::Input(input::Event::Press {
932                keymap_index: pressed_keymap_index,
933            }) => {
934                if self
935                    .possible_chords
936                    .iter()
937                    .any(|c| c.chord.has_index(pressed_keymap_index))
938                {
939                    self.last_foreign_press = Some(pressed_keymap_index);
940                }
941
942                // Another key was pressed.
943
944                let maybe_satisfied_chord_id = self
945                    .satisfied_chord()
946                    .map(|chord_state| chord_state.index as u8);
947
948                // Update pressed_indices.
949                let pos = self
950                    .pressed_indices
951                    .binary_search(&keymap_index)
952                    .unwrap_or_else(|e| e);
953                let push_res = self.pressed_indices.insert(pos, pressed_keymap_index);
954                // pressed_indices has capacity of MAX_CHORD_SIZE.
955                // pressed_indices will only be full without resolving
956                // if multiple chords with max chord size
957                //  having the same indices.
958                if push_res.is_err() {
959                    panic!();
960                }
961
962                // Chords only remain possible if they have the pressed keymap index.
963                self.possible_chords
964                    .retain(|chord_state| chord_state.chord.has_index(pressed_keymap_index));
965
966                // Re-evaluate the chord satisfaction states.
967                for chord in self.possible_chords.iter_mut() {
968                    chord.is_satisfied = chord.chord.is_satisfied_by(&self.pressed_indices);
969                }
970
971                let resolution = match self.check_resolution() {
972                    PendingChordState::Resolved(resolution) => Some(resolution),
973                    PendingChordState::Pending(_) => None,
974                };
975
976                // If the chord resolution is now passthrough (i.e. no chords satisfiable),
977                // then resolve the chord with the satisfied chord.
978                match (resolution, maybe_satisfied_chord_id) {
979                    (Some(ChordResolution::Passthrough), Some(satisfied_chord_id)) => {
980                        Some(ChordResolution::Chord(satisfied_chord_id))
981                    }
982                    _ => resolution,
983                }
984            }
985            key::Event::Input(input::Event::Release {
986                keymap_index: released_keymap_index,
987            }) => {
988                if released_keymap_index == keymap_index {
989                    let maybe_satisfied_chord_id = self
990                        .satisfied_chord()
991                        .map(|chord_state| chord_state.index as u8);
992
993                    match maybe_satisfied_chord_id {
994                        Some(satisfied_chord_id) => {
995                            Some(ChordResolution::Chord(satisfied_chord_id))
996                        }
997
998                        // This key state resolves to "Passthrough",
999                        //  since it has been released before any chord is satisfied.
1000                        None => Some(ChordResolution::Passthrough),
1001                    }
1002                } else {
1003                    None
1004                }
1005            }
1006            _ => None,
1007        }
1008    }
1009}
1010
1011/// Key state used by [System]. (Chorded keys do not have a key state).
1012#[derive(Debug, Clone, Copy, PartialEq)]
1013pub struct KeyState;
1014
1015/// The [key::System] implementation for the chorded key system.
1016#[derive(Debug, Clone, Copy, PartialEq)]
1017pub struct System<
1018    R: Copy + Debug + PartialEq,
1019    Keys: Index<
1020        usize,
1021        Output = Key<
1022            R,
1023            MAX_CHORDS,
1024            MAX_CHORD_SIZE,
1025            MAX_OVERLAPPING_CHORD_SIZE,
1026            MAX_PRESSED_INDICES,
1027        >,
1028    >,
1029    AuxiliaryKeys: Index<usize, Output = AuxiliaryKey<R, MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>>,
1030    const MAX_CHORDS: usize,
1031    const MAX_CHORD_SIZE: usize,
1032    const MAX_OVERLAPPING_CHORD_SIZE: usize,
1033    const MAX_PRESSED_INDICES: usize,
1034> {
1035    keys: Keys,
1036    auxiliary_keys: AuxiliaryKeys,
1037}
1038
1039impl<
1040        R: Copy + Debug + PartialEq,
1041        Keys: Index<
1042            usize,
1043            Output = Key<
1044                R,
1045                MAX_CHORDS,
1046                MAX_CHORD_SIZE,
1047                MAX_OVERLAPPING_CHORD_SIZE,
1048                MAX_PRESSED_INDICES,
1049            >,
1050        >,
1051        AuxiliaryKeys: Index<usize, Output = AuxiliaryKey<R, MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>>,
1052        const MAX_CHORDS: usize,
1053        const MAX_CHORD_SIZE: usize,
1054        const MAX_OVERLAPPING_CHORD_SIZE: usize,
1055        const MAX_PRESSED_INDICES: usize,
1056    >
1057    System<
1058        R,
1059        Keys,
1060        AuxiliaryKeys,
1061        MAX_CHORDS,
1062        MAX_CHORD_SIZE,
1063        MAX_OVERLAPPING_CHORD_SIZE,
1064        MAX_PRESSED_INDICES,
1065    >
1066{
1067    /// Constructs a new [System] with the given key data.
1068    pub const fn new(keys: Keys, auxiliary_keys: AuxiliaryKeys) -> Self {
1069        Self {
1070            keys,
1071            auxiliary_keys,
1072        }
1073    }
1074
1075    fn binding_for(&self, id: ChordId) -> Option<R>
1076    where
1077        Keys: AsRef<
1078            [Key<R, MAX_CHORDS, MAX_CHORD_SIZE, MAX_OVERLAPPING_CHORD_SIZE, MAX_PRESSED_INDICES>],
1079        >,
1080    {
1081        self.keys.as_ref().iter().find_map(|k| k.binding_for(id))
1082    }
1083}
1084
1085impl<
1086        R: Copy + Debug + PartialEq,
1087        Keys: Debug
1088            + Index<
1089                usize,
1090                Output = Key<
1091                    R,
1092                    MAX_CHORDS,
1093                    MAX_CHORD_SIZE,
1094                    MAX_OVERLAPPING_CHORD_SIZE,
1095                    MAX_PRESSED_INDICES,
1096                >,
1097            > + AsRef<
1098                [Key<
1099                    R,
1100                    MAX_CHORDS,
1101                    MAX_CHORD_SIZE,
1102                    MAX_OVERLAPPING_CHORD_SIZE,
1103                    MAX_PRESSED_INDICES,
1104                >],
1105            >,
1106        AuxiliaryKeys: Debug
1107            + Index<usize, Output = AuxiliaryKey<R, MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>>,
1108        const MAX_CHORDS: usize,
1109        const MAX_CHORD_SIZE: usize,
1110        const MAX_OVERLAPPING_CHORD_SIZE: usize,
1111        const MAX_PRESSED_INDICES: usize,
1112    > key::System<R>
1113    for System<
1114        R,
1115        Keys,
1116        AuxiliaryKeys,
1117        MAX_CHORDS,
1118        MAX_CHORD_SIZE,
1119        MAX_OVERLAPPING_CHORD_SIZE,
1120        MAX_PRESSED_INDICES,
1121    >
1122{
1123    type Ref = Ref;
1124    type Context = Context<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>;
1125    type Event = Event;
1126    type PendingKeyState = PendingKeyState<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>;
1127    type KeyState = KeyState;
1128
1129    fn new_pressed_key(
1130        &self,
1131        keymap_index: u16,
1132        context: &Self::Context,
1133        key_ref: Ref,
1134    ) -> (
1135        key::PressedKeyResult<R, Self::PendingKeyState, Self::KeyState>,
1136        key::KeyEvents<Self::Event>,
1137    ) {
1138        let lookup = |id| self.binding_for(id);
1139        match key_ref {
1140            Ref::Chorded(i) => self.keys[i as usize].new_pressed_key(context, keymap_index, lookup),
1141            Ref::Auxiliary(i) => {
1142                self.auxiliary_keys[i as usize].new_pressed_key(context, keymap_index, lookup)
1143            }
1144        }
1145    }
1146
1147    fn update_pending_state(
1148        &self,
1149        pending_state: &mut Self::PendingKeyState,
1150        keymap_index: u16,
1151        context: &Self::Context,
1152        key_ref: Ref,
1153        event: key::Event<Self::Event>,
1154    ) -> (Option<key::NewPressedKey<R>>, key::KeyEvents<Self::Event>) {
1155        let lookup = |id| self.binding_for(id);
1156        match key_ref {
1157            Ref::Chorded(i) => self.keys[i as usize].update_pending_state(
1158                pending_state,
1159                keymap_index,
1160                context,
1161                event,
1162                lookup,
1163            ),
1164            Ref::Auxiliary(i) => self.auxiliary_keys[i as usize].update_pending_state(
1165                pending_state,
1166                keymap_index,
1167                context,
1168                event,
1169                lookup,
1170            ),
1171        }
1172    }
1173
1174    fn update_state(
1175        &self,
1176        _key_state: &mut Self::KeyState,
1177        _key_ref: &Self::Ref,
1178        _context: &Self::Context,
1179        _keymap_index: u16,
1180        _event: key::Event<Self::Event>,
1181    ) -> key::KeyEvents<Self::Event> {
1182        panic!()
1183    }
1184
1185    fn key_output(
1186        &self,
1187        _key_ref: &Self::Ref,
1188        _key_state: &Self::KeyState,
1189    ) -> Option<key::KeyOutput> {
1190        panic!()
1191    }
1192}
1193
1194#[cfg(test)]
1195#[allow(clippy::unwrap_used, clippy::expect_used)]
1196mod tests {
1197    use super::*;
1198
1199    use key::keyboard;
1200
1201    const MAX_CHORDS: usize = 4;
1202    const MAX_CHORD_SIZE: usize = 16;
1203    const MAX_PRESSED_INDICES: usize = MAX_CHORD_SIZE * 2;
1204
1205    const DEFAULT_CONTEXT: Context<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES> =
1206        Context::from_config(Config::new());
1207
1208    type AuxiliaryKey =
1209        super::AuxiliaryKey<keyboard::Ref, MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>;
1210    type ChordedKey = super::Key<keyboard::Ref, MAX_CHORDS, MAX_CHORD_SIZE, 4, MAX_PRESSED_INDICES>;
1211    type PendingKeyState = super::PendingKeyState<MAX_CHORDS, MAX_CHORD_SIZE, MAX_PRESSED_INDICES>;
1212
1213    #[test]
1214    fn test_sizeof_ref() {
1215        assert_eq!(2, core::mem::size_of::<Ref>());
1216    }
1217
1218    #[test]
1219    fn test_sizeof_event() {
1220        assert_eq!(4, core::mem::size_of::<Event>());
1221    }
1222
1223    #[test]
1224    fn test_timeout_resolves_unsatisfied_aux_state_as_passthrough_key() {
1225        // Assemble: an Auxilary chorded key, and its PKS.
1226        let context = DEFAULT_CONTEXT;
1227        let expected_ref = keyboard::Ref::KeyCode(0x04);
1228        let _chorded_key = AuxiliaryKey::new(expected_ref);
1229        let keymap_index: u16 = 0;
1230        let mut pks: PendingKeyState = PendingKeyState::new(&context, keymap_index);
1231
1232        // Act: handle a timeout ev.
1233        let timeout_ev = key::Event::key_event(keymap_index, Event::Timeout);
1234        let actual_resolution = pks.handle_event(keymap_index, timeout_ev);
1235
1236        // Assert
1237        let expected_resolution = Some(ChordResolution::Passthrough);
1238        assert_eq!(expected_resolution, actual_resolution);
1239    }
1240
1241    #[test]
1242    fn test_press_non_chorded_key_resolves_aux_state_as_interrupted() {
1243        // Assemble: an Auxilary chorded key, and its PKS.
1244        let context = DEFAULT_CONTEXT;
1245        let expected_ref = keyboard::Ref::KeyCode(0x04);
1246        let _chorded_key = AuxiliaryKey::new(expected_ref);
1247        let keymap_index: u16 = 0;
1248        let mut pks: PendingKeyState = PendingKeyState::new(&context, keymap_index);
1249
1250        // Act: handle a key press, for an index that's not part of any chord.
1251        let non_chord_press = input::Event::Press { keymap_index: 9 }.into();
1252        let actual_resolution = pks.handle_event(keymap_index, non_chord_press);
1253
1254        // Assert
1255        let expected_resolution = Some(ChordResolution::Passthrough);
1256        assert_eq!(expected_resolution, actual_resolution);
1257    }
1258
1259    // "unambiguous" in the sense that the chord
1260    // is not overlapped by another chord.
1261    // e.g. chord "01" is overlapped by chord "012",
1262    //  and "pressed {0, 1}" would be 'ambiguous';
1263    //  wheres "pressed {0, 1, 2}" would be 'unambiguous'.
1264
1265    #[test]
1266    fn test_press_chorded_key_resolves_unambiguous_aux_state_as_chord() {
1267        // Assemble: an Auxilary chorded key, and its PKS, with chord 01.
1268        let mut context = Context::from_config(Config {
1269            chords: Slice::from_slice(&[ChordIndices::from_slice(&[0, 1])]),
1270            ..Config::new()
1271        });
1272        let passthrough = keyboard::Ref::KeyCode(0x04);
1273        let _chorded_key = AuxiliaryKey::new(passthrough);
1274        let keymap_index: u16 = 0;
1275        context.handle_event(key::Event::Input(input::Event::Press { keymap_index: 0 }));
1276        let mut pks: PendingKeyState = PendingKeyState::new(&context, keymap_index);
1277
1278        // Act: handle a key press, for an index that completes (satisfies unambiguously) the chord.
1279        let chord_press = input::Event::Press { keymap_index: 1 }.into();
1280        let actual_resolution = pks.handle_event(keymap_index, chord_press);
1281
1282        // Assert: resolved aux key should have no events, should have (resolved) no output.
1283        let expected_resolution = Some(ChordResolution::Chord(0));
1284        assert_eq!(expected_resolution, actual_resolution);
1285    }
1286
1287    #[test]
1288    fn test_release_pending_aux_state_resolves_as_tapped_key() {
1289        // Assemble: an Auxilary chorded key, and its PKS.
1290        let context = DEFAULT_CONTEXT;
1291        let expected_ref = keyboard::Ref::KeyCode(0x04);
1292        let _chorded_key = AuxiliaryKey::new(expected_ref);
1293        let keymap_index: u16 = 0;
1294        let mut pks: PendingKeyState = PendingKeyState::new(&context, keymap_index);
1295
1296        // Act: handle a key press, for an index that's not part of any chord.
1297        let chorded_key_release = input::Event::Release { keymap_index }.into();
1298        let actual_resolution = pks.handle_event(keymap_index, chorded_key_release);
1299
1300        // Assert
1301        let expected_resolution = Some(ChordResolution::Passthrough);
1302        assert_eq!(expected_resolution, actual_resolution);
1303    }
1304
1305    #[test]
1306    fn primary_pending_press_aux_emits_chord_activated_for_aux() {
1307        // Assemble: primary chorded key pending on index 0, chord [0, 1].
1308        let context = Context::from_config(Config {
1309            chords: Slice::from_slice(&[ChordIndices::from_slice(&[0, 1])]),
1310            ..Config::new()
1311        });
1312        let key = ChordedKey::new(
1313            &[(0, keyboard::Ref::KeyCode(0x06))],
1314            keyboard::Ref::KeyCode(0x04),
1315        );
1316        let mut pks = PendingKeyState::new(&context, 0);
1317        let lookup = |id| key.binding_for(id);
1318
1319        // Act: aux press completes the chord.
1320        let (maybe_npk, pke) = key.update_pending_state(
1321            &mut pks,
1322            0,
1323            &context,
1324            input::Event::Press { keymap_index: 1 }.into(),
1325            lookup,
1326        );
1327
1328        // Assert: pending primary is NoOp; activator is the aux index.
1329        assert_eq!(maybe_npk, Some(key::NewPressedKey::no_op()));
1330        let activated = pke.into_iter().any(|sch_ev| {
1331            matches!(
1332                sch_ev.event,
1333                key::Event::Key {
1334                    key_event: Event::ChordActivated { keymap_index: 1 },
1335                    ..
1336                }
1337            )
1338        });
1339        assert!(activated);
1340    }
1341
1342    #[test]
1343    fn aux_pending_press_primary_emits_chord_activated_for_primary() {
1344        // Assemble: aux chorded key pending on index 1, chord [0, 1].
1345        let context = Context::from_config(Config {
1346            chords: Slice::from_slice(&[ChordIndices::from_slice(&[0, 1])]),
1347            ..Config::new()
1348        });
1349        let key = AuxiliaryKey::new(keyboard::Ref::KeyCode(0x05));
1350        let mut pks = PendingKeyState::new(&context, 1);
1351        let lookup = |_id: ChordId| Some(keyboard::Ref::KeyCode(0x06));
1352
1353        // Act: primary press completes the chord.
1354        let (maybe_npk, pke) = key.update_pending_state(
1355            &mut pks,
1356            1,
1357            &context,
1358            input::Event::Press { keymap_index: 0 }.into(),
1359            lookup,
1360        );
1361
1362        // Assert: pending aux is NoOp; activator is the primary index.
1363        assert_eq!(maybe_npk, Some(key::NewPressedKey::no_op()));
1364        let activated = pke.into_iter().any(|sch_ev| {
1365            matches!(
1366                sch_ev.event,
1367                key::Event::Key {
1368                    key_event: Event::ChordActivated { keymap_index: 0 },
1369                    ..
1370                }
1371            )
1372        });
1373        assert!(activated);
1374    }
1375}