saudade 0.5.0

Classic looking retained-mode, cross-platform Rust GUI library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
//! Keyboard accelerators for menu items.
//!
//! An [`Accel`] names a chord in *platform-independent* terms: its modifiers
//! are roles ([`AccelMods`]) rather than physical keys, resolved through a
//! [`ModifierScheme`] both when the chord is matched against keyboard input
//! and when its label is rendered. An application declares
//! `Accel::primary('r')` once; on a Mac that means ⌘R (displayed "Cmd+R"),
//! everywhere else Ctrl+R (displayed "Ctrl+R").
//!
//! The role → key mapping is chosen so that translation between platforms is
//! *injective* — no two distinct chords on one platform collapse into the same
//! chord on another:
//!
//! | role        | macOS         | elsewhere       |
//! |-------------|---------------|-----------------|
//! | `primary`   | ⌘ Command     | Ctrl            |
//! | `secondary` | ⌃ Control     | Super (Win key) |
//! | `alt`       | ⌥ Option      | Alt             |
//! | `shift`     | Shift         | Shift           |
//!
//! `secondary` is best-effort outside macOS: the OS or compositor claims many
//! Super chords for itself (Win+X on Windows, window management on Wayland),
//! so reserve it for rare power chords that also carry `primary` — the way
//! macOS uses ⌃⌘.

use crate::event::{Key, Modifiers, NamedKey};

/// How the abstract modifier roles of an [`Accel`] map onto the physical
/// modifier keys of [`Modifiers`]. See the [module docs](self) for the table.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ModifierScheme {
    /// `primary` is the Command (logo) key, `secondary` the Control key.
    Mac,
    /// `primary` is the Control key, `secondary` the Super / Windows (logo)
    /// key. Used everywhere but macOS.
    Pc,
}

impl ModifierScheme {
    /// The scheme matching the platform this binary was built for.
    pub fn native() -> Self {
        if cfg!(target_os = "macos") {
            ModifierScheme::Mac
        } else {
            ModifierScheme::Pc
        }
    }
}

/// The platform-independent modifier roles of an accelerator chord. Resolved
/// to physical modifiers through a [`ModifierScheme`].
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct AccelMods {
    /// The platform's command modifier: ⌘ on macOS, Ctrl elsewhere.
    pub primary: bool,
    /// The platform's "other" system modifier: ⌃ on macOS, Super elsewhere.
    pub secondary: bool,
    /// The Alt / ⌥ Option key — the same key everywhere.
    pub alt: bool,
    pub shift: bool,
}

/// A keyboard accelerator: a chord of modifier *roles* plus a key.
///
/// Build one with [`Accel::primary`] and the chainable [`shift`](Self::shift) /
/// [`alt`](Self::alt) / [`secondary`](Self::secondary) builders, or parse the
/// conventional string form (`"Ctrl+Shift+R"`, `"Ctrl+Enter"`) via `From<&str>`
/// — in strings, `Ctrl` and `Cmd` both mean the `primary` role. Exotic
/// combinations without `primary` can be built from the public fields.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Accel {
    pub mods: AccelMods,
    pub key: Key,
}

impl Accel {
    /// A chord of the primary command modifier plus `key` — the everyday
    /// accelerator: `Accel::primary('r')`, `Accel::primary(NamedKey::Enter)`.
    pub fn primary(key: impl Into<Key>) -> Self {
        Self {
            mods: AccelMods {
                primary: true,
                ..AccelMods::default()
            },
            key: key.into(),
        }
    }

    /// Add Shift to the chord.
    pub fn shift(mut self) -> Self {
        self.mods.shift = true;
        self
    }

    /// Add Alt (⌥ Option) to the chord.
    pub fn alt(mut self) -> Self {
        self.mods.alt = true;
        self
    }

    /// Add the secondary system modifier (⌃ on macOS, Super elsewhere) to the
    /// chord. Best-effort outside macOS — see the [module docs](self).
    pub fn secondary(mut self) -> Self {
        self.mods.secondary = true;
        self
    }

    /// Whether the pressed `key` + `modifiers` are exactly this chord under
    /// `scheme`. Modifiers must match exactly (a `Ctrl+R` accel does not fire
    /// on Ctrl+Shift+R); character keys compare case-insensitively. AltGr
    /// chords never match — the right Alt composes characters (and Windows
    /// reports it as Ctrl+Alt), so those keystrokes are text entry, not
    /// commands.
    pub fn matches(&self, key: Key, modifiers: Modifiers, scheme: ModifierScheme) -> bool {
        if modifiers.alt_graph {
            return false;
        }
        let (control, logo) = match scheme {
            ModifierScheme::Mac => (self.mods.secondary, self.mods.primary),
            ModifierScheme::Pc => (self.mods.primary, self.mods.secondary),
        };
        if modifiers.control != control
            || modifiers.logo != logo
            || modifiers.alt != self.mods.alt
            || modifiers.shift != self.mods.shift
        {
            return false;
        }
        match (self.key, key) {
            (Key::Char(a), Key::Char(b)) => a.eq_ignore_ascii_case(&b),
            (a, b) => a == b,
        }
    }

