1use egui::os::OperatingSystem;
2use egui::{Id, Key, KeyboardShortcut, Modifiers};
3use smallvec::{SmallVec, smallvec};
4
5use crate::context_ext::ContextExt as _;
6
7pub trait UICommandSender {
9 fn send_ui(&self, command: UICommand);
10}
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13pub struct SetPlaybackSpeed(pub egui::emath::OrderedFloat<f32>);
14
15impl Default for SetPlaybackSpeed {
16 fn default() -> Self {
17 Self(egui::emath::OrderedFloat(1.0))
18 }
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, strum_macros::EnumIter)]
27pub enum UICommand {
28 Open,
30 OpenUrl,
31 Import,
32
33 SaveRecording,
35 SaveRecordingSelection,
36 SaveBlueprint,
37 CloseCurrentRecording,
38 CloseAllEntries,
39
40 NextRecording,
41 PreviousRecording,
42
43 NavigateBack,
44 NavigateForward,
45
46 Undo,
47 Redo,
48
49 #[cfg(not(target_arch = "wasm32"))]
50 Quit,
51
52 OpenWebHelp,
53 OpenRerunDiscord,
54
55 ResetViewer,
56 ClearActiveBlueprint,
57 ClearActiveBlueprintAndEnableHeuristics,
58
59 #[cfg(not(target_arch = "wasm32"))]
60 OpenProfiler,
61
62 TogglePanelStateOverrides,
63 ToggleMemoryPanel,
64 ToggleTopPanel,
65 ToggleBlueprintPanel,
66 ExpandBlueprintPanel,
67 ToggleSelectionPanel,
68 ExpandSelectionPanel,
69 ToggleTimePanel,
70 ToggleChunkStoreBrowser,
71 Settings,
72
73 #[cfg(debug_assertions)]
74 ToggleBlueprintInspectionPanel,
75
76 #[cfg(debug_assertions)]
77 ToggleEguiDebugPanel,
78
79 ToggleFullscreen,
80 #[cfg(not(target_arch = "wasm32"))]
81 ZoomIn,
82 #[cfg(not(target_arch = "wasm32"))]
83 ZoomOut,
84 #[cfg(not(target_arch = "wasm32"))]
85 ZoomReset,
86
87 ToggleCommandPalette,
88
89 PlaybackTogglePlayPause,
91 PlaybackFollow,
92 PlaybackStepBack,
93 PlaybackStepForward,
94 PlaybackBack,
95 PlaybackForward,
96 PlaybackBackFast,
97 PlaybackForwardFast,
98 PlaybackBeginning,
99 PlaybackEnd,
100 PlaybackRestart,
101 PlaybackSpeed(SetPlaybackSpeed),
102
103 #[cfg(not(target_arch = "wasm32"))]
105 ScreenshotWholeApp,
106 #[cfg(not(target_arch = "wasm32"))]
107 PrintChunkStore,
108 #[cfg(not(target_arch = "wasm32"))]
109 PrintBlueprintStore,
110 #[cfg(not(target_arch = "wasm32"))]
111 PrintPrimaryCache,
112
113 #[cfg(debug_assertions)]
114 ResetEguiMemory,
115
116 Share,
117 CopyDirectLink,
118
119 CopyTimeSelectionLink,
120
121 CopyEntityHierarchy,
122
123 #[cfg(target_arch = "wasm32")]
125 RestartWithWebGl,
126 #[cfg(target_arch = "wasm32")]
127 RestartWithWebGpu,
128
129 AddRedapServer,
131}
132
133impl UICommand {
134 pub fn text(self) -> &'static str {
135 self.text_and_tooltip().0
136 }
137
138 pub fn tooltip(self) -> &'static str {
139 self.text_and_tooltip().1
140 }
141
142 pub fn text_and_tooltip(self) -> (&'static str, &'static str) {
143 match self {
144 Self::SaveRecording => (
145 "Save recording…",
146 "Save all data to a Rerun data file (.rrd)",
147 ),
148
149 Self::SaveRecordingSelection => (
150 "Save current time selection…",
151 "Save data for the current loop selection to a Rerun data file (.rrd)",
152 ),
153
154 Self::SaveBlueprint => (
155 "Save blueprint…",
156 "Save the current viewer setup as a Rerun blueprint file (.rbl)",
157 ),
158
159 Self::Open => (
160 "Open file…",
161 "Open any supported files (.rrd, images, meshes, …) in a new recording",
162 ),
163 Self::OpenUrl => (
164 "Open from URL…",
165 "Open or navigate to data from any supported URL",
166 ),
167 Self::Import => (
168 "Import into current recording…",
169 "Import any supported files (.rrd, images, meshes, …) in the current recording",
170 ),
171
172 Self::CloseCurrentRecording => (
173 "Close current recording",
174 "Close the current recording (unsaved data will be lost)",
175 ),
176
177 Self::CloseAllEntries => (
178 "Close all recordings",
179 "Close all open current recording (unsaved data will be lost)",
180 ),
181
182 Self::NextRecording => ("Next recording", "Switch to the next open recording"),
183 Self::PreviousRecording => (
184 "Previous recording",
185 "Switch to the previous open recording",
186 ),
187
188 Self::NavigateBack => ("Back in history", "Go back in history"),
189 Self::NavigateForward => ("Forward in history", "Go forward in history"),
190
191 Self::Undo => (
192 "Undo",
193 "Undo the last blueprint edit for the open recording",
194 ),
195 Self::Redo => ("Redo", "Redo the last undone thing"),
196
197 #[cfg(not(target_arch = "wasm32"))]
198 Self::Quit => ("Quit", "Close the Rerun Viewer"),
199
200 Self::OpenWebHelp => (
201 "Help",
202 "Visit the help page on our website, with troubleshooting tips and more",
203 ),
204 Self::OpenRerunDiscord => (
205 "Rerun Discord",
206 "Visit the Rerun Discord server, where you can ask questions and get help",
207 ),
208
209 Self::ResetViewer => (
210 "Reset Viewer",
211 "Reset the Viewer to how it looked the first time you ran it, forgetting UI state and all stored blueprints, except the ones loaded from *.rbl resources",
212 ),
213
214 Self::ClearActiveBlueprint => (
215 "Reset to default blueprint",
216 "Clear active blueprint and use the default blueprint instead. If no default blueprint is set, this will use a heuristic blueprint.",
217 ),
218
219 Self::ClearActiveBlueprintAndEnableHeuristics => (
220 "Reset to heuristic blueprint",
221 "Re-populate viewport with automatically chosen views using default visualizers",
222 ),
223
224 #[cfg(not(target_arch = "wasm32"))]
225 Self::OpenProfiler => (
226 "Open profiler",
227 "Starts a profiler, showing what makes the viewer run slow",
228 ),
229
230 Self::ToggleMemoryPanel => (
231 "Toggle memory panel",
232 "View and track current RAM usage inside Rerun Viewer",
233 ),
234
235 Self::TogglePanelStateOverrides => (
236 "Toggle panel state overrides",
237 "Toggle panel state between app blueprint and overrides",
238 ),
239 Self::ToggleTopPanel => ("Toggle top panel", "Toggle the top panel"),
240 Self::ToggleBlueprintPanel => ("Toggle blueprint panel", "Toggle the left panel"),
241 Self::ExpandBlueprintPanel => ("Expand blueprint panel", "Expand the left panel"),
242 Self::ToggleSelectionPanel => ("Toggle selection panel", "Toggle the right panel"),
243 Self::ExpandSelectionPanel => ("Expand selection panel", "Expand the right panel"),
244 Self::ToggleTimePanel => ("Toggle time panel", "Toggle the bottom panel"),
245 Self::ToggleChunkStoreBrowser => (
246 "Toggle chunk store browser",
247 "Toggle the chunk store browser",
248 ),
249 Self::Settings => ("Settings…", "Show the settings screen"),
250
251 #[cfg(debug_assertions)]
252 Self::ToggleBlueprintInspectionPanel => (
253 "Toggle blueprint inspection panel",
254 "Inspect the timeline of the internal blueprint data.",
255 ),
256
257 #[cfg(debug_assertions)]
258 Self::ToggleEguiDebugPanel => (
259 "Toggle egui debug panel",
260 "View and change global egui style settings",
261 ),
262
263 #[cfg(not(target_arch = "wasm32"))]
264 Self::ToggleFullscreen => (
265 "Toggle fullscreen",
266 "Toggle between windowed and fullscreen viewer",
267 ),
268
269 #[cfg(target_arch = "wasm32")]
270 Self::ToggleFullscreen => (
271 "Toggle fullscreen",
272 "Toggle between full viewport dimensions and initial dimensions",
273 ),
274
275 #[cfg(not(target_arch = "wasm32"))]
276 Self::ZoomIn => ("Zoom in", "Increases the UI zoom level"),
277 #[cfg(not(target_arch = "wasm32"))]
278 Self::ZoomOut => ("Zoom out", "Decreases the UI zoom level"),
279 #[cfg(not(target_arch = "wasm32"))]
280 Self::ZoomReset => (
281 "Reset zoom",
282 "Resets the UI zoom level to the operating system's default value",
283 ),
284
285 Self::ToggleCommandPalette => ("Command palette…", "Toggle the Command Palette"),
286
287 Self::PlaybackTogglePlayPause => ("Toggle play/pause", "Either play or pause the time"),
288 Self::PlaybackFollow => ("Follow", "Follow on from end of timeline"),
289 Self::PlaybackStepBack => (
290 "Step backwards",
291 "Move the time marker back to the previous point in time with any data",
292 ),
293 Self::PlaybackStepForward => (
294 "Step forwards",
295 "Move the time marker to the next point in time with any data",
296 ),
297 Self::PlaybackBack => (
298 "Move backwards",
299 "Move the time marker backward by 1 second",
300 ),
301 Self::PlaybackForward => (
302 "Move forwards",
303 "Move the time marker forward by 0.1 seconds",
304 ),
305 Self::PlaybackBackFast => (
306 "Move backwards fast",
307 "Move the time marker backwards by 1 second",
308 ),
309 Self::PlaybackForwardFast => (
310 "Move forwards fast",
311 "Move the time marker forwards by 0.1 seconds",
312 ),
313 Self::PlaybackBeginning => ("Go to beginning", "Go to beginning of timeline"),
314 Self::PlaybackEnd => ("Go to end", "Go to end of timeline"),
315 Self::PlaybackRestart => ("Restart", "Restart from beginning of timeline"),
316
317 Self::PlaybackSpeed(_) => (
318 "Set playback speed",
319 "This is a chord, so you can press 5+0 to set the speed to 50x",
320 ),
321
322 #[cfg(not(target_arch = "wasm32"))]
323 Self::ScreenshotWholeApp => (
324 "Screenshot",
325 "Copy screenshot of the whole app to clipboard",
326 ),
327 #[cfg(not(target_arch = "wasm32"))]
328 Self::PrintChunkStore => (
329 "Print datastore",
330 "Prints the entire chunk store to the console and clipboard. WARNING: this may be A LOT of text.",
331 ),
332 #[cfg(not(target_arch = "wasm32"))]
333 Self::PrintBlueprintStore => (
334 "Print blueprint store",
335 "Prints the entire blueprint store to the console and clipboard. WARNING: this may be A LOT of text.",
336 ),
337 #[cfg(not(target_arch = "wasm32"))]
338 Self::PrintPrimaryCache => (
339 "Print primary cache",
340 "Prints the state of the entire primary cache to the console and clipboard. WARNING: this may be A LOT of text.",
341 ),
342
343 #[cfg(debug_assertions)]
344 Self::ResetEguiMemory => (
345 "Reset egui memory",
346 "Reset egui memory, useful for debugging UI code.",
347 ),
348
349 Self::Share => ("Share…", "Share the current screen as a link"),
350 Self::CopyDirectLink => (
351 "Copy direct link",
352 "Try to copy a shareable link to the current screen. This is not supported for all data sources & viewer states.",
353 ),
354
355 Self::CopyTimeSelectionLink => (
356 "Copy link to selected time range",
357 "Copy a link to the part of the active recording within the loop selection bounds.",
358 ),
359
360 Self::CopyEntityHierarchy => (
361 "Copy entity hierarchy",
362 "Copy the complete entity hierarchy tree of the currently active recording to the clipboard.",
363 ),
364
365 #[cfg(target_arch = "wasm32")]
366 Self::RestartWithWebGl => (
367 "Restart with WebGL",
368 "Reloads the webpage and force WebGL for rendering. All data will be lost.",
369 ),
370 #[cfg(target_arch = "wasm32")]
371 Self::RestartWithWebGpu => (
372 "Restart with WebGPU",
373 "Reloads the webpage and force WebGPU for rendering. All data will be lost.",
374 ),
375
376 Self::AddRedapServer => (
377 "Connect to a server…",
378 "Connect to a Redap server (experimental)",
379 ),
380 }
381 }
382
383 pub fn kb_shortcuts(self, os: OperatingSystem) -> SmallVec<[KeyboardShortcut; 2]> {
385 fn key(key: Key) -> KeyboardShortcut {
386 KeyboardShortcut::new(Modifiers::NONE, key)
387 }
388
389 fn ctrl(key: Key) -> KeyboardShortcut {
390 KeyboardShortcut::new(Modifiers::CTRL, key)
391 }
392
393 fn cmd(key: Key) -> KeyboardShortcut {
394 KeyboardShortcut::new(Modifiers::COMMAND, key)
395 }
396
397 fn alt(key: Key) -> KeyboardShortcut {
398 KeyboardShortcut::new(Modifiers::ALT, key)
399 }
400
401 fn shift(key: Key) -> KeyboardShortcut {
402 KeyboardShortcut::new(Modifiers::SHIFT, key)
403 }
404
405 fn cmd_shift(key: Key) -> KeyboardShortcut {
406 KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, key)
407 }
408
409 fn cmd_alt(key: Key) -> KeyboardShortcut {
410 KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::ALT, key)
411 }
412
413 fn ctrl_shift(key: Key) -> KeyboardShortcut {
414 KeyboardShortcut::new(Modifiers::CTRL | Modifiers::SHIFT, key)
415 }
416
417 match self {
418 Self::SaveRecording => smallvec![cmd(Key::S)],
419 Self::SaveRecordingSelection => smallvec![cmd_alt(Key::S)],
420 Self::SaveBlueprint => smallvec![],
421 Self::Open => smallvec![cmd(Key::O)],
422 Self::OpenUrl => smallvec![cmd_shift(Key::L)],
426 Self::Import => smallvec![cmd_shift(Key::O)],
427 Self::CloseCurrentRecording => smallvec![],
428 Self::CloseAllEntries => smallvec![],
429
430 Self::NextRecording => smallvec![cmd_alt(Key::ArrowDown)],
431 Self::PreviousRecording => smallvec![cmd_alt(Key::ArrowUp)],
432
433 Self::NavigateBack => smallvec![cmd(Key::OpenBracket)],
434 Self::NavigateForward => smallvec![cmd(Key::CloseBracket)],
435
436 Self::Undo => smallvec![cmd(Key::Z)],
437 Self::Redo => {
438 if os == OperatingSystem::Mac {
439 smallvec![cmd_shift(Key::Z), cmd(Key::Y)]
440 } else {
441 smallvec![ctrl(Key::Y), ctrl_shift(Key::Z)]
442 }
443 }
444
445 #[cfg(not(target_arch = "wasm32"))]
446 Self::Quit => {
447 if os == OperatingSystem::Windows {
448 smallvec![KeyboardShortcut::new(Modifiers::ALT, Key::F4)]
449 } else {
450 smallvec![cmd(Key::Q)]
451 }
452 }
453
454 Self::OpenWebHelp => smallvec![],
455 Self::OpenRerunDiscord => smallvec![],
456
457 Self::ResetViewer => smallvec![ctrl_shift(Key::R)],
458 Self::ClearActiveBlueprint => smallvec![],
459 Self::ClearActiveBlueprintAndEnableHeuristics => smallvec![],
460
461 #[cfg(not(target_arch = "wasm32"))]
462 Self::OpenProfiler => smallvec![ctrl_shift(Key::P)],
463 Self::ToggleMemoryPanel => smallvec![ctrl_shift(Key::M)],
464 Self::TogglePanelStateOverrides => smallvec![],
465 Self::ToggleTopPanel => smallvec![],
466 Self::ToggleBlueprintPanel => smallvec![ctrl_shift(Key::B)],
467 Self::ExpandBlueprintPanel => smallvec![],
468 Self::ToggleSelectionPanel => smallvec![ctrl_shift(Key::S)],
469 Self::ExpandSelectionPanel => smallvec![],
470 Self::ToggleTimePanel => smallvec![ctrl_shift(Key::T)],
471 Self::ToggleChunkStoreBrowser => smallvec![ctrl_shift(Key::D)],
472 Self::Settings => smallvec![cmd(Key::Comma)],
473
474 #[cfg(debug_assertions)]
475 Self::ToggleBlueprintInspectionPanel => smallvec![ctrl_shift(Key::I)],
476
477 #[cfg(debug_assertions)]
478 Self::ToggleEguiDebugPanel => smallvec![ctrl_shift(Key::U)],
479
480 Self::ToggleFullscreen => {
481 if cfg!(target_arch = "wasm32") {
482 smallvec![]
483 } else {
484 smallvec![key(Key::F11)]
485 }
486 }
487
488 #[cfg(not(target_arch = "wasm32"))]
489 Self::ZoomIn => smallvec![egui::gui_zoom::kb_shortcuts::ZOOM_IN],
490 #[cfg(not(target_arch = "wasm32"))]
491 Self::ZoomOut => smallvec![egui::gui_zoom::kb_shortcuts::ZOOM_OUT],
492 #[cfg(not(target_arch = "wasm32"))]
493 Self::ZoomReset => smallvec![egui::gui_zoom::kb_shortcuts::ZOOM_RESET],
494
495 Self::ToggleCommandPalette => smallvec![cmd(Key::P), cmd(Key::K)],
496
497 Self::PlaybackTogglePlayPause => smallvec![key(Key::Space)],
498 Self::PlaybackFollow => smallvec![alt(Key::ArrowRight)],
499 Self::PlaybackStepBack => smallvec![cmd(Key::ArrowLeft)],
500 Self::PlaybackStepForward => smallvec![cmd(Key::ArrowRight)],
501 Self::PlaybackBack => smallvec![key(Key::ArrowLeft)],
502 Self::PlaybackForward => smallvec![key(Key::ArrowRight)],
503 Self::PlaybackBackFast => smallvec![shift(Key::ArrowLeft)],
504 Self::PlaybackForwardFast => smallvec![shift(Key::ArrowRight)],
505 Self::PlaybackBeginning => smallvec![key(Key::Home)],
506 Self::PlaybackEnd => smallvec![key(Key::End)],
507 Self::PlaybackRestart => smallvec![alt(Key::ArrowLeft)],
508
509 Self::PlaybackSpeed(_) => {
510 smallvec![]
512 }
513
514 #[cfg(not(target_arch = "wasm32"))]
515 Self::ScreenshotWholeApp => smallvec![],
516 #[cfg(not(target_arch = "wasm32"))]
517 Self::PrintChunkStore => smallvec![],
518 #[cfg(not(target_arch = "wasm32"))]
519 Self::PrintBlueprintStore => smallvec![],
520 #[cfg(not(target_arch = "wasm32"))]
521 Self::PrintPrimaryCache => smallvec![],
522
523 #[cfg(debug_assertions)]
524 Self::ResetEguiMemory => smallvec![],
525
526 Self::Share => smallvec![cmd(Key::L)],
527 Self::CopyDirectLink => smallvec![],
528
529 Self::CopyTimeSelectionLink => smallvec![],
530
531 Self::CopyEntityHierarchy => smallvec![ctrl_shift(Key::E)],
532
533 #[cfg(target_arch = "wasm32")]
534 Self::RestartWithWebGl => smallvec![],
535 #[cfg(target_arch = "wasm32")]
536 Self::RestartWithWebGpu => smallvec![],
537
538 Self::AddRedapServer => smallvec![],
539 }
540 }
541
542 pub fn primary_kb_shortcut(self, os: OperatingSystem) -> Option<KeyboardShortcut> {
544 self.kb_shortcuts(os).first().copied()
545 }
546
547 pub fn formatted_kb_shortcut(self, egui_ctx: &egui::Context) -> Option<String> {
550 if matches!(self, Self::PlaybackSpeed(_)) {
551 return Some("01-99".to_owned());
552 }
553 self.primary_kb_shortcut(egui_ctx.os())
556 .map(|shortcut| egui_ctx.format_shortcut(&shortcut))
557 }
558
559 pub fn icon(self) -> Option<&'static crate::Icon> {
560 match self {
561 Self::OpenWebHelp => Some(&crate::icons::EXTERNAL_LINK),
562 Self::OpenRerunDiscord => Some(&crate::icons::DISCORD),
563 _ => None,
564 }
565 }
566
567 pub fn is_link(self) -> bool {
568 matches!(self, Self::OpenWebHelp | Self::OpenRerunDiscord)
569 }
570
571 fn handle_playback_chord(ctx: &egui::Context) -> Option<Self> {
572 const CHORD_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(500);
573 const NUMBER_KEYS: [Key; 10] = [
574 Key::Num0,
575 Key::Num1,
576 Key::Num2,
577 Key::Num3,
578 Key::Num4,
579 Key::Num5,
580 Key::Num6,
581 Key::Num7,
582 Key::Num8,
583 Key::Num9,
584 ];
585
586 fn key_to_digit(key: Key) -> Option<char> {
587 let i = NUMBER_KEYS.iter().position(|&k| k == key)?;
588 char::from_digit(i as u32, 10)
589 }
590
591 #[derive(Default, Clone)]
592 struct PlaybackChordState {
593 last_key_time: Option<web_time::Instant>,
594 accumulated: String,
595 }
596
597 if ctx.text_edit_focused() {
598 return None;
599 }
600
601 let mut chord_state = ctx.data_mut(|data| {
602 data.get_temp_mut_or_default::<PlaybackChordState>(Id::NULL)
603 .clone()
604 });
605
606 let now = web_time::Instant::now();
607
608 let pressed_number = ctx.input(|i| {
609 let mut pressed_number = NUMBER_KEYS.iter().find(|&&k| i.key_pressed(k)).copied();
610 let has_other = i.keys_down.iter().any(|k| !NUMBER_KEYS.contains(k));
611
612 if has_other || i.modifiers.any() {
613 chord_state = PlaybackChordState::default();
614 pressed_number = None;
615 }
616
617 pressed_number
618 });
619
620 if let Some(last_time) = chord_state.last_key_time
622 && now.duration_since(last_time) >= CHORD_TIMEOUT
623 {
624 chord_state = PlaybackChordState::default();
625 }
626
627 let mut command = None;
628
629 if let Some(key) = pressed_number {
631 if let Some(digit) = key_to_digit(key) {
632 chord_state.accumulated.push(digit);
633 }
634
635 chord_state.last_key_time = Some(now);
636
637 let leading_zeros = chord_state
640 .accumulated
641 .chars()
642 .take_while(|&c| c == '0')
643 .count();
644
645 let factor = 10usize.pow(leading_zeros as u32);
646
647 if let Ok(speed) = chord_state.accumulated.parse::<f32>()
648 && speed > 0.0
649 {
650 command = Some(Self::PlaybackSpeed(SetPlaybackSpeed(
651 egui::emath::OrderedFloat(speed / factor as f32),
652 )));
653 }
654 }
655
656 ctx.data_mut(|data| data.insert_temp(Id::NULL, chord_state.clone()));
657
658 command
659 }
660
661 #[must_use = "Returns the Command that was triggered by some keyboard shortcut"]
662 pub fn listen_for_kb_shortcut(egui_ctx: &egui::Context) -> Option<Self> {
663 fn conflicts_with_text_editing(kb_shortcut: &KeyboardShortcut) -> bool {
664 kb_shortcut.modifiers.is_none()
666 || matches!(
667 kb_shortcut.logical_key,
668 Key::Space
669 | Key::ArrowLeft
670 | Key::ArrowRight
671 | Key::ArrowUp
672 | Key::ArrowDown
673 | Key::Home
674 | Key::End
675 )
676 }
677
678 use strum::IntoEnumIterator as _;
679
680 let text_edit_has_focus = egui_ctx.text_edit_focused();
681
682 let mut commands: Vec<(KeyboardShortcut, Self)> = Self::iter()
683 .flat_map(|cmd| {
684 cmd.kb_shortcuts(egui_ctx.os())
685 .into_iter()
686 .map(move |kb_shortcut| (kb_shortcut, cmd))
687 })
688 .collect();
689
690 commands.sort_by_key(|(kb_shortcut, _cmd)| {
698 let num_shift_alts =
699 kb_shortcut.modifiers.shift as i32 + kb_shortcut.modifiers.alt as i32;
700 -num_shift_alts });
702
703 let command = egui_ctx.input_mut(|input| {
704 for (kb_shortcut, command) in commands {
705 if text_edit_has_focus && conflicts_with_text_editing(&kb_shortcut) {
706 continue; }
708
709 if input.consume_shortcut(&kb_shortcut) {
710 input.keys_down.remove(&kb_shortcut.logical_key);
712 return Some(command);
713 }
714 }
715 None
716 });
717
718 if command.is_none() {
719 Self::handle_playback_chord(egui_ctx)
720 } else {
721 command
722 }
723 }
724
725 pub fn menu_button_ui(
729 self,
730 ui: &mut egui::Ui,
731 command_sender: &impl UICommandSender,
732 ) -> egui::Response {
733 let button = self.menu_button(ui.ctx());
734 let mut response = ui.add(button).on_hover_text(self.tooltip());
735
736 if self.is_link() {
737 response = response.on_hover_cursor(egui::CursorIcon::PointingHand);
738 }
739
740 if response.clicked() {
741 command_sender.send_ui(self);
742 ui.close();
743 }
744
745 response
746 }
747
748 pub fn menu_button(self, egui_ctx: &egui::Context) -> egui::Button<'static> {
749 let tokens = egui_ctx.tokens();
750
751 let mut button = if let Some(icon) = self.icon() {
752 egui::Button::image_and_text(
753 icon.as_image()
754 .tint(tokens.label_button_icon_color)
755 .fit_to_exact_size(tokens.small_icon_size),
756 self.text(),
757 )
758 } else {
759 egui::Button::new(self.text())
760 };
761
762 if let Some(shortcut_text) = self.formatted_kb_shortcut(egui_ctx) {
763 button = button.shortcut_text(shortcut_text);
764 }
765
766 button
767 }
768
769 pub fn tooltip_ui(self, ui: &mut egui::Ui) {
771 let os = ui.os();
772
773 let (label, details) = self.text_and_tooltip();
774
775 if let Some(shortcut) = self.primary_kb_shortcut(os) {
776 crate::Help::new_without_title()
777 .control(label, crate::IconText::from_keyboard_shortcut(os, shortcut))
778 .ui(ui);
779 } else {
780 ui.label(label);
781 }
782
783 ui.set_max_width(220.0);
784 ui.label(details);
785 }
786}
787
788#[test]
789fn check_for_clashing_command_shortcuts() {
790 fn clashes(a: KeyboardShortcut, b: KeyboardShortcut) -> bool {
791 if a.logical_key != b.logical_key {
792 return false;
793 }
794
795 if a.modifiers.alt != b.modifiers.alt {
796 return false;
797 }
798
799 if a.modifiers.shift != b.modifiers.shift {
800 return false;
801 }
802
803 (a.modifiers.command || a.modifiers.ctrl) == (b.modifiers.command || b.modifiers.ctrl)
805 }
806
807 use strum::IntoEnumIterator as _;
808
809 for os in [
810 OperatingSystem::Mac,
811 OperatingSystem::Windows,
812 OperatingSystem::Nix,
813 ] {
814 for a_cmd in UICommand::iter() {
815 for a_shortcut in a_cmd.kb_shortcuts(os) {
816 for b_cmd in UICommand::iter() {
817 if a_cmd == b_cmd {
818 continue;
819 }
820 for b_shortcut in b_cmd.kb_shortcuts(os) {
821 assert!(
822 !clashes(a_shortcut, b_shortcut),
823 "Command '{a_cmd:?}' and '{b_cmd:?}' have overlapping keyboard shortcuts: {:?} vs {:?}",
824 a_shortcut.format(&egui::ModifierNames::NAMES, true),
825 b_shortcut.format(&egui::ModifierNames::NAMES, true),
826 );
827 }
828 }
829 }
830 }
831 }
832}