Skip to main content

fret_chart/
input_map.rs

1use fret_core::{Modifiers, MouseButton};
2
3#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
4pub struct ModifiersMask {
5    pub shift: bool,
6    pub ctrl: bool,
7    pub alt: bool,
8    pub alt_gr: bool,
9    pub meta: bool,
10}
11
12impl ModifiersMask {
13    pub const NONE: Self = Self {
14        shift: false,
15        ctrl: false,
16        alt: false,
17        alt_gr: false,
18        meta: false,
19    };
20
21    pub fn matches(self, modifiers: Modifiers, allow_extra: bool) -> bool {
22        let required = self;
23        if required.shift && !modifiers.shift {
24            return false;
25        }
26        if required.ctrl && !modifiers.ctrl {
27            return false;
28        }
29        if required.alt && !modifiers.alt {
30            return false;
31        }
32        if required.alt_gr && !modifiers.alt_gr {
33            return false;
34        }
35        if required.meta && !modifiers.meta {
36            return false;
37        }
38
39        if allow_extra {
40            return true;
41        }
42
43        if modifiers.shift != required.shift {
44            return false;
45        }
46        if modifiers.ctrl != required.ctrl {
47            return false;
48        }
49        if modifiers.alt != required.alt {
50            return false;
51        }
52        if modifiers.alt_gr != required.alt_gr {
53            return false;
54        }
55        if modifiers.meta != required.meta {
56            return false;
57        }
58
59        true
60    }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum ModifierKey {
65    Shift,
66    Ctrl,
67    Alt,
68    AltGr,
69    Meta,
70}
71
72impl ModifierKey {
73    pub fn is_pressed(self, modifiers: Modifiers) -> bool {
74        match self {
75            Self::Shift => modifiers.shift,
76            Self::Ctrl => modifiers.ctrl,
77            Self::Alt => modifiers.alt,
78            Self::AltGr => modifiers.alt_gr,
79            Self::Meta => modifiers.meta,
80        }
81    }
82
83    pub fn is_required_by(self, required: ModifiersMask) -> bool {
84        match self {
85            Self::Shift => required.shift,
86            Self::Ctrl => required.ctrl,
87            Self::Alt => required.alt,
88            Self::AltGr => required.alt_gr,
89            Self::Meta => required.meta,
90        }
91    }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub struct PointerChord {
96    pub button: MouseButton,
97    pub modifiers: ModifiersMask,
98    pub allow_extra_modifiers: bool,
99}
100
101impl PointerChord {
102    pub const fn new(button: MouseButton, modifiers: ModifiersMask) -> Self {
103        Self {
104            button,
105            modifiers,
106            allow_extra_modifiers: false,
107        }
108    }
109
110    pub fn matches(self, button: MouseButton, modifiers: Modifiers) -> bool {
111        if self.button != button {
112            return false;
113        }
114        self.modifiers
115            .matches(modifiers, self.allow_extra_modifiers)
116    }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub struct ChartInputMap {
121    pub pan: PointerChord,
122    pub box_zoom: PointerChord,
123    pub box_zoom_alt: Option<PointerChord>,
124    pub box_zoom_cancel: Option<PointerChord>,
125    pub brush_select: PointerChord,
126    pub box_zoom_expand_x: Option<ModifierKey>,
127    pub box_zoom_expand_y: Option<ModifierKey>,
128    pub wheel_zoom_mod: Option<ModifierKey>,
129    pub axis_lock_toggle: PointerChord,
130}
131
132impl Default for ChartInputMap {
133    fn default() -> Self {
134        // Defaults are aligned with ImPlot's core interactions:
135        // - LMB drag: pan
136        // - RMB drag: box zoom
137        // - Alt + RMB drag: brush select (persistent selection window; does not zoom)
138        // plus an accessibility alternative:
139        // - Shift + LMB drag: box zoom
140        // and Ctrl + LMB: axis lock toggle.
141        Self {
142            pan: PointerChord::new(MouseButton::Left, ModifiersMask::NONE),
143            box_zoom: PointerChord::new(MouseButton::Right, ModifiersMask::NONE),
144            box_zoom_alt: Some(PointerChord::new(
145                MouseButton::Left,
146                ModifiersMask {
147                    shift: true,
148                    ..ModifiersMask::NONE
149                },
150            )),
151            box_zoom_cancel: Some(PointerChord::new(MouseButton::Left, ModifiersMask::NONE)),
152            brush_select: PointerChord::new(
153                MouseButton::Right,
154                ModifiersMask {
155                    alt: true,
156                    ..ModifiersMask::NONE
157                },
158            ),
159            box_zoom_expand_x: Some(ModifierKey::Alt),
160            box_zoom_expand_y: Some(ModifierKey::Shift),
161            wheel_zoom_mod: None,
162            axis_lock_toggle: PointerChord::new(
163                MouseButton::Left,
164                ModifiersMask {
165                    ctrl: true,
166                    ..ModifiersMask::NONE
167                },
168            ),
169        }
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn modifiers_mask_exact_vs_allow_extra() {
179        let required = ModifiersMask {
180            shift: true,
181            ..ModifiersMask::NONE
182        };
183        let mods = Modifiers {
184            shift: true,
185            ctrl: true,
186            ..Modifiers::default()
187        };
188        assert!(!required.matches(mods, false));
189        assert!(required.matches(mods, true));
190    }
191}