    /// The chord's display label under `scheme`, e.g. `"Ctrl+Shift+R"` on PC
    /// and `"Shift+Cmd+R"` on a Mac. Modifier order follows each platform's
    /// convention: Ctrl, Super, Alt, Shift on PC; Ctrl, Opt, Shift, Cmd
    /// (Apple's ⌃⌥⇧⌘) on macOS.
    pub fn label(&self, scheme: ModifierScheme) -> String {
        let mut parts: Vec<&str> = Vec::new();
        match scheme {
            ModifierScheme::Pc => {
                if self.mods.primary {
                    parts.push("Ctrl");
                }
                if self.mods.secondary {
                    parts.push("Super");
                }
                if self.mods.alt {
                    parts.push("Alt");
                }
                if self.mods.shift {
                    parts.push("Shift");
                }
            }
            ModifierScheme::Mac => {
                if self.mods.secondary {
                    parts.push("Ctrl");
                }
                if self.mods.alt {
                    parts.push("Opt");
                }
                if self.mods.shift {
                    parts.push("Shift");
                }
                if self.mods.primary {
                    parts.push("Cmd");
                }
            }
        }
        let key = key_label(self.key);
        parts.push(&key);
        parts.join("+")
    }
}

fn key_label(key: Key) -> String {
    match key {
        Key::Char(c) => c.to_ascii_uppercase().to_string(),
        Key::Named(named) => match named {
            NamedKey::Enter => "Enter",
            NamedKey::Backspace => "Backspace",
            NamedKey::Delete => "Delete",
            NamedKey::Tab => "Tab",
            NamedKey::Escape => "Esc",
            NamedKey::Space => "Space",
            NamedKey::Left => "Left",
            NamedKey::Right => "Right",
            NamedKey::Up => "Up",
            NamedKey::Down => "Down",
            NamedKey::Home => "Home",
            NamedKey::End => "End",
            NamedKey::PageUp => "PageUp",
            NamedKey::PageDown => "PageDown",
        }
        .to_string(),
    }
}

impl From<&str> for Accel {
    /// Parse the conventional `"Ctrl+Shift+R"` string form. Modifier tokens
    /// (case-insensitive): `Ctrl` / `Cmd` / `Command` / `Primary` → the
    /// primary role; `Super` / `Win` / `Secondary` → secondary; `Alt` / `Opt`
    /// / `Option` → alt; `Shift`. The final token is the key: a single
    /// character, or a named key (`Enter`, `Esc`, `Left`, `PageUp`, …).
    ///
    /// # Panics
    ///
    /// On malformed input. Accelerator strings are in practice compile-time
    /// constants, so a typo should fail loudly at first use, not bind nothing.
    fn from(s: &str) -> Self {
        let mut mods = AccelMods::default();
        let mut tokens = s.split('+').peekable();
        loop {
            let token = tokens
                .next()
                .unwrap_or_else(|| panic!("empty accelerator string {s:?}"));
            // The last token is the key; everything before it is a modifier.
            if tokens.peek().is_none() {
                return Accel {
                    mods,
                    key: parse_key(token, s),
                };
            }
            match token.to_ascii_lowercase().as_str() {
                "ctrl" | "control" | "cmd" | "command" | "primary" => mods.primary = true,
                "super" | "win" | "secondary" => mods.secondary = true,
                "alt" | "opt" | "option" => mods.alt = true,
                "shift" => mods.shift = true,
                _ => panic!("unknown modifier {token:?} in accelerator {s:?}"),
            }
        }
    }
}

fn parse_key(token: &str, accel: &str) -> Key {
    let mut chars = token.chars();
    if let (Some(c), None) = (chars.next(), chars.next()) {
        return Key::Char(c.to_ascii_lowercase());
    }
    let named = match token.to_ascii_lowercase().as_str() {
        "enter" | "return" => NamedKey::Enter,
        "backspace" => NamedKey::Backspace,
        "delete" | "del" => NamedKey::Delete,
        "tab" => NamedKey::Tab,
        "escape" | "esc" => NamedKey::Escape,
        "space" => NamedKey::Space,
        "left" => NamedKey::Left,
        "right" => NamedKey::Right,
        "up" => NamedKey::Up,
        "down" => NamedKey::Down,
        "home" => NamedKey::Home,
        "end" => NamedKey::End,
        "pageup" | "pgup" => NamedKey::PageUp,
        "pagedown" | "pgdn" => NamedKey::PageDown,
        _ => panic!("unknown key {token:?} in accelerator {accel:?}"),
    };
    Key::Named(named)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn mods(control: bool, logo: bool, alt: bool, shift: bool) -> Modifiers {
        Modifiers {
            control,
            logo,
            alt,
            shift,
            alt_graph: false,
        }
    }

    #[test]
    fn primary_resolves_to_ctrl_on_pc_and_cmd_on_mac() {
        let accel = Accel::primary('r');
        let ctrl_r = mods(true, false, false, false);
        let cmd_r = mods(false, true, false, false);
        assert!(accel.matches(Key::Char('r'), ctrl_r, ModifierScheme::Pc));
        assert!(!accel.matches(Key::Char('r'), cmd_r, ModifierScheme::Pc));
        assert!(accel.matches(Key::Char('r'), cmd_r, ModifierScheme::Mac));
        assert!(!accel.matches(Key::Char('r'), ctrl_r, ModifierScheme::Mac));
    }

    #[test]
    fn secondary_resolves_to_super_on_pc_and_ctrl_on_mac() {
        let accel = Accel::primary('r').secondary();
        // PC: Ctrl+Super+R; Mac: Ctrl+Cmd+R.
        assert!(accel.matches(
            Key::Char('r'),
            mods(true, true, false, false),
            ModifierScheme::Pc
        ));
        assert!(accel.matches(
            Key::Char('r'),
            mods(true, true, false, false),
            ModifierScheme::Mac
        ));
        // Plain primary input must not satisfy the bigger chord.
        assert!(!accel.matches(
            Key::Char('r'),
            mods(true, false, false, false),
            ModifierScheme::Pc
        ));
    }

    #[test]
    fn modifiers_must_match_exactly() {
        let accel = Accel::primary('r');
        // Ctrl+Shift+R is a different chord than Ctrl+R.
        assert!(!accel.matches(
            Key::Char('r'),
            mods(true, false, false, true),
            ModifierScheme::Pc
        ));
        // …and so is Ctrl+Alt+R.
        assert!(!accel.matches(
            Key::Char('r'),
            mods(true, false, true, false),
            ModifierScheme::Pc
        ));
        // The declared chord requires every declared modifier.
        let shifted = Accel::primary('r').shift();
        assert!(shifted.matches(
            Key::Char('r'),
            mods(true, false, false, true),
            ModifierScheme::Pc
        ));
        assert!(!shifted.matches(
            Key::Char('r'),
            mods(true, false, false, false),
            ModifierScheme::Pc
        ));
    }

    #[test]
    fn char_keys_match_case_insensitively() {
        let accel = Accel::primary('r');
        assert!(accel.matches(
            Key::Char('R'),
            mods(true, false, false, false),
            ModifierScheme::Pc
        ));
        assert!(Accel::from("Ctrl+R").matches(
            Key::Char('r'),
            mods(true, false, false, false),
            ModifierScheme::Pc
        ));
    }

    #[test]
    fn altgr_chords_never_match() {
        // Windows reports AltGr as Ctrl+Alt: composing "@" on a German layout
        // arrives as Ctrl+Alt+Q with alt_graph set. That must not fire a
        // Ctrl+Alt+Q accelerator — it's text entry.
        let accel = Accel::primary('q').alt();
        let altgr = Modifiers {
            control: true,
            alt: true,
            alt_graph: true,
            ..Modifiers::default()
        };
        assert!(!accel.matches(Key::Char('q'), altgr, ModifierScheme::Pc));
    }

    #[test]
    fn parses_the_conventional_string_forms() {
        assert_eq!(Accel::from("Ctrl+R"), Accel::primary('r'));
        assert_eq!(Accel::from("Cmd+R"), Accel::primary('r'));
        assert_eq!(Accel::from("Primary+R"), Accel::primary('r'));
        assert_eq!(Accel::from("Ctrl+Shift+T"), Accel::primary('t').shift());
        assert_eq!(Accel::from("Ctrl+Enter"), Accel::primary(NamedKey::Enter));
        assert_eq!(Accel::from("Ctrl+Left"), Accel::primary(NamedKey::Left));
        assert_eq!(
            Accel::from("Ctrl+Super+Alt+R"),
            Accel::primary('r').secondary().alt()
        );
    }

    #[test]
    #[should_panic(expected = "unknown modifier")]
    fn parsing_an_unknown_modifier_panics() {
        let _ = Accel::from("Hyper+R");
    }

    #[test]
    #[should_panic(expected = "unknown key")]
    fn parsing_an_unknown_key_panics() {
        let _ = Accel::from("Ctrl+Klick");
    }

    #[test]
    fn labels_follow_each_platforms_convention() {
        let accel = Accel::primary('r').shift();
        assert_eq!(accel.label(ModifierScheme::Pc), "Ctrl+Shift+R");
        assert_eq!(accel.label(ModifierScheme::Mac), "Shift+Cmd+R");
        // The full chord: Ctrl+Super+Alt+Shift on PC, Apple's ⌃⌥⇧⌘ order
        // (Ctrl, Opt, Shift, Cmd) on a Mac.
        let full = Accel::primary('r').secondary().alt().shift();
        assert_eq!(full.label(ModifierScheme::Pc), "Ctrl+Super+Alt+Shift+R");
        assert_eq!(full.label(ModifierScheme::Mac), "Ctrl+Opt+Shift+Cmd+R");
    }

    #[test]
    fn pc_labels_round_trip_through_parsing() {
        // The strings journey bakes into its menu snapshots must come back out
        // exactly as they went in.
        for s in ["Ctrl+R", "Ctrl+Enter", "Ctrl+Left", "Ctrl+Right", "Ctrl+Q"] {
            assert_eq!(Accel::from(s).label(ModifierScheme::Pc), s);
        }
    }
}