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}