Skip to main content

egui_charts/config/
keyboard.rs

1//! Keyboard shortcut configuration and action mapping.
2//!
3//! [`KeyboardOptions`] toggles keyboard shortcuts on/off and configures
4//! sensitivity.  [`KeyboardAction`] is the complete enum of actions that
5//! can be triggered by key presses, with methods to map egui key events
6//! to actions and to retrieve human-readable labels for a shortcuts dialog.
7
8use egui::Key;
9use std::fmt;
10
11/// Global keyboard shortcut configuration.
12#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
13pub struct KeyboardOptions {
14    /// Enable keyboard shortcuts
15    pub enabled: bool,
16
17    /// Pan amount in bars for arrow keys
18    pub pan_amount: f32,
19
20    /// Zoom step for +/- keys
21    pub zoom_step: f32,
22}
23
24impl Default for KeyboardOptions {
25    fn default() -> Self {
26        Self {
27            enabled: true,
28            pan_amount: 1.0,
29            zoom_step: 0.2,
30        }
31    }
32}
33
34/// Keyboard actions that can be performed on the chart
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36#[non_exhaustive]
37pub enum KeyboardAction {
38    /// Pan left by a page.
39    PanLeftFast,
40    /// Pan right by a page.
41    PanRightFast,
42    /// Undo the last edit.
43    Undo,
44    /// Redo the last undone edit.
45    Redo,
46    /// Open quick search.
47    OpenQuickSearch,
48    /// Open interval entry.
49    OpenIntervalSearch,
50    /// Go to a date.
51    GoToDate,
52    /// Take a chart snapshot.
53    Snapshot,
54    /// Invert the price scale.
55    InvertScale,
56    /// Copy the selected drawing.
57    CopyDrawing,
58    /// Paste a copied drawing.
59    PasteDrawing,
60    /// Hide or show drawings.
61    ToggleDrawings,
62    /// Maximize the active chart.
63    MaximizeChart,
64    /// Complete a multi-point drawing.
65    CompleteDrawing,
66    /// Select a drawing tool. A horizontal, vertical or cross line is placed
67    /// at once at the pointer when the pointer is over the chart.
68    DrawingTool(crate::config::DrawingToolType),
69
70    // Navigation
71    /// Pan left (Left arrow)
72    PanLeft,
73    /// Pan right (Right arrow)
74    PanRight,
75    /// Pan up in the price scale
76    PanUp,
77    /// Pan down in the price scale
78    PanDown,
79
80    // Zoom
81    /// Zoom in (+, =)
82    ZoomIn,
83    /// Zoom out (-, _)
84    ZoomOut,
85    /// Page up (zoom in more)
86    PageUp,
87    /// Page down (zoom out more)
88    PageDown,
89
90    // Reset/Fit
91    /// Scroll to real-time / latest data (Alt+Shift+Right, End)
92    ScrollToRealTime,
93    /// Scroll the oldest bar to the left edge, keeping the zoom (Alt+Shift+Left, Home)
94    ScrollToFirstBar,
95    /// Fit content to visible range
96    FitContent,
97    /// Reset zoom to default
98    ResetZoom,
99    /// Reset chart (includes zoom and pan) (Alt+R)
100    ResetChart,
101
102    // Scale modes
103    /// Toggle auto-scale
104    ToggleAutoScale,
105    /// Toggle logarithmic scale
106    ToggleLogScale,
107    /// Toggle percentage scale
108    TogglePercentageScale,
109
110    // Visual toggles
111    /// Toggle crosshair
112    ToggleCrosshair,
113    /// Toggle grid
114    ToggleGrid,
115    /// Toggle volume
116    ToggleVolume,
117
118    // Chart type
119    /// Cycle through chart types
120    CycleChartType,
121
122    // Drawing tools
123    /// Enable/disable drawing mode
124    ToggleDrawingMode,
125    /// Delete the selected drawings, or the selected study when no drawing is selected
126    /// (Delete, Backspace)
127    DeleteSelectedDrawing,
128    /// Escape/cancel current action (Escape)
129    Cancel,
130
131    // Timeframe commands (unbound by default)
132    /// 1-minute timeframe
133    SetTimeframe1Min,
134    /// 5-minute timeframe
135    SetTimeframe5Min,
136    /// 15-minute timeframe
137    SetTimeframe15Min,
138    /// 30-minute timeframe
139    SetTimeframe30Min,
140    /// 1-hour timeframe
141    SetTimeframe1Hour,
142    /// 4-hour timeframe
143    SetTimeframe4Hour,
144    /// Daily timeframe
145    SetTimeframeDaily,
146    /// Weekly timeframe
147    SetTimeframeWeekly,
148    /// Monthly timeframe
149    SetTimeframeMonthly,
150
151    // App-level shortcuts
152    /// Create alert (Alt+A)
153    CreateAlert,
154    /// Open indicator search (/)
155    OpenIndicatorSearch,
156    /// Open symbol search
157    OpenSymbolSearch,
158    /// Save layout
159    SaveLayout,
160    /// Open a saved layout (.)
161    OpenLayout,
162    /// Show the data window (Alt+D)
163    OpenDataWindow,
164    /// Switch between the regular and extended trading sessions (Alt+Shift+E)
165    ToggleSession,
166    /// Show keyboard shortcuts help (?)
167    ShowKeyboardHelp,
168
169    // Multi-chart focus
170    /// Cycle focus to next chart (Tab)
171    CycleChartFocus,
172    /// Cycle focus to previous chart (Shift+Tab)
173    CycleChartFocusBack,
174}
175
176impl KeyboardAction {
177    /// Scan all key presses in the current input state and return the first matching action.
178    pub fn from_key_press_with_modifiers(input: &egui::InputState) -> Option<Self> {
179        for event in &input.events {
180            if let egui::Event::Key {
181                key,
182                pressed: true,
183                modifiers,
184                ..
185            } = event
186                && let Some(action) = Self::from_key_press(*key, modifiers)
187            {
188                return Some(action);
189            }
190        }
191        None
192    }
193
194    /// Map egui key press to keyboard action
195    /// Returns None if the key is not mapped
196    pub fn from_key_press(key: Key, modifiers: &egui::Modifiers) -> Option<Self> {
197        Self::all_by_category()
198            .into_iter()
199            .flat_map(|(_, actions)| actions)
200            .find(|action| {
201                action.shortcuts().iter().any(|shortcut| {
202                    shortcut.logical_key == key && modifiers.matches_exact(shortcut.modifiers)
203                })
204            })
205    }
206
207    /// Human-readable description of the action. Use `shortcuts` for its bound keys.
208    pub fn desc(&self) -> &str {
209        self.label()
210    }
211
212    /// Human-readable label for the action (used in shortcuts dialog)
213    pub fn label(&self) -> &str {
214        match self {
215            Self::PanLeftFast => "Pan left by a page",
216            Self::PanRightFast => "Pan right by a page",
217            Self::Undo => "Undo the last edit",
218            Self::Redo => "Redo the last undone edit",
219            Self::OpenQuickSearch => "Open quick search",
220            Self::OpenIntervalSearch => "Open interval entry",
221            Self::GoToDate => "Go to a date",
222            Self::Snapshot => "Take a chart snapshot",
223            Self::InvertScale => "Invert the price scale",
224            Self::CopyDrawing => "Copy the selected drawing",
225            Self::PasteDrawing => "Paste a copied drawing",
226            Self::ToggleDrawings => "Hide or show drawings",
227            Self::MaximizeChart => "Maximize the active chart",
228            Self::CompleteDrawing => "Complete drawing",
229            Self::DrawingTool(tool) => tool.as_str(),
230            Self::PanLeft => "Pan left",
231            Self::PanRight => "Pan right",
232            Self::PanUp => "Pan up",
233            Self::PanDown => "Pan down",
234            Self::ZoomIn => "Zoom in",
235            Self::ZoomOut => "Zoom out",
236            Self::PageUp => "Zoom in more",
237            Self::PageDown => "Zoom out more",
238            Self::ScrollToRealTime => "Jump to latest",
239            Self::ScrollToFirstBar => "Jump to first bar",
240            Self::FitContent => "Fit to screen",
241            Self::ResetZoom => "Reset zoom",
242            Self::ResetChart => "Reset chart",
243            Self::ToggleAutoScale => "Toggle auto-scale",
244            Self::ToggleLogScale => "Toggle log scale",
245            Self::TogglePercentageScale => "Toggle percentage scale",
246            Self::ToggleCrosshair => "Toggle crosshair",
247            Self::ToggleGrid => "Toggle grid",
248            Self::ToggleVolume => "Toggle volume",
249            Self::CycleChartType => "Cycle chart type",
250            Self::ToggleDrawingMode => "Toggle drawing mode",
251            Self::DeleteSelectedDrawing => "Delete drawing or study",
252            Self::Cancel => "Cancel",
253            Self::SetTimeframe1Min => "1 minute",
254            Self::SetTimeframe5Min => "5 minutes",
255            Self::SetTimeframe15Min => "15 minutes",
256            Self::SetTimeframe30Min => "30 minutes",
257            Self::SetTimeframe1Hour => "1 hour",
258            Self::SetTimeframe4Hour => "4 hours",
259            Self::SetTimeframeDaily => "Daily",
260            Self::SetTimeframeWeekly => "Weekly",
261            Self::SetTimeframeMonthly => "Monthly",
262            Self::CreateAlert => "Create alert",
263            Self::OpenIndicatorSearch => "Open indicators",
264            Self::OpenSymbolSearch => "Symbol search",
265            Self::SaveLayout => "Save layout",
266            Self::OpenLayout => "Open layout",
267            Self::OpenDataWindow => "Data window",
268            Self::ToggleSession => "Switch session",
269            Self::ShowKeyboardHelp => "Keyboard shortcuts",
270            Self::CycleChartFocus => "Next chart",
271            Self::CycleChartFocusBack => "Previous chart",
272        }
273    }
274
275    /// The key combinations for this action, the first of which menus show. Empty for the
276    /// actions the shortcuts dialog and the menus do not show.
277    pub fn shortcuts(&self) -> &'static [egui::KeyboardShortcut] {
278        use egui::{KeyboardShortcut, Modifiers};
279        const fn key(key: Key) -> KeyboardShortcut {
280            KeyboardShortcut::new(Modifiers::NONE, key)
281        }
282        match self {
283            Self::PanLeft => const { &[key(Key::ArrowLeft)] },
284            Self::PanRight => const { &[key(Key::ArrowRight)] },
285            Self::PanLeftFast => {
286                const { &[KeyboardShortcut::new(Modifiers::COMMAND, Key::ArrowLeft)] }
287            }
288            Self::PanRightFast => {
289                const { &[KeyboardShortcut::new(Modifiers::COMMAND, Key::ArrowRight)] }
290            }
291            Self::ZoomIn => {
292                const {
293                    &[
294                        key(Key::Plus),
295                        key(Key::Equals),
296                        KeyboardShortcut::new(Modifiers::COMMAND, Key::ArrowUp),
297                    ]
298                }
299            }
300            Self::ZoomOut => {
301                const {
302                    &[
303                        key(Key::Minus),
304                        KeyboardShortcut::new(Modifiers::COMMAND, Key::ArrowDown),
305                    ]
306                }
307            }
308            Self::PageUp => const { &[key(Key::PageUp)] },
309            Self::PageDown => const { &[key(Key::PageDown)] },
310            Self::ScrollToRealTime => {
311                const {
312                    &[
313                        KeyboardShortcut::new(
314                            Modifiers::ALT.plus(Modifiers::SHIFT),
315                            Key::ArrowRight,
316                        ),
317                        key(Key::End),
318                    ]
319                }
320            }
321            Self::ScrollToFirstBar => {
322                const {
323                    &[
324                        KeyboardShortcut::new(
325                            Modifiers::ALT.plus(Modifiers::SHIFT),
326                            Key::ArrowLeft,
327                        ),
328                        key(Key::Home),
329                    ]
330                }
331            }
332
333            Self::ResetChart => const { &[KeyboardShortcut::new(Modifiers::ALT, Key::R)] },
334
335            Self::ToggleLogScale => const { &[KeyboardShortcut::new(Modifiers::ALT, Key::L)] },
336            Self::TogglePercentageScale => {
337                const { &[KeyboardShortcut::new(Modifiers::ALT, Key::P)] }
338            }
339            Self::InvertScale => const { &[KeyboardShortcut::new(Modifiers::ALT, Key::I)] },
340            Self::CreateAlert => const { &[KeyboardShortcut::new(Modifiers::ALT, Key::A)] },
341            Self::OpenIndicatorSearch => const { &[key(Key::Slash)] },
342            Self::OpenQuickSearch => const { &[KeyboardShortcut::new(Modifiers::COMMAND, Key::K)] },
343            Self::GoToDate => const { &[KeyboardShortcut::new(Modifiers::ALT, Key::G)] },
344            Self::Snapshot => const { &[KeyboardShortcut::new(Modifiers::ALT, Key::S)] },
345            Self::SaveLayout => const { &[KeyboardShortcut::new(Modifiers::COMMAND, Key::S)] },
346            Self::OpenLayout => const { &[key(Key::Period)] },
347            Self::OpenDataWindow => const { &[KeyboardShortcut::new(Modifiers::ALT, Key::D)] },
348            Self::ToggleSession => {
349                const {
350                    &[KeyboardShortcut::new(
351                        Modifiers::ALT.plus(Modifiers::SHIFT),
352                        Key::E,
353                    )]
354                }
355            }
356            Self::Undo => const { &[KeyboardShortcut::new(Modifiers::COMMAND, Key::Z)] },
357            Self::Redo => {
358                const {
359                    &[
360                        KeyboardShortcut::new(Modifiers::COMMAND, Key::Y),
361                        KeyboardShortcut::new(Modifiers::COMMAND.plus(Modifiers::SHIFT), Key::Z),
362                    ]
363                }
364            }
365            Self::CopyDrawing => const { &[KeyboardShortcut::new(Modifiers::COMMAND, Key::C)] },
366            Self::PasteDrawing => const { &[KeyboardShortcut::new(Modifiers::COMMAND, Key::V)] },
367            Self::ToggleDrawings => {
368                const {
369                    &[KeyboardShortcut::new(
370                        Modifiers::COMMAND.plus(Modifiers::ALT),
371                        Key::H,
372                    )]
373                }
374            }
375            Self::MaximizeChart => const { &[KeyboardShortcut::new(Modifiers::ALT, Key::Enter)] },
376            Self::CycleChartFocus => const { &[key(Key::Tab)] },
377            Self::CycleChartFocusBack => {
378                const { &[KeyboardShortcut::new(Modifiers::SHIFT, Key::Tab)] }
379            }
380            Self::DeleteSelectedDrawing => const { &[key(Key::Delete), key(Key::Backspace)] },
381            Self::Cancel => const { &[key(Key::Escape)] },
382            Self::CompleteDrawing => const { &[key(Key::Enter)] },
383            Self::ShowKeyboardHelp => {
384                const {
385                    &[
386                        KeyboardShortcut::new(Modifiers::COMMAND, Key::Slash),
387                        key(Key::Questionmark),
388                        key(Key::F1),
389                    ]
390                }
391            }
392            Self::DrawingTool(crate::config::DrawingToolType::TrendLine) => {
393                const { &[KeyboardShortcut::new(Modifiers::ALT, Key::T)] }
394            }
395            Self::DrawingTool(crate::config::DrawingToolType::HorizontalLine) => {
396                const { &[KeyboardShortcut::new(Modifiers::ALT, Key::H)] }
397            }
398            Self::DrawingTool(crate::config::DrawingToolType::VerticalLine) => {
399                const { &[KeyboardShortcut::new(Modifiers::ALT, Key::V)] }
400            }
401            Self::DrawingTool(crate::config::DrawingToolType::CrossLine) => {
402                const { &[KeyboardShortcut::new(Modifiers::ALT, Key::C)] }
403            }
404            Self::DrawingTool(crate::config::DrawingToolType::HorizontalRay) => {
405                const { &[KeyboardShortcut::new(Modifiers::ALT, Key::J)] }
406            }
407            Self::DrawingTool(crate::config::DrawingToolType::FibonacciRetracement) => {
408                const { &[KeyboardShortcut::new(Modifiers::ALT, Key::F)] }
409            }
410            Self::DrawingTool(crate::config::DrawingToolType::Rect) => {
411                const {
412                    &[KeyboardShortcut::new(
413                        Modifiers::ALT.plus(Modifiers::SHIFT),
414                        Key::R,
415                    )]
416                }
417            }
418            _ => &[],
419        }
420    }
421
422    /// The first of [`shortcuts`](Self::shortcuts), for menus to show with
423    /// [`egui::Context::format_shortcut`].
424    pub fn shortcut(&self) -> Option<egui::KeyboardShortcut> {
425        self.shortcuts().first().copied()
426    }
427
428    /// Short display text for default bindings. Menus format `shortcuts` for the platform.
429    pub fn shortcut_key(&self) -> &'static str {
430        match self {
431            Self::PanLeft => "←",
432            Self::PanRight => "→",
433            Self::PanLeftFast => "Ctrl+Left",
434            Self::PanRightFast => "Ctrl+Right",
435            Self::ZoomIn => "+ / = / Ctrl+Up",
436            Self::ZoomOut => "- / Ctrl+Down",
437            Self::PageUp => "Page Up",
438            Self::PageDown => "Page Down",
439            Self::ScrollToRealTime => "Alt+Shift+Right / End",
440            Self::ScrollToFirstBar => "Alt+Shift+Left / Home",
441            Self::ResetChart => "Alt+R",
442            Self::ToggleLogScale => "Alt+L",
443            Self::TogglePercentageScale => "Alt+P",
444            Self::InvertScale => "Alt+I",
445            Self::Undo => "Ctrl+Z",
446            Self::Redo => "Ctrl+Y / Ctrl+Shift+Z",
447            Self::CopyDrawing => "Ctrl+C",
448            Self::PasteDrawing => "Ctrl+V",
449            Self::ToggleDrawings => "Ctrl+Alt+H",
450            Self::CreateAlert => "Alt+A",
451            Self::OpenQuickSearch => "Ctrl+K",
452            Self::OpenIndicatorSearch => "/",
453            Self::GoToDate => "Alt+G",
454            Self::Snapshot => "Alt+S",
455            Self::SaveLayout => "Ctrl+S",
456            Self::OpenLayout => ".",
457            Self::OpenDataWindow => "Alt+D",
458            Self::ToggleSession => "Alt+Shift+E",
459            Self::MaximizeChart => "Alt+Enter",
460            Self::DeleteSelectedDrawing => "Del / Backspace",
461            Self::Cancel => "Esc",
462            Self::CompleteDrawing => "Enter",
463            Self::ShowKeyboardHelp => "Ctrl+/ / ? / F1",
464            Self::CycleChartFocus => "Tab",
465            Self::CycleChartFocusBack => "Shift+Tab",
466            Self::DrawingTool(crate::config::DrawingToolType::TrendLine) => "Alt+T",
467            Self::DrawingTool(crate::config::DrawingToolType::HorizontalLine) => "Alt+H",
468            Self::DrawingTool(crate::config::DrawingToolType::VerticalLine) => "Alt+V",
469            Self::DrawingTool(crate::config::DrawingToolType::CrossLine) => "Alt+C",
470            Self::DrawingTool(crate::config::DrawingToolType::HorizontalRay) => "Alt+J",
471            Self::DrawingTool(crate::config::DrawingToolType::FibonacciRetracement) => "Alt+F",
472            Self::DrawingTool(crate::config::DrawingToolType::Rect) => "Alt+Shift+R",
473            _ => "",
474        }
475    }
476
477    /// The actions with a bound key, grouped by category (for the shortcuts dialog).
478    pub fn all_by_category() -> Vec<(&'static str, Vec<KeyboardAction>)> {
479        use crate::config::DrawingToolType;
480        vec![
481            (
482                "Navigation",
483                vec![
484                    Self::PanLeft,
485                    Self::PanRight,
486                    Self::PanLeftFast,
487                    Self::PanRightFast,
488                ],
489            ),
490            (
491                "Zoom",
492                vec![Self::ZoomIn, Self::ZoomOut, Self::PageUp, Self::PageDown],
493            ),
494            (
495                "View",
496                vec![
497                    Self::ScrollToRealTime,
498                    Self::ScrollToFirstBar,
499                    Self::FitContent,
500                    Self::ResetChart,
501                    Self::ToggleLogScale,
502                    Self::TogglePercentageScale,
503                    Self::InvertScale,
504                    Self::ToggleSession,
505                ],
506            ),
507            (
508                "Editing",
509                vec![
510                    Self::Undo,
511                    Self::Redo,
512                    Self::CopyDrawing,
513                    Self::PasteDrawing,
514                    Self::ToggleDrawings,
515                ],
516            ),
517            (
518                "Drawing tools",
519                vec![
520                    Self::DrawingTool(DrawingToolType::TrendLine),
521                    Self::DrawingTool(DrawingToolType::HorizontalLine),
522                    Self::DrawingTool(DrawingToolType::HorizontalRay),
523                    Self::DrawingTool(DrawingToolType::VerticalLine),
524                    Self::DrawingTool(DrawingToolType::CrossLine),
525                    Self::DrawingTool(DrawingToolType::FibonacciRetracement),
526                    Self::DrawingTool(DrawingToolType::Rect),
527                    Self::DeleteSelectedDrawing,
528                    Self::Cancel,
529                    Self::CompleteDrawing,
530                ],
531            ),
532            (
533                "App",
534                vec![
535                    Self::OpenQuickSearch,
536                    Self::OpenIndicatorSearch,
537                    Self::CreateAlert,
538                    Self::GoToDate,
539                    Self::Snapshot,
540                    Self::SaveLayout,
541                    Self::OpenLayout,
542                    Self::OpenDataWindow,
543                    Self::MaximizeChart,
544                    Self::CycleChartFocus,
545                    Self::CycleChartFocusBack,
546                    Self::ShowKeyboardHelp,
547                ],
548            ),
549        ]
550    }
551}
552
553impl fmt::Display for KeyboardAction {
554    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
555        write!(f, "{}", self.desc())
556    }
557}