Skip to main content

egui_charts/ui/drawing_toolbar/
shortcuts.rs

1//! Keyboard shortcuts for drawing tools.
2
3use crate::config::KeyboardAction;
4use crate::drawings::DrawingToolType;
5use crate::widget::ShortcutMap;
6use egui::{Context, KeyboardShortcut, ModifierNames};
7
8/// Keyboard shortcuts for drawing tools
9///
10/// # Examples
11///
12/// ```
13/// use egui_charts::ui::drawing_toolbar::DrawingToolShortcuts;
14/// use egui_charts::drawings::DrawingToolType;
15/// use egui::{KeyboardShortcut, Key, Modifiers};
16///
17/// let mut shortcuts = DrawingToolShortcuts::new();
18///
19/// // Add custom shortcut
20/// shortcuts.add_shortcut(
21///     KeyboardShortcut::new(Modifiers::CTRL, Key::D),
22///     DrawingToolType::TrendLine
23/// );
24/// ```
25pub struct DrawingToolShortcuts {
26    shortcuts: Vec<(KeyboardShortcut, DrawingToolType)>,
27}
28
29impl Default for DrawingToolShortcuts {
30    fn default() -> Self {
31        Self {
32            shortcuts: ShortcutMap::default()
33                .bindings
34                .into_iter()
35                .filter_map(|(key, action)| {
36                    if let KeyboardAction::DrawingTool(tool) = action {
37                        Some((key, tool))
38                    } else {
39                        None
40                    }
41                })
42                .collect(),
43        }
44    }
45}
46
47impl DrawingToolShortcuts {
48    /// Create a [`DrawingToolShortcuts`] with default settings.
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    /// Check for keyboard shortcuts and return the tool if one was triggered
54    ///
55    /// # Examples
56    ///
57    /// ```
58    /// use egui::{Event, Key, Modifiers, RawInput};
59    /// use egui_charts::drawings::DrawingToolType;
60    /// use egui_charts::ui::drawing_toolbar::DrawingToolShortcuts;
61    ///
62    /// let shortcuts = DrawingToolShortcuts::new();
63    /// let ctx = egui::Context::default();
64    ///
65    /// // A frame in which the user pressed Alt+T.
66    /// let mut input = RawInput::default();
67    /// input.events.push(Event::Key {
68    ///     key: Key::T,
69    ///     physical_key: None,
70    ///     pressed: true,
71    ///     repeat: false,
72    ///     modifiers: Modifiers::ALT,
73    /// });
74    ///
75    /// let mut triggered = None;
76    /// ctx.run_ui(input, |ui| triggered = shortcuts.check(ui.ctx()))
77    ///     .drop_without_applying_deltas();
78    /// assert_eq!(triggered, Some(DrawingToolType::TrendLine));
79    /// ```
80    pub fn check(&self, ctx: &Context) -> Option<DrawingToolType> {
81        let map = ctx
82            .data(|data| data.get_temp::<ShortcutMap>(egui::Id::NULL.with("chart_shortcuts")))
83            .unwrap_or_else(|| ShortcutMap {
84                bindings: self
85                    .shortcuts
86                    .iter()
87                    .map(|(key, tool)| (*key, KeyboardAction::DrawingTool(*tool)))
88                    .collect(),
89            });
90        match map.consume(ctx, true, |action, _| {
91            matches!(action, KeyboardAction::DrawingTool(_))
92        }) {
93            Some((KeyboardAction::DrawingTool(tool), _)) => Some(tool),
94            _ => None,
95        }
96    }
97
98    /// The shortcut that selects `tool`, written the way the platform writes it ("Alt+T"), for
99    /// the hint next to the tool in a flyout.
100    pub(super) fn hint(&self, ctx: &Context, tool: DrawingToolType) -> Option<String> {
101        if let Some(map) =
102            ctx.data(|data| data.get_temp::<ShortcutMap>(egui::Id::NULL.with("chart_shortcuts")))
103        {
104            return map
105                .shortcut(KeyboardAction::DrawingTool(tool))
106                .map(|shortcut| ctx.format_shortcut(&shortcut));
107        }
108        self.shortcuts
109            .iter()
110            .find(|(_, bound)| *bound == tool)
111            .map(|(shortcut, _)| ctx.format_shortcut(shortcut))
112    }
113
114    /// Add a custom shortcut
115    ///
116    /// # Examples
117    ///
118    /// ```
119    /// # use egui_charts::ui::drawing_toolbar::DrawingToolShortcuts;
120    /// # use egui_charts::drawings::DrawingToolType;
121    /// # use egui::{KeyboardShortcut, Key, Modifiers};
122    /// let mut shortcuts = DrawingToolShortcuts::new();
123    /// shortcuts.add_shortcut(
124    ///     KeyboardShortcut::new(Modifiers::CTRL, Key::T),
125    ///     DrawingToolType::TrendLine
126    /// );
127    /// ```
128    pub fn add_shortcut(&mut self, shortcut: KeyboardShortcut, tool: DrawingToolType) {
129        self.shortcuts.push((shortcut, tool));
130    }
131
132    /// Get help text for shortcuts
133    ///
134    /// Returns a list of (shortcut_text, tool_name) pairs, such as ("Alt+T", "Trend Line").
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// # use egui_charts::ui::drawing_toolbar::DrawingToolShortcuts;
140    /// let shortcuts = DrawingToolShortcuts::new();
141    /// for (key, tool) in shortcuts.help_text() {
142    ///     println!("{}: {}", key, tool);
143    /// }
144    /// ```
145    pub fn help_text(&self) -> Vec<(String, String)> {
146        self.shortcuts
147            .iter()
148            .map(|(shortcut, tool)| {
149                let key_text = shortcut.format(&ModifierNames::NAMES, false);
150                (key_text, tool.as_str().to_string())
151            })
152            .collect()
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use egui::{Event, Key, Modifiers, RawInput};
160
161    /// Alt+Shift+R selects the rectangle tool by default, plain Alt+R does not, and a host
162    /// map that rebinds the tool replaces the default.
163    #[test]
164    fn alt_shift_r_selects_the_rectangle_until_rebound() {
165        let alt_shift = Modifiers::ALT.plus(Modifiers::SHIFT);
166        let rebound = ShortcutMap {
167            bindings: vec![(
168                KeyboardShortcut::new(Modifiers::CTRL, Key::J),
169                KeyboardAction::DrawingTool(DrawingToolType::Rect),
170            )],
171        };
172        for (map, key, modifiers, expected) in [
173            (None, Key::R, alt_shift, Some(DrawingToolType::Rect)),
174            (None, Key::R, Modifiers::ALT, None),
175            (Some(&rebound), Key::R, alt_shift, None),
176            (
177                Some(&rebound),
178                Key::J,
179                Modifiers::CTRL,
180                Some(DrawingToolType::Rect),
181            ),
182        ] {
183            let ctx = Context::default();
184            if let Some(map) = map {
185                ctx.data_mut(|data| {
186                    data.insert_temp(egui::Id::NULL.with("chart_shortcuts"), map.clone())
187                });
188            }
189            let input = RawInput {
190                events: vec![Event::Key {
191                    key,
192                    physical_key: None,
193                    pressed: true,
194                    repeat: false,
195                    modifiers,
196                }],
197                ..Default::default()
198            };
199            let mut got = None;
200            ctx.run_ui(input, |ui| {
201                got = DrawingToolShortcuts::new().check(ui.ctx())
202            })
203            .drop_without_applying_deltas();
204            assert_eq!(got, expected, "{key:?} {modifiers:?}");
205        }
206    }
207}