1use crate::config::Config;
8use crate::core::{HintMatcher, MatchResult, WindowHint};
9use crate::util::TimeoutTracker;
10use smithay_client_toolkit::seat::keyboard::Keysym;
11use std::time::{Duration, Instant};
12
13#[derive(Debug, Clone)]
15pub enum AppState {
16 BorderOnly {
18 start_time: Instant,
20 frame_count: u32,
22 },
23
24 FullOverlay {
26 selected_hint_index: usize,
28 input: String,
30 },
31
32 PendingActivation {
34 hint_index: usize,
36 input: String,
38 timeout: TimeoutTracker,
40 },
41
42 Exiting {
44 result: ActivationResult,
46 },
47}
48
49#[derive(Debug, Clone)]
51pub enum ActivationResult {
52 Window(usize),
54 QuickSwitch,
56 Launch(String),
58 Cancelled,
60}
61
62#[derive(Debug, Clone)]
64pub enum Event {
65 KeyPress {
67 keysym: Keysym,
68 shift: bool,
69 },
70 AltReleased,
72 Tick,
74 FrameCallback,
76 CycleForward,
78 CycleBackward,
79 Configure {
81 width: u32,
82 height: u32,
83 },
84}
85
86#[derive(Debug, Clone, PartialEq)]
88pub enum Action {
89 ScheduleRedraw,
91 Exit,
93}
94
95pub struct Transition {
97 pub new_state: AppState,
98 pub actions: Vec<Action>,
99}
100
101impl AppState {
102 pub fn initial(
107 launcher_mode: bool,
108 hints: &[WindowHint],
109 previous_window_id: Option<&str>,
110 ) -> Self {
111 if launcher_mode {
112 let selected_index = previous_window_id
114 .and_then(|prev_id| hints.iter().position(|h| h.window_id.as_str() == prev_id))
115 .unwrap_or(0);
116
117 tracing::info!(
118 "FullOverlay initial: selected_index={} (previous_window_id={:?})",
119 selected_index,
120 previous_window_id
121 );
122
123 AppState::FullOverlay {
124 selected_hint_index: selected_index,
125 input: String::new(),
126 }
127 } else {
128 AppState::BorderOnly {
129 start_time: Instant::now(),
130 frame_count: 0,
131 }
132 }
133 }
134
135 pub fn handle_event(
137 &self,
138 event: Event,
139 config: &Config,
140 hints: &[WindowHint],
141 previous_window_id: Option<&str>,
142 ) -> Transition {
143 match (self, event) {
144 (
148 AppState::BorderOnly {
149 start_time,
150 frame_count,
151 },
152 Event::FrameCallback,
153 ) => Transition {
154 new_state: AppState::BorderOnly {
155 start_time: *start_time,
156 frame_count: frame_count + 1,
157 },
158 actions: vec![],
159 },
160
161 (
163 AppState::BorderOnly {
164 start_time,
165 frame_count,
166 },
167 Event::Tick,
168 ) => {
169 let elapsed = start_time.elapsed();
170 let delay = Duration::from_millis(config.settings.overlay_delay);
171
172 if elapsed >= delay && *frame_count >= 2 {
174 let selected_index = previous_window_id
176 .and_then(|prev_id| {
177 hints.iter().position(|h| h.window_id.as_str() == prev_id)
178 })
179 .unwrap_or(0);
180
181 Transition {
182 new_state: AppState::FullOverlay {
183 selected_hint_index: selected_index,
184 input: String::new(),
185 },
186 actions: vec![Action::ScheduleRedraw],
187 }
188 } else {
189 Transition {
190 new_state: self.clone(),
191 actions: vec![],
192 }
193 }
194 }
195
196 (AppState::BorderOnly { start_time, .. }, Event::AltReleased) => {
198 let elapsed = start_time.elapsed();
199 let threshold = Duration::from_millis(config.settings.quick_switch_threshold);
200
201 let result = if elapsed < threshold {
202 if let Some(prev_id) = previous_window_id {
204 if let Some((idx, _)) = hints
205 .iter()
206 .enumerate()
207 .find(|(_, h)| h.window_id.as_str() == prev_id)
208 {
209 ActivationResult::Window(idx)
210 } else {
211 ActivationResult::Window(0)
213 }
214 } else {
215 ActivationResult::QuickSwitch
216 }
217 } else {
218 ActivationResult::Window(0)
220 };
221
222 Transition {
223 new_state: AppState::Exiting { result },
224 actions: vec![Action::Exit],
225 }
226 }
227
228 (AppState::BorderOnly { .. }, Event::KeyPress { keysym, shift }) => {
230 if is_tab(keysym) {
231 let idx = if shift {
232 hints.len().saturating_sub(1)
233 } else {
234 1.min(hints.len().saturating_sub(1))
235 };
236 Transition {
237 new_state: AppState::FullOverlay {
238 selected_hint_index: idx,
239 input: String::new(),
240 },
241 actions: vec![Action::ScheduleRedraw],
242 }
243 } else if keysym == Keysym::Escape {
244 Transition {
245 new_state: AppState::Exiting {
246 result: ActivationResult::Cancelled,
247 },
248 actions: vec![Action::Exit],
249 }
250 } else if let Some(c) = keysym_to_char(keysym) {
251 let input = c.to_string();
254 let matcher = HintMatcher::new(hints);
255 match matcher.match_input(&input) {
256 MatchResult::Exact { index, .. } => {
257 let mut timeout = TimeoutTracker::new(config.settings.activation_delay);
259 timeout.start();
260 Transition {
261 new_state: AppState::PendingActivation {
262 hint_index: index,
263 input,
264 timeout,
265 },
266 actions: vec![Action::ScheduleRedraw],
267 }
268 }
269 MatchResult::Partial(_) => {
270 Transition {
272 new_state: AppState::FullOverlay {
273 selected_hint_index: 0,
274 input,
275 },
276 actions: vec![Action::ScheduleRedraw],
277 }
278 }
279 MatchResult::None => {
280 let key_str = c.to_string();
282 if config.launch_config(&key_str).is_some() {
283 Transition {
284 new_state: AppState::Exiting {
285 result: ActivationResult::Launch(key_str),
286 },
287 actions: vec![Action::Exit],
288 }
289 } else {
290 Transition {
292 new_state: AppState::FullOverlay {
293 selected_hint_index: 0,
294 input: String::new(),
295 },
296 actions: vec![Action::ScheduleRedraw],
297 }
298 }
299 }
300 }
301 } else {
302 Transition {
304 new_state: AppState::FullOverlay {
305 selected_hint_index: 0,
306 input: String::new(),
307 },
308 actions: vec![Action::ScheduleRedraw],
309 }
310 }
311 }
312
313 (AppState::BorderOnly { .. }, Event::CycleForward) => Transition {
315 new_state: AppState::FullOverlay {
316 selected_hint_index: 1.min(hints.len().saturating_sub(1)),
317 input: String::new(),
318 },
319 actions: vec![Action::ScheduleRedraw],
320 },
321
322 (AppState::BorderOnly { .. }, Event::CycleBackward) => Transition {
323 new_state: AppState::FullOverlay {
324 selected_hint_index: hints.len().saturating_sub(1),
325 input: String::new(),
326 },
327 actions: vec![Action::ScheduleRedraw],
328 },
329
330 (
334 AppState::FullOverlay {
335 selected_hint_index,
336 input,
337 },
338 Event::KeyPress { keysym, shift },
339 ) if is_tab(keysym) => {
340 let new_idx = cycle_index(*selected_hint_index, hints.len(), !shift);
341 Transition {
342 new_state: AppState::FullOverlay {
343 selected_hint_index: new_idx,
344 input: input.clone(),
345 },
346 actions: vec![Action::ScheduleRedraw],
347 }
348 }
349
350 (
352 AppState::FullOverlay {
353 selected_hint_index,
354 input,
355 },
356 Event::KeyPress { keysym, .. },
357 ) if keysym == Keysym::Down || keysym == Keysym::KP_Down => {
358 let new_idx = cycle_index(*selected_hint_index, hints.len(), true);
359 Transition {
360 new_state: AppState::FullOverlay {
361 selected_hint_index: new_idx,
362 input: input.clone(),
363 },
364 actions: vec![Action::ScheduleRedraw],
365 }
366 }
367
368 (
369 AppState::FullOverlay {
370 selected_hint_index,
371 input,
372 },
373 Event::KeyPress { keysym, .. },
374 ) if keysym == Keysym::Up || keysym == Keysym::KP_Up => {
375 let new_idx = cycle_index(*selected_hint_index, hints.len(), false);
376 Transition {
377 new_state: AppState::FullOverlay {
378 selected_hint_index: new_idx,
379 input: input.clone(),
380 },
381 actions: vec![Action::ScheduleRedraw],
382 }
383 }
384
385 (
387 AppState::FullOverlay {
388 selected_hint_index,
389 ..
390 },
391 Event::KeyPress { keysym, .. },
392 ) if keysym == Keysym::Return || keysym == Keysym::KP_Enter => Transition {
393 new_state: AppState::Exiting {
394 result: ActivationResult::Window(*selected_hint_index),
395 },
396 actions: vec![Action::Exit],
397 },
398
399 (AppState::FullOverlay { .. }, Event::KeyPress { keysym, .. })
401 if keysym == Keysym::Escape =>
402 {
403 Transition {
404 new_state: AppState::Exiting {
405 result: ActivationResult::Cancelled,
406 },
407 actions: vec![Action::Exit],
408 }
409 }
410
411 (
413 AppState::FullOverlay {
414 selected_hint_index,
415 input,
416 },
417 Event::KeyPress { keysym, .. },
418 ) if keysym == Keysym::BackSpace => {
419 let mut new_input = input.clone();
420 new_input.pop();
421 Transition {
422 new_state: AppState::FullOverlay {
423 selected_hint_index: *selected_hint_index,
424 input: new_input,
425 },
426 actions: vec![Action::ScheduleRedraw],
427 }
428 }
429
430 (
432 AppState::FullOverlay {
433 selected_hint_index,
434 input,
435 },
436 Event::KeyPress { keysym, .. },
437 ) => {
438 if let Some(c) = keysym_to_char(keysym) {
439 let mut new_input = input.clone();
440 new_input.push(c);
441
442 let matcher = HintMatcher::new(hints);
443 match matcher.match_input(&new_input) {
444 MatchResult::Exact { index, .. } => {
445 let mut timeout = TimeoutTracker::new(config.settings.activation_delay);
447 timeout.start();
448 Transition {
449 new_state: AppState::PendingActivation {
450 hint_index: index,
451 input: new_input,
452 timeout,
453 },
454 actions: vec![Action::ScheduleRedraw],
455 }
456 }
457 MatchResult::Partial(_) => {
458 Transition {
460 new_state: AppState::FullOverlay {
461 selected_hint_index: *selected_hint_index,
462 input: new_input,
463 },
464 actions: vec![Action::ScheduleRedraw],
465 }
466 }
467 MatchResult::None => {
468 let key_str = c.to_string();
470 if config.launch_config(&key_str).is_some() {
471 Transition {
472 new_state: AppState::Exiting {
473 result: ActivationResult::Launch(key_str),
474 },
475 actions: vec![Action::Exit],
476 }
477 } else {
478 Transition {
480 new_state: AppState::FullOverlay {
481 selected_hint_index: *selected_hint_index,
482 input: input.clone(),
483 },
484 actions: vec![],
485 }
486 }
487 }
488 }
489 } else {
490 Transition {
492 new_state: self.clone(),
493 actions: vec![],
494 }
495 }
496 }
497
498 (
500 AppState::FullOverlay {
501 selected_hint_index,
502 ..
503 },
504 Event::AltReleased,
505 ) => Transition {
506 new_state: AppState::Exiting {
507 result: ActivationResult::Window(*selected_hint_index),
508 },
509 actions: vec![Action::Exit],
510 },
511
512 (
514 AppState::FullOverlay {
515 selected_hint_index,
516 input,
517 },
518 Event::CycleForward,
519 ) => {
520 let new_idx = cycle_index(*selected_hint_index, hints.len(), true);
521 Transition {
522 new_state: AppState::FullOverlay {
523 selected_hint_index: new_idx,
524 input: input.clone(),
525 },
526 actions: vec![Action::ScheduleRedraw],
527 }
528 }
529
530 (
531 AppState::FullOverlay {
532 selected_hint_index,
533 input,
534 },
535 Event::CycleBackward,
536 ) => {
537 let new_idx = cycle_index(*selected_hint_index, hints.len(), false);
538 Transition {
539 new_state: AppState::FullOverlay {
540 selected_hint_index: new_idx,
541 input: input.clone(),
542 },
543 actions: vec![Action::ScheduleRedraw],
544 }
545 }
546
547 (
551 AppState::PendingActivation {
552 hint_index,
553 timeout,
554 ..
555 },
556 Event::Tick,
557 ) => {
558 if timeout.has_elapsed() {
559 Transition {
560 new_state: AppState::Exiting {
561 result: ActivationResult::Window(*hint_index),
562 },
563 actions: vec![Action::Exit],
564 }
565 } else {
566 Transition {
567 new_state: self.clone(),
568 actions: vec![],
569 }
570 }
571 }
572
573 (
575 AppState::PendingActivation {
576 hint_index, input, ..
577 },
578 Event::KeyPress { keysym, .. },
579 ) => {
580 if let Some(c) = keysym_to_char(keysym) {
581 let mut new_input = input.clone();
582 new_input.push(c);
583
584 let matcher = HintMatcher::new(hints);
585 match matcher.match_input(&new_input) {
586 MatchResult::Exact { index, .. } => {
587 let mut timeout = TimeoutTracker::new(config.settings.activation_delay);
589 timeout.start();
590 Transition {
591 new_state: AppState::PendingActivation {
592 hint_index: index,
593 input: new_input,
594 timeout,
595 },
596 actions: vec![Action::ScheduleRedraw],
597 }
598 }
599 MatchResult::Partial(_) => {
600 Transition {
602 new_state: AppState::FullOverlay {
603 selected_hint_index: *hint_index,
604 input: new_input,
605 },
606 actions: vec![Action::ScheduleRedraw],
607 }
608 }
609 MatchResult::None => {
610 Transition {
612 new_state: self.clone(),
613 actions: vec![],
614 }
615 }
616 }
617 } else if keysym == Keysym::Escape {
618 Transition {
619 new_state: AppState::Exiting {
620 result: ActivationResult::Cancelled,
621 },
622 actions: vec![Action::Exit],
623 }
624 } else if keysym == Keysym::BackSpace {
625 let mut new_input = input.clone();
627 new_input.pop();
628 Transition {
629 new_state: AppState::FullOverlay {
630 selected_hint_index: *hint_index,
631 input: new_input,
632 },
633 actions: vec![Action::ScheduleRedraw],
634 }
635 } else {
636 Transition {
637 new_state: self.clone(),
638 actions: vec![],
639 }
640 }
641 }
642
643 (AppState::PendingActivation { hint_index, .. }, Event::AltReleased) => Transition {
645 new_state: AppState::Exiting {
646 result: ActivationResult::Window(*hint_index),
647 },
648 actions: vec![Action::Exit],
649 },
650
651 _ => Transition {
653 new_state: self.clone(),
654 actions: vec![],
655 },
656 }
657 }
658
659 pub fn selected_hint_index(&self) -> usize {
661 match self {
662 AppState::FullOverlay {
663 selected_hint_index,
664 ..
665 } => *selected_hint_index,
666 AppState::PendingActivation { hint_index, .. } => *hint_index,
667 _ => 0,
668 }
669 }
670
671 pub fn input(&self) -> &str {
673 match self {
674 AppState::FullOverlay { input, .. } => input,
675 AppState::PendingActivation { input, .. } => input,
676 _ => "",
677 }
678 }
679
680 pub fn is_full_overlay(&self) -> bool {
682 matches!(
683 self,
684 AppState::FullOverlay { .. } | AppState::PendingActivation { .. }
685 )
686 }
687
688 pub fn is_exiting(&self) -> bool {
690 matches!(self, AppState::Exiting { .. })
691 }
692
693 pub fn activation_result(&self) -> Option<&ActivationResult> {
695 match self {
696 AppState::Exiting { result } => Some(result),
697 _ => None,
698 }
699 }
700}
701
702fn is_tab(keysym: Keysym) -> bool {
705 keysym == Keysym::Tab
706 || keysym == Keysym::ISO_Left_Tab
707 || keysym.raw() == 0xff09
708 || keysym.raw() == 0xfe20
709}
710
711fn cycle_index(current: usize, len: usize, forward: bool) -> usize {
712 if len == 0 {
713 return 0;
714 }
715 if forward {
716 (current + 1) % len
717 } else if current == 0 {
718 len - 1
719 } else {
720 current - 1
721 }
722}
723
724fn keysym_to_char(keysym: Keysym) -> Option<char> {
725 let raw = keysym.raw();
726 if (0x20..=0x7e).contains(&raw) {
728 Some(raw as u8 as char)
729 } else {
730 None
731 }
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737 use crate::core::{HintSequence, WindowId};
738
739 fn make_test_config() -> Config {
744 let mut config = Config::default();
745 config.settings.overlay_delay = 500;
746 config.settings.activation_delay = 200;
747 config.settings.quick_switch_threshold = 250;
748 config
749 }
750
751 fn make_hints(count: usize) -> Vec<WindowHint> {
753 (0..count)
754 .map(|i| WindowHint {
755 hint: HintSequence::new((b'a' + i as u8) as char, 1),
756 app_id: format!("app{}", i),
757 window_id: WindowId::new(format!("window{}", i)),
758 title: format!("Window {}", i),
759 index: i,
760 })
761 .collect()
762 }
763
764 fn make_realistic_hints() -> Vec<WindowHint> {
766 vec![
767 WindowHint {
768 hint: HintSequence::new('e', 1),
769 app_id: "microsoft-edge".to_string(),
770 window_id: WindowId::new("win-edge-abc123"),
771 title: "Microsoft Edge".to_string(),
772 index: 0,
773 },
774 WindowHint {
775 hint: HintSequence::new('f', 1),
776 app_id: "firefox".to_string(),
777 window_id: WindowId::new("win-firefox-def456"),
778 title: "Mozilla Firefox".to_string(),
779 index: 1,
780 },
781 WindowHint {
782 hint: HintSequence::new('g', 1),
783 app_id: "com.mitchellh.ghostty".to_string(),
784 window_id: WindowId::new("win-ghostty-ghi789"),
785 title: "ghostty".to_string(),
786 index: 2,
787 },
788 ]
789 }
790
791 #[test]
796 fn test_initial_state_switcher_mode() {
797 let hints = make_hints(3);
798 let state = AppState::initial(false, &hints, None);
799 assert!(
800 matches!(state, AppState::BorderOnly { .. }),
801 "Switcher mode starts in BorderOnly state"
802 );
803 }
804
805 #[test]
806 fn test_initial_state_launcher_mode() {
807 let hints = make_hints(3);
808 let state = AppState::initial(true, &hints, None);
809 match state {
810 AppState::FullOverlay {
811 selected_hint_index,
812 input,
813 } => {
814 assert_eq!(
815 selected_hint_index, 0,
816 "Starts with first item selected when no previous window"
817 );
818 assert!(input.is_empty(), "Starts with empty input");
819 }
820 _ => panic!("Launcher mode starts in FullOverlay state"),
821 }
822 }
823
824 #[test]
825 fn test_initial_state_launcher_mode_with_previous() {
826 let hints = make_realistic_hints();
827 let state = AppState::initial(true, &hints, Some("win-firefox-def456"));
829 match state {
830 AppState::FullOverlay {
831 selected_hint_index,
832 input,
833 } => {
834 assert_eq!(
835 selected_hint_index, 1,
836 "Starts with previous window selected"
837 );
838 assert!(input.is_empty(), "Starts with empty input");
839 }
840 _ => panic!("Launcher mode starts in FullOverlay state"),
841 }
842 }
843
844 #[test]
845 fn test_initial_state_launcher_mode_with_invalid_previous() {
846 let hints = make_realistic_hints();
847 let state = AppState::initial(true, &hints, Some("nonexistent-window"));
849 match state {
850 AppState::FullOverlay {
851 selected_hint_index,
852 input,
853 } => {
854 assert_eq!(
855 selected_hint_index, 0,
856 "Falls back to index 0 when previous window not found"
857 );
858 assert!(input.is_empty(), "Starts with empty input");
859 }
860 _ => panic!("Launcher mode starts in FullOverlay state"),
861 }
862 }
863
864 #[test]
869 fn test_cycle_forward() {
870 assert_eq!(cycle_index(0, 3, true), 1);
871 assert_eq!(cycle_index(1, 3, true), 2);
872 assert_eq!(cycle_index(2, 3, true), 0, "Wraps to 0");
873 }
874
875 #[test]
876 fn test_cycle_backward() {
877 assert_eq!(cycle_index(2, 3, false), 1);
878 assert_eq!(cycle_index(1, 3, false), 0);
879 assert_eq!(cycle_index(0, 3, false), 2, "Wraps to last");
880 }
881
882 #[test]
883 fn test_cycle_empty() {
884 assert_eq!(cycle_index(0, 0, true), 0, "Empty list returns 0");
885 assert_eq!(cycle_index(0, 0, false), 0);
886 }
887
888 #[test]
889 fn test_keysym_to_char_letters() {
890 assert_eq!(keysym_to_char(Keysym::from(0x67)), Some('g'));
892 assert_eq!(keysym_to_char(Keysym::from(0x66)), Some('f'));
893 assert_eq!(keysym_to_char(Keysym::from(0x65)), Some('e'));
894 }
895
896 #[test]
897 fn test_keysym_to_char_non_printable() {
898 assert_eq!(keysym_to_char(Keysym::Tab), None);
899 assert_eq!(keysym_to_char(Keysym::Escape), None);
900 assert_eq!(keysym_to_char(Keysym::Return), None);
901 }
902
903 #[test]
904 fn test_is_tab() {
905 assert!(is_tab(Keysym::Tab));
906 assert!(is_tab(Keysym::ISO_Left_Tab));
907 assert!(is_tab(Keysym::from(0xff09)));
908 assert!(is_tab(Keysym::from(0xfe20)));
909 assert!(!is_tab(Keysym::Return));
910 assert!(!is_tab(Keysym::from(0x67)));
911 }
912
913 #[test]
918 fn test_border_only_tick_before_delay() {
919 let config = make_test_config();
920 let hints = make_realistic_hints();
921
922 let state = AppState::BorderOnly {
923 start_time: Instant::now(),
924 frame_count: 5,
925 };
926
927 let transition = state.handle_event(Event::Tick, &config, &hints, None);
928
929 assert!(
930 matches!(transition.new_state, AppState::BorderOnly { .. }),
931 "Remains in BorderOnly state before delay elapsed"
932 );
933 assert!(
934 transition.actions.is_empty(),
935 "No redraw scheduled before delay elapsed"
936 );
937 }
938
939 #[test]
940 fn test_border_only_tick_after_delay_transitions() {
941 let config = make_test_config();
942 let hints = make_realistic_hints();
943
944 let state = AppState::BorderOnly {
946 start_time: Instant::now() - Duration::from_millis(600),
947 frame_count: 5,
948 };
949
950 let transition = state.handle_event(Event::Tick, &config, &hints, None);
951
952 match transition.new_state {
953 AppState::FullOverlay {
954 selected_hint_index,
955 input,
956 } => {
957 assert_eq!(selected_hint_index, 0);
958 assert!(input.is_empty());
959 }
960 _ => panic!("Transitions to FullOverlay after delay"),
961 }
962 assert!(transition.actions.contains(&Action::ScheduleRedraw));
963 }
964
965 #[test]
966 fn test_border_only_requires_minimum_frames() {
967 let config = make_test_config();
968 let hints = make_realistic_hints();
969
970 let state = AppState::BorderOnly {
972 start_time: Instant::now() - Duration::from_millis(600),
973 frame_count: 1, };
975
976 let transition = state.handle_event(Event::Tick, &config, &hints, None);
977
978 assert!(
979 matches!(transition.new_state, AppState::BorderOnly { .. }),
980 "Does not transition without minimum frames rendered"
981 );
982 }
983
984 #[test]
985 fn test_border_only_frame_callback_increments_counter() {
986 let config = make_test_config();
987 let hints = make_realistic_hints();
988
989 let state = AppState::BorderOnly {
990 start_time: Instant::now(),
991 frame_count: 0,
992 };
993
994 let transition = state.handle_event(Event::FrameCallback, &config, &hints, None);
995
996 match transition.new_state {
997 AppState::BorderOnly { frame_count, .. } => {
998 assert_eq!(frame_count, 1, "Frame count increments");
999 }
1000 _ => panic!("Remains in BorderOnly state"),
1001 }
1002 }
1003
1004 #[test]
1005 fn test_border_only_quick_alt_release_with_previous_window() {
1006 let config = make_test_config();
1007 let hints = make_realistic_hints();
1008
1009 let state = AppState::BorderOnly {
1011 start_time: Instant::now(),
1012 frame_count: 0,
1013 };
1014
1015 let transition = state.handle_event(
1016 Event::AltReleased,
1017 &config,
1018 &hints,
1019 Some("win-firefox-def456"),
1020 );
1021
1022 match transition.new_state {
1023 AppState::Exiting {
1024 result: ActivationResult::Window(idx),
1025 } => {
1026 assert_eq!(idx, 1, "Activates firefox at index 1");
1027 }
1028 _ => panic!("Exits with window activation result"),
1029 }
1030 assert!(transition.actions.contains(&Action::Exit));
1031 }
1032
1033 #[test]
1034 fn test_border_only_quick_alt_release_no_previous() {
1035 let config = make_test_config();
1036 let hints = make_realistic_hints();
1037
1038 let state = AppState::BorderOnly {
1039 start_time: Instant::now(),
1040 frame_count: 0,
1041 };
1042
1043 let transition = state.handle_event(Event::AltReleased, &config, &hints, None);
1044
1045 match transition.new_state {
1046 AppState::Exiting {
1047 result: ActivationResult::QuickSwitch,
1048 } => {}
1049 _ => panic!("Exits with QuickSwitch when no previous window"),
1050 }
1051 }
1052
1053 #[test]
1054 fn test_border_only_slow_alt_release_activates_first() {
1055 let config = make_test_config();
1056 let hints = make_realistic_hints();
1057
1058 let state = AppState::BorderOnly {
1060 start_time: Instant::now() - Duration::from_millis(300),
1061 frame_count: 0,
1062 };
1063
1064 let transition = state.handle_event(Event::AltReleased, &config, &hints, None);
1065
1066 match transition.new_state {
1067 AppState::Exiting {
1068 result: ActivationResult::Window(idx),
1069 } => {
1070 assert_eq!(idx, 0, "Activates first window");
1071 }
1072 _ => panic!("Exits with window 0 activation"),
1073 }
1074 }
1075
1076 #[test]
1077 fn test_border_only_tab_transitions_to_full() {
1078 let config = make_test_config();
1079 let hints = make_realistic_hints();
1080
1081 let state = AppState::BorderOnly {
1082 start_time: Instant::now(),
1083 frame_count: 0,
1084 };
1085
1086 let transition = state.handle_event(
1087 Event::KeyPress {
1088 keysym: Keysym::Tab,
1089 shift: false,
1090 },
1091 &config,
1092 &hints,
1093 None,
1094 );
1095
1096 match transition.new_state {
1097 AppState::FullOverlay {
1098 selected_hint_index,
1099 ..
1100 } => {
1101 assert_eq!(selected_hint_index, 1, "Tab selects index 1");
1102 }
1103 _ => panic!("Tab transitions to FullOverlay"),
1104 }
1105 }
1106
1107 #[test]
1108 fn test_border_only_shift_tab_selects_last() {
1109 let config = make_test_config();
1110 let hints = make_realistic_hints();
1111
1112 let state = AppState::BorderOnly {
1113 start_time: Instant::now(),
1114 frame_count: 0,
1115 };
1116
1117 let transition = state.handle_event(
1118 Event::KeyPress {
1119 keysym: Keysym::Tab,
1120 shift: true,
1121 },
1122 &config,
1123 &hints,
1124 None,
1125 );
1126
1127 match transition.new_state {
1128 AppState::FullOverlay {
1129 selected_hint_index,
1130 ..
1131 } => {
1132 assert_eq!(selected_hint_index, 2, "Shift+Tab selects last");
1133 }
1134 _ => panic!("Shift+Tab transitions to FullOverlay"),
1135 }
1136 }
1137
1138 #[test]
1139 fn test_border_only_character_key_goes_to_pending_on_exact_match() {
1140 let config = make_test_config();
1141 let hints = make_realistic_hints();
1142
1143 let state = AppState::BorderOnly {
1144 start_time: Instant::now(),
1145 frame_count: 0,
1146 };
1147
1148 let transition = state.handle_event(
1150 Event::KeyPress {
1151 keysym: Keysym::from(0x67), shift: false,
1153 },
1154 &config,
1155 &hints,
1156 None,
1157 );
1158
1159 match transition.new_state {
1160 AppState::PendingActivation {
1161 hint_index, input, ..
1162 } => {
1163 assert_eq!(hint_index, 2, "Matches ghostty at index 2");
1164 assert_eq!(input, "g");
1165 }
1166 _ => panic!(
1167 "Character key with exact match transitions to PendingActivation, got {:?}",
1168 transition.new_state
1169 ),
1170 }
1171 }
1172
1173 #[test]
1174 fn test_border_only_escape_cancels() {
1175 let config = make_test_config();
1176 let hints = make_realistic_hints();
1177
1178 let state = AppState::BorderOnly {
1179 start_time: Instant::now(),
1180 frame_count: 0,
1181 };
1182
1183 let transition = state.handle_event(
1184 Event::KeyPress {
1185 keysym: Keysym::Escape,
1186 shift: false,
1187 },
1188 &config,
1189 &hints,
1190 None,
1191 );
1192
1193 assert!(matches!(
1194 transition.new_state,
1195 AppState::Exiting {
1196 result: ActivationResult::Cancelled
1197 }
1198 ));
1199 }
1200
1201 #[test]
1206 fn test_full_overlay_tab_cycles_selection() {
1207 let config = make_test_config();
1208 let hints = make_hints(3);
1209 let state = AppState::FullOverlay {
1210 selected_hint_index: 0,
1211 input: String::new(),
1212 };
1213
1214 let transition = state.handle_event(
1215 Event::KeyPress {
1216 keysym: Keysym::Tab,
1217 shift: false,
1218 },
1219 &config,
1220 &hints,
1221 None,
1222 );
1223
1224 match transition.new_state {
1225 AppState::FullOverlay {
1226 selected_hint_index,
1227 ..
1228 } => {
1229 assert_eq!(selected_hint_index, 1);
1230 }
1231 _ => panic!("Expected FullOverlay"),
1232 }
1233 }
1234
1235 #[test]
1236 fn test_full_overlay_down_arrow_cycles() {
1237 let config = make_test_config();
1238 let hints = make_hints(3);
1239 let state = AppState::FullOverlay {
1240 selected_hint_index: 0,
1241 input: String::new(),
1242 };
1243
1244 let transition = state.handle_event(
1245 Event::KeyPress {
1246 keysym: Keysym::Down,
1247 shift: false,
1248 },
1249 &config,
1250 &hints,
1251 None,
1252 );
1253
1254 match transition.new_state {
1255 AppState::FullOverlay {
1256 selected_hint_index,
1257 ..
1258 } => {
1259 assert_eq!(selected_hint_index, 1);
1260 }
1261 _ => panic!("Down arrow should cycle selection"),
1262 }
1263 }
1264
1265 #[test]
1266 fn test_full_overlay_up_arrow_cycles() {
1267 let config = make_test_config();
1268 let hints = make_hints(3);
1269 let state = AppState::FullOverlay {
1270 selected_hint_index: 1,
1271 input: String::new(),
1272 };
1273
1274 let transition = state.handle_event(
1275 Event::KeyPress {
1276 keysym: Keysym::Up,
1277 shift: false,
1278 },
1279 &config,
1280 &hints,
1281 None,
1282 );
1283
1284 match transition.new_state {
1285 AppState::FullOverlay {
1286 selected_hint_index,
1287 ..
1288 } => {
1289 assert_eq!(selected_hint_index, 0);
1290 }
1291 _ => panic!("Up arrow should cycle selection"),
1292 }
1293 }
1294
1295 #[test]
1296 fn test_full_overlay_enter_activates_selected() {
1297 let config = make_test_config();
1298 let hints = make_hints(3);
1299 let state = AppState::FullOverlay {
1300 selected_hint_index: 2,
1301 input: String::new(),
1302 };
1303
1304 let transition = state.handle_event(
1305 Event::KeyPress {
1306 keysym: Keysym::Return,
1307 shift: false,
1308 },
1309 &config,
1310 &hints,
1311 None,
1312 );
1313
1314 match transition.new_state {
1315 AppState::Exiting {
1316 result: ActivationResult::Window(idx),
1317 } => {
1318 assert_eq!(idx, 2);
1319 }
1320 _ => panic!("Enter should activate selected window"),
1321 }
1322 }
1323
1324 #[test]
1325 fn test_full_overlay_escape_cancels() {
1326 let config = make_test_config();
1327 let hints = make_hints(3);
1328 let state = AppState::FullOverlay {
1329 selected_hint_index: 0,
1330 input: String::new(),
1331 };
1332
1333 let transition = state.handle_event(
1334 Event::KeyPress {
1335 keysym: Keysym::Escape,
1336 shift: false,
1337 },
1338 &config,
1339 &hints,
1340 None,
1341 );
1342
1343 assert!(matches!(
1344 transition.new_state,
1345 AppState::Exiting {
1346 result: ActivationResult::Cancelled
1347 }
1348 ));
1349 }
1350
1351 #[test]
1352 fn test_full_overlay_backspace_removes_char() {
1353 let config = make_test_config();
1354 let hints = make_hints(3);
1355 let state = AppState::FullOverlay {
1356 selected_hint_index: 0,
1357 input: "ab".to_string(),
1358 };
1359
1360 let transition = state.handle_event(
1361 Event::KeyPress {
1362 keysym: Keysym::BackSpace,
1363 shift: false,
1364 },
1365 &config,
1366 &hints,
1367 None,
1368 );
1369
1370 match transition.new_state {
1371 AppState::FullOverlay { input, .. } => {
1372 assert_eq!(input, "a");
1373 }
1374 _ => panic!("Backspace should stay in FullOverlay"),
1375 }
1376 }
1377
1378 #[test]
1379 fn test_full_overlay_alt_released_activates() {
1380 let config = make_test_config();
1381 let hints = make_hints(3);
1382 let state = AppState::FullOverlay {
1383 selected_hint_index: 1,
1384 input: String::new(),
1385 };
1386
1387 let transition = state.handle_event(Event::AltReleased, &config, &hints, None);
1388
1389 match transition.new_state {
1390 AppState::Exiting {
1391 result: ActivationResult::Window(idx),
1392 } => {
1393 assert_eq!(idx, 1);
1394 }
1395 _ => panic!("Alt release should activate selected window"),
1396 }
1397 }
1398
1399 #[test]
1400 fn test_full_overlay_character_exact_match_goes_pending() {
1401 let config = make_test_config();
1402 let hints = make_realistic_hints();
1403 let state = AppState::FullOverlay {
1404 selected_hint_index: 0,
1405 input: String::new(),
1406 };
1407
1408 let transition = state.handle_event(
1410 Event::KeyPress {
1411 keysym: Keysym::from(0x66), shift: false,
1413 },
1414 &config,
1415 &hints,
1416 None,
1417 );
1418
1419 match transition.new_state {
1420 AppState::PendingActivation {
1421 hint_index, input, ..
1422 } => {
1423 assert_eq!(hint_index, 1, "Should match firefox");
1424 assert_eq!(input, "f");
1425 }
1426 _ => panic!("Exact match should go to PendingActivation"),
1427 }
1428 }
1429
1430 #[test]
1431 fn test_full_overlay_ipc_cycle_forward() {
1432 let config = make_test_config();
1433 let hints = make_hints(3);
1434 let state = AppState::FullOverlay {
1435 selected_hint_index: 0,
1436 input: String::new(),
1437 };
1438
1439 let transition = state.handle_event(Event::CycleForward, &config, &hints, None);
1440
1441 match transition.new_state {
1442 AppState::FullOverlay {
1443 selected_hint_index,
1444 ..
1445 } => {
1446 assert_eq!(selected_hint_index, 1);
1447 }
1448 _ => panic!("CycleForward should update selection"),
1449 }
1450 }
1451
1452 #[test]
1453 fn test_full_overlay_ipc_cycle_backward() {
1454 let config = make_test_config();
1455 let hints = make_hints(3);
1456 let state = AppState::FullOverlay {
1457 selected_hint_index: 1,
1458 input: String::new(),
1459 };
1460
1461 let transition = state.handle_event(Event::CycleBackward, &config, &hints, None);
1462
1463 match transition.new_state {
1464 AppState::FullOverlay {
1465 selected_hint_index,
1466 ..
1467 } => {
1468 assert_eq!(selected_hint_index, 0);
1469 }
1470 _ => panic!("CycleBackward should update selection"),
1471 }
1472 }
1473
1474 #[test]
1479 fn test_pending_activation_timeout_activates() {
1480 let config = make_test_config();
1481 let hints = make_realistic_hints();
1482
1483 let mut timeout = TimeoutTracker::new(config.settings.activation_delay);
1485 timeout.start();
1486 let state = AppState::PendingActivation {
1488 hint_index: 2,
1489 input: "g".to_string(),
1490 timeout,
1491 };
1492 std::thread::sleep(Duration::from_millis(250));
1494
1495 let transition = state.handle_event(Event::Tick, &config, &hints, None);
1496
1497 match transition.new_state {
1498 AppState::Exiting {
1499 result: ActivationResult::Window(idx),
1500 } => {
1501 assert_eq!(idx, 2);
1502 }
1503 _ => panic!("Timeout should activate window"),
1504 }
1505 }
1506
1507 #[test]
1508 fn test_pending_activation_no_timeout_yet() {
1509 let config = make_test_config();
1510 let hints = make_realistic_hints();
1511
1512 let mut timeout = TimeoutTracker::new(config.settings.activation_delay);
1513 timeout.start();
1514 let state = AppState::PendingActivation {
1515 hint_index: 2,
1516 input: "g".to_string(),
1517 timeout,
1518 };
1519
1520 let transition = state.handle_event(Event::Tick, &config, &hints, None);
1521
1522 assert!(
1523 matches!(transition.new_state, AppState::PendingActivation { .. }),
1524 "Should stay pending before timeout"
1525 );
1526 }
1527
1528 #[test]
1529 fn test_pending_activation_escape_cancels() {
1530 let config = make_test_config();
1531 let hints = make_realistic_hints();
1532
1533 let mut timeout = TimeoutTracker::new(config.settings.activation_delay);
1534 timeout.start();
1535 let state = AppState::PendingActivation {
1536 hint_index: 2,
1537 input: "g".to_string(),
1538 timeout,
1539 };
1540
1541 let transition = state.handle_event(
1542 Event::KeyPress {
1543 keysym: Keysym::Escape,
1544 shift: false,
1545 },
1546 &config,
1547 &hints,
1548 None,
1549 );
1550
1551 assert!(matches!(
1552 transition.new_state,
1553 AppState::Exiting {
1554 result: ActivationResult::Cancelled
1555 }
1556 ));
1557 }
1558
1559 #[test]
1560 fn test_pending_activation_backspace_returns_to_full() {
1561 let config = make_test_config();
1562 let hints = make_realistic_hints();
1563
1564 let mut timeout = TimeoutTracker::new(config.settings.activation_delay);
1565 timeout.start();
1566 let state = AppState::PendingActivation {
1567 hint_index: 2,
1568 input: "g".to_string(),
1569 timeout,
1570 };
1571
1572 let transition = state.handle_event(
1573 Event::KeyPress {
1574 keysym: Keysym::BackSpace,
1575 shift: false,
1576 },
1577 &config,
1578 &hints,
1579 None,
1580 );
1581
1582 match transition.new_state {
1583 AppState::FullOverlay { input, .. } => {
1584 assert!(input.is_empty(), "Backspace should remove char");
1585 }
1586 _ => panic!("Backspace should return to FullOverlay"),
1587 }
1588 }
1589
1590 #[test]
1591 fn test_pending_activation_alt_release_activates_immediately() {
1592 let config = make_test_config();
1593 let hints = make_realistic_hints();
1594
1595 let mut timeout = TimeoutTracker::new(config.settings.activation_delay);
1596 timeout.start();
1597 let state = AppState::PendingActivation {
1598 hint_index: 1,
1599 input: "f".to_string(),
1600 timeout,
1601 };
1602
1603 let transition = state.handle_event(Event::AltReleased, &config, &hints, None);
1604
1605 match transition.new_state {
1606 AppState::Exiting {
1607 result: ActivationResult::Window(idx),
1608 } => {
1609 assert_eq!(idx, 1);
1610 }
1611 _ => panic!("Alt release should activate immediately"),
1612 }
1613 }
1614
1615 #[test]
1620 fn test_scenario_launcher_type_g_wait_activate() {
1621 let config = make_test_config();
1622 let hints = make_realistic_hints();
1623
1624 let mut state = AppState::initial(true, &hints, None);
1626 assert!(
1627 matches!(state, AppState::FullOverlay { .. }),
1628 "Launcher starts in FullOverlay"
1629 );
1630
1631 let trans = state.handle_event(
1633 Event::KeyPress {
1634 keysym: Keysym::from(0x67),
1635 shift: false,
1636 },
1637 &config,
1638 &hints,
1639 None,
1640 );
1641 state = trans.new_state;
1642 assert!(
1643 matches!(state, AppState::PendingActivation { hint_index: 2, .. }),
1644 "Should match ghostty (index 2)"
1645 );
1646
1647 std::thread::sleep(Duration::from_millis(250));
1649
1650 let trans = state.handle_event(Event::Tick, &config, &hints, None);
1652 state = trans.new_state;
1653
1654 match state {
1655 AppState::Exiting {
1656 result: ActivationResult::Window(idx),
1657 } => {
1658 assert_eq!(idx, 2, "Should activate ghostty");
1659 }
1660 _ => panic!("Should exit with window activation"),
1661 }
1662 }
1663
1664 #[test]
1665 fn test_scenario_switcher_quick_alt_tab() {
1666 let config = make_test_config();
1667 let hints = make_realistic_hints();
1668
1669 let state = AppState::initial(false, &hints, None);
1671 assert!(matches!(state, AppState::BorderOnly { .. }));
1672
1673 let trans = state.handle_event(
1675 Event::AltReleased,
1676 &config,
1677 &hints,
1678 Some("win-firefox-def456"),
1679 );
1680
1681 match trans.new_state {
1682 AppState::Exiting {
1683 result: ActivationResult::Window(idx),
1684 } => {
1685 assert_eq!(idx, 1, "Should switch to previous window (firefox)");
1686 }
1687 _ => panic!("Quick Alt+Tab should activate previous window"),
1688 }
1689 }
1690
1691 #[test]
1692 fn test_scenario_switcher_hold_and_tab() {
1693 let config = make_test_config();
1694 let hints = make_realistic_hints();
1695
1696 let mut state = AppState::initial(false, &hints, None);
1698
1699 let trans = state.handle_event(
1701 Event::KeyPress {
1702 keysym: Keysym::Tab,
1703 shift: false,
1704 },
1705 &config,
1706 &hints,
1707 None,
1708 );
1709 state = trans.new_state;
1710
1711 match &state {
1712 AppState::FullOverlay {
1713 selected_hint_index,
1714 ..
1715 } => {
1716 assert_eq!(*selected_hint_index, 1);
1717 }
1718 _ => panic!("Tab should transition to FullOverlay"),
1719 }
1720
1721 let trans = state.handle_event(
1723 Event::KeyPress {
1724 keysym: Keysym::Tab,
1725 shift: false,
1726 },
1727 &config,
1728 &hints,
1729 None,
1730 );
1731 state = trans.new_state;
1732
1733 match &state {
1734 AppState::FullOverlay {
1735 selected_hint_index,
1736 ..
1737 } => {
1738 assert_eq!(*selected_hint_index, 2);
1739 }
1740 _ => panic!("Second Tab should update selection"),
1741 }
1742
1743 let trans = state.handle_event(Event::AltReleased, &config, &hints, None);
1745
1746 match trans.new_state {
1747 AppState::Exiting {
1748 result: ActivationResult::Window(idx),
1749 } => {
1750 assert_eq!(idx, 2, "Should activate final selection");
1751 }
1752 _ => panic!("Alt release should activate"),
1753 }
1754 }
1755
1756 #[test]
1757 fn test_scenario_launcher_arrow_navigate_enter() {
1758 let config = make_test_config();
1759 let hints = make_realistic_hints();
1760
1761 let mut state = AppState::initial(true, &hints, None);
1763
1764 let trans = state.handle_event(
1766 Event::KeyPress {
1767 keysym: Keysym::Down,
1768 shift: false,
1769 },
1770 &config,
1771 &hints,
1772 None,
1773 );
1774 state = trans.new_state;
1775
1776 let trans = state.handle_event(
1777 Event::KeyPress {
1778 keysym: Keysym::Down,
1779 shift: false,
1780 },
1781 &config,
1782 &hints,
1783 None,
1784 );
1785 state = trans.new_state;
1786
1787 let trans = state.handle_event(
1789 Event::KeyPress {
1790 keysym: Keysym::Return,
1791 shift: false,
1792 },
1793 &config,
1794 &hints,
1795 None,
1796 );
1797
1798 match trans.new_state {
1799 AppState::Exiting {
1800 result: ActivationResult::Window(idx),
1801 } => {
1802 assert_eq!(idx, 2, "Should activate third item (down, down from 0)");
1803 }
1804 _ => panic!("Enter should activate"),
1805 }
1806 }
1807
1808 #[test]
1809 fn test_scenario_escape_at_any_stage() {
1810 let config = make_test_config();
1811 let hints = make_realistic_hints();
1812
1813 let mut timeout = TimeoutTracker::new(config.settings.activation_delay);
1815 timeout.start();
1816 let states = vec![
1817 AppState::initial(false, &hints, None), AppState::initial(true, &hints, None), AppState::PendingActivation {
1820 hint_index: 0,
1821 input: "e".to_string(),
1822 timeout,
1823 },
1824 ];
1825
1826 for state in states {
1827 let trans = state.handle_event(
1828 Event::KeyPress {
1829 keysym: Keysym::Escape,
1830 shift: false,
1831 },
1832 &config,
1833 &hints,
1834 None,
1835 );
1836
1837 assert!(
1838 matches!(
1839 trans.new_state,
1840 AppState::Exiting {
1841 result: ActivationResult::Cancelled
1842 }
1843 ),
1844 "Escape should cancel from any state"
1845 );
1846 }
1847 }
1848
1849 #[test]
1854 fn test_selected_hint_index() {
1855 assert_eq!(
1856 AppState::FullOverlay {
1857 selected_hint_index: 5,
1858 input: String::new()
1859 }
1860 .selected_hint_index(),
1861 5
1862 );
1863 let mut timeout = TimeoutTracker::new(200);
1864 timeout.start();
1865 assert_eq!(
1866 AppState::PendingActivation {
1867 hint_index: 3,
1868 input: "x".to_string(),
1869 timeout
1870 }
1871 .selected_hint_index(),
1872 3
1873 );
1874 assert_eq!(
1875 AppState::BorderOnly {
1876 start_time: Instant::now(),
1877 frame_count: 0
1878 }
1879 .selected_hint_index(),
1880 0
1881 );
1882 }
1883
1884 #[test]
1885 fn test_input_accessor() {
1886 assert_eq!(
1887 AppState::FullOverlay {
1888 selected_hint_index: 0,
1889 input: "abc".to_string()
1890 }
1891 .input(),
1892 "abc"
1893 );
1894 let mut timeout = TimeoutTracker::new(200);
1895 timeout.start();
1896 assert_eq!(
1897 AppState::PendingActivation {
1898 hint_index: 0,
1899 input: "xyz".to_string(),
1900 timeout
1901 }
1902 .input(),
1903 "xyz"
1904 );
1905 assert_eq!(
1906 AppState::BorderOnly {
1907 start_time: Instant::now(),
1908 frame_count: 0
1909 }
1910 .input(),
1911 ""
1912 );
1913 }
1914
1915 #[test]
1916 fn test_is_full_overlay() {
1917 assert!(
1918 !AppState::BorderOnly {
1919 start_time: Instant::now(),
1920 frame_count: 0
1921 }
1922 .is_full_overlay()
1923 );
1924 assert!(
1925 AppState::FullOverlay {
1926 selected_hint_index: 0,
1927 input: String::new()
1928 }
1929 .is_full_overlay()
1930 );
1931 let mut timeout = TimeoutTracker::new(200);
1932 timeout.start();
1933 assert!(
1934 AppState::PendingActivation {
1935 hint_index: 0,
1936 input: String::new(),
1937 timeout
1938 }
1939 .is_full_overlay()
1940 );
1941 assert!(
1942 !AppState::Exiting {
1943 result: ActivationResult::Cancelled
1944 }
1945 .is_full_overlay()
1946 );
1947 }
1948
1949 #[test]
1950 fn test_is_exiting() {
1951 assert!(
1952 !AppState::BorderOnly {
1953 start_time: Instant::now(),
1954 frame_count: 0
1955 }
1956 .is_exiting()
1957 );
1958 assert!(
1959 !AppState::FullOverlay {
1960 selected_hint_index: 0,
1961 input: String::new()
1962 }
1963 .is_exiting()
1964 );
1965 assert!(
1966 AppState::Exiting {
1967 result: ActivationResult::Cancelled
1968 }
1969 .is_exiting()
1970 );
1971 }
1972
1973 #[test]
1974 fn test_activation_result_accessor() {
1975 assert!(
1976 AppState::BorderOnly {
1977 start_time: Instant::now(),
1978 frame_count: 0
1979 }
1980 .activation_result()
1981 .is_none()
1982 );
1983
1984 let state = AppState::Exiting {
1985 result: ActivationResult::Window(5),
1986 };
1987 match state.activation_result() {
1988 Some(ActivationResult::Window(idx)) => assert_eq!(*idx, 5),
1989 _ => panic!("Should return Window(5)"),
1990 }
1991 }
1992}