1use ratatui::layout::Rect;
21
22use crate::game::state::GameState;
23use crate::game::tree::coord::TreeCoord;
24use crate::sim::{Action, BuyQty};
25use crate::ui::{HelpAction, Mode, TreeButtonAction};
26
27#[derive(Clone, Debug)]
31pub enum InputEvent {
32 KeyPress { code: KeyCode, mods: Modifiers },
35 MouseDown {
37 col: u16,
38 row: u16,
39 button: MouseButton,
40 mods: Modifiers,
41 },
42 MouseUp {
45 col: u16,
46 row: u16,
47 button: MouseButton,
48 },
49 MouseMoved { col: u16, row: u16 },
52 Wheel {
55 col: u16,
56 row: u16,
57 delta: WheelDelta,
58 },
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum KeyCode {
65 Char(char),
66 Esc,
67 F(u8),
68 Up,
70 Down,
71 Left,
72 Right,
73 Enter,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82pub enum MouseButton {
83 Left,
84 Right,
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
88pub struct Modifiers {
89 pub shift: bool,
90 pub alt: bool,
91 pub ctrl: bool,
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum WheelDelta {
96 Up,
97 Down,
98}
99
100pub struct UiState {
103 pub mode: Mode,
104 pub zoom_idx: usize,
105 pub running: bool,
106 pub last_mouse_pos: Option<(u16, u16)>,
107 pub tree_render: TreeRenderState,
108 pub prestige_confirm_pending: bool,
116}
117
118impl UiState {
119 pub fn new() -> Self {
120 Self {
121 mode: Mode::Game,
122 zoom_idx: 0,
123 running: true,
124 last_mouse_pos: None,
125 tree_render: TreeRenderState::default(),
126 prestige_confirm_pending: false,
127 }
128 }
129}
130
131impl Default for UiState {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137#[derive(Clone, Copy, Debug)]
144pub struct TreeRenderState {
145 pub pan_x: f32,
148 pub pan_y: f32,
149 pub target_pan_x: f32,
152 pub target_pan_y: f32,
153 pub prev_cursor: TreeCoord,
156 pub initialized: bool,
161 pub drag_last: Option<(u16, u16)>,
166}
167
168impl Default for TreeRenderState {
169 fn default() -> Self {
170 Self {
171 pan_x: 0.0,
172 pan_y: 0.0,
173 target_pan_x: 0.0,
174 target_pan_y: 0.0,
175 prev_cursor: TreeCoord::ORIGIN,
176 initialized: false,
177 drag_last: None,
178 }
179 }
180}
181
182pub struct InputContext<'a> {
186 pub fingerer_rows: &'a [(usize, Rect)],
187 pub tree_node_rects: &'a [(crate::game::tree::coord::TreeCoord, Rect)],
188 pub tree_action_button: Option<(TreeButtonAction, Rect, TreeCoord)>,
193 pub help_hits: &'a [(HelpAction, Rect)],
194 pub biscuit_rect: Rect,
195 pub biscuit_focal: (u16, u16),
199 pub powerup_rects: &'a [(u64, Rect)],
205 pub play_area: Rect,
206 pub prestige_reset_rect: Rect,
207 pub prestige_confirm_yes_rect: Rect,
208 pub prestige_confirm_no_rect: Rect,
209 pub debug: bool,
210 pub current: &'a GameState,
211}
212
213impl<'a> InputContext<'a> {
214 pub fn from_layout(
224 layout: &'a crate::ui::DrawOutput,
225 current: &'a GameState,
226 debug: bool,
227 ) -> Self {
228 InputContext {
229 fingerer_rows: &layout.fingerer_rows,
230 tree_node_rects: &layout.tree_node_rects,
231 tree_action_button: layout.tree_action_button,
232 help_hits: &layout.help_hits,
233 biscuit_rect: layout.biscuit_rect,
234 biscuit_focal: layout.biscuit_focal,
235 powerup_rects: &layout.powerup_rects,
236 play_area: layout.play_area,
237 prestige_reset_rect: layout.prestige_reset_rect,
238 prestige_confirm_yes_rect: layout.prestige_confirm_yes_rect,
239 prestige_confirm_no_rect: layout.prestige_confirm_no_rect,
240 debug,
241 current,
242 }
243 }
244}
245
246pub fn process_input_event(
252 ev: InputEvent,
253 ui: &mut UiState,
254 ctx: &InputContext,
255 out: &mut Vec<Action>,
256) {
257 match ev {
258 InputEvent::KeyPress { code, mods } => {
259 if ui.mode == Mode::Tree {
263 ui.tree_render.drag_last = None;
264 }
265 handle_key(code, mods, ui, ctx, out);
266 }
267 InputEvent::MouseDown {
268 col,
269 row,
270 button,
271 mods,
272 } => {
273 ui.last_mouse_pos = Some((col, row));
274 if ui.mode == Mode::Tree && button == MouseButton::Left {
279 ui.tree_render.drag_last = Some((col, row));
280 }
281 if try_help_click(col, row, ui, ctx, out) {
286 return;
287 }
288 handle_click(col, row, button, mods, ui, ctx, out);
289 }
290 InputEvent::MouseUp { col, row, button } => {
291 ui.last_mouse_pos = Some((col, row));
292 if button == MouseButton::Left {
295 ui.tree_render.drag_last = None;
296 }
297 }
298 InputEvent::MouseMoved { col, row } => {
299 if ui.mode == Mode::Tree
305 && let Some((lc, lr)) = ui.tree_render.drag_last
306 {
307 let dx = col as i32 - lc as i32;
308 let dy = row as i32 - lr as i32;
309 if dx != 0 || dy != 0 {
310 let r = &mut ui.tree_render;
311 r.pan_x -= dx as f32;
312 r.pan_y -= dy as f32;
313 r.target_pan_x -= dx as f32;
314 r.target_pan_y -= dy as f32;
315 r.drag_last = Some((col, row));
316 }
317 }
318 ui.last_mouse_pos = Some((col, row));
319 }
320 InputEvent::Wheel { col, row, delta } => {
321 if ui.mode == Mode::Tree {
327 return;
328 }
329 if !in_play_area(col, row, ctx.play_area) {
334 return;
335 }
336 match delta {
337 WheelDelta::Up => ui.zoom_idx = ui.zoom_idx.saturating_sub(1),
338 WheelDelta::Down => {
339 ui.zoom_idx = (ui.zoom_idx + 1).min(crate::ui::biscuit::level_count() - 1);
340 }
341 }
342 }
343 }
344}
345
346fn in_play_area(col: u16, row: u16, play_area: Rect) -> bool {
351 if play_area.width == 0 || play_area.height == 0 {
352 return true;
353 }
354 col >= play_area.x
355 && col < play_area.x + play_area.width
356 && row >= play_area.y
357 && row < play_area.y + play_area.height
358}
359
360fn rect_contains(rect: Rect, col: u16, row: u16) -> bool {
361 rect.width > 0
362 && rect.height > 0
363 && col >= rect.x
364 && col < rect.x + rect.width
365 && row >= rect.y
366 && row < rect.y + rect.height
367}
368
369fn push_grab_most_urgent(ctx: &InputContext, out: &mut Vec<Action>) {
374 if let Some(p) = ctx.current.powerups.iter().min_by_key(|p| p.life_ticks) {
375 out.push(Action::CatchPowerup(p.spawn_id));
376 }
377}
378
379fn click_buy_qty(mods: Modifiers) -> BuyQty {
380 if mods.alt || mods.ctrl {
381 BuyQty::Max
382 } else if mods.shift {
383 BuyQty::Ten
384 } else {
385 BuyQty::One
386 }
387}
388
389fn try_help_click(
393 col: u16,
394 row: u16,
395 ui: &mut UiState,
396 ctx: &InputContext,
397 out: &mut Vec<Action>,
398) -> bool {
399 if rect_contains(ctx.prestige_confirm_yes_rect, col, row) {
404 if ctx.current.prestige_available() > 0 {
405 out.push(Action::PrestigeReset);
406 }
407 ui.prestige_confirm_pending = false;
408 ui.mode = Mode::Game;
409 return true;
410 }
411 if rect_contains(ctx.prestige_confirm_no_rect, col, row) {
412 ui.prestige_confirm_pending = false;
413 return true;
414 }
415 if rect_contains(ctx.prestige_reset_rect, col, row) && ctx.current.prestige_available() > 0 {
416 ui.prestige_confirm_pending = true;
417 return true;
418 }
419 for &(action, rect) in ctx.help_hits {
420 if !rect_contains(rect, col, row) {
421 continue;
422 }
423 match action {
424 HelpAction::OpenMode(target) => {
425 ui.prestige_confirm_pending = false;
430 ui.mode = if ui.mode == target {
431 Mode::Game
432 } else {
433 target
434 };
435 }
436 HelpAction::GrabGolden => {
437 push_grab_most_urgent(ctx, out);
440 }
441 HelpAction::Quit => {
442 ui.running = false;
443 }
444 HelpAction::TreeFocusOrigin => {
445 out.push(Action::TreeFocus(TreeCoord::ORIGIN));
446 }
447 HelpAction::TreeFocusLastBought => {
448 let target = ctx.current.tree.last_bought.unwrap_or(TreeCoord::ORIGIN);
449 out.push(Action::TreeFocus(target));
450 }
451 }
452 return true;
453 }
454 false
455}
456
457fn handle_click(
458 col: u16,
459 row: u16,
460 button: MouseButton,
461 mods: Modifiers,
462 ui: &UiState,
463 ctx: &InputContext,
464 out: &mut Vec<Action>,
465) {
466 if ui.mode == Mode::Tree {
476 if let Some((action, r, captured_cursor)) = ctx.tree_action_button
481 && rect_contains(r, col, row)
482 {
483 match action {
488 TreeButtonAction::Buy => out.push(Action::TreeBuy(captured_cursor)),
489 TreeButtonAction::Refund => out.push(Action::TreeRefund(captured_cursor)),
490 }
491 return;
492 }
493 for &(lot, r) in ctx.tree_node_rects {
494 if rect_contains(r, col, row) {
495 out.push(Action::TreeFocus(lot));
496 if button == MouseButton::Right {
497 let owned = ctx.current.tree.bought.contains(&lot);
498 if owned {
499 out.push(Action::TreeRefund(lot));
503 } else if ctx.current.can_buy_tree_node(lot) {
504 out.push(Action::TreeBuy(lot));
505 }
506 }
507 return;
508 }
509 }
510 let _ = mods;
514 return;
515 }
516
517 for &(id, rect) in ctx.powerup_rects {
526 if rect_contains(rect, col, row) {
527 out.push(Action::CatchPowerup(id));
528 return;
529 }
530 }
531 if rect_contains(ctx.biscuit_rect, col, row) {
535 if button == MouseButton::Left {
536 out.push(Action::Click { col, row });
537 }
538 return;
539 }
540 if ui.mode == Mode::Game {
545 for &(idx, r) in ctx.fingerer_rows {
546 if rect_contains(r, col, row) {
547 let qty = if button == MouseButton::Right {
548 BuyQty::Max
549 } else {
550 click_buy_qty(mods)
551 };
552 out.push(Action::BuyFingerer { idx, qty });
553 return;
554 }
555 }
556 }
557 let _ = mods;
558 if button != MouseButton::Left {
570 return;
571 }
572 if !rect_contains(ctx.play_area, col, row) {
573 return;
574 }
575 if crate::ui::hands::occupied_at(col, row, ctx.biscuit_rect, ctx.biscuit_focal, ctx.current) {
576 return;
577 }
578 out.push(Action::Misclick { col, row });
579}
580
581fn handle_key(
582 code: KeyCode,
583 mods: Modifiers,
584 ui: &mut UiState,
585 ctx: &InputContext,
586 out: &mut Vec<Action>,
587) {
588 match code {
589 KeyCode::Char('q') if crate::platform::CAPABILITIES.can_quit => ui.running = false,
594 KeyCode::Esc => {
599 if ui.mode == Mode::Prestige && ui.prestige_confirm_pending {
603 ui.prestige_confirm_pending = false;
604 } else {
605 match ui.mode {
606 Mode::Game => {}
607 _ => {
608 ui.prestige_confirm_pending = false;
609 ui.mode = Mode::Game;
610 }
611 }
612 }
613 }
614 KeyCode::Char('s') | KeyCode::Char('S') => {
615 ui.prestige_confirm_pending = false;
616 ui.mode = if matches!(ui.mode, Mode::Stats) {
617 Mode::Game
618 } else {
619 Mode::Stats
620 };
621 }
622 KeyCode::Char('a') | KeyCode::Char('A') => {
623 ui.prestige_confirm_pending = false;
624 ui.mode = if matches!(ui.mode, Mode::Achievements) {
625 Mode::Game
626 } else {
627 Mode::Achievements
628 };
629 }
630 KeyCode::Char('t') | KeyCode::Char('T') => {
631 ui.prestige_confirm_pending = false;
632 ui.mode = if matches!(ui.mode, Mode::Tree) {
633 Mode::Game
634 } else {
635 Mode::Tree
636 };
637 }
638 KeyCode::Char('g') | KeyCode::Char('G') => {
644 push_grab_most_urgent(ctx, out);
645 }
646 KeyCode::F(8) if ctx.debug => {
652 out.push(Action::DevForcePowerup(
653 crate::game::powerup::PowerupKind::Lucky,
654 ));
655 }
656 KeyCode::F(2) if ctx.debug => {
657 out.push(Action::DevForcePowerup(
658 crate::game::powerup::PowerupKind::Frenzy,
659 ));
660 }
661 KeyCode::F(3) if ctx.debug => {
662 out.push(Action::DevForcePowerup(
663 crate::game::powerup::PowerupKind::Buff,
664 ));
665 }
666 KeyCode::F(4) if ctx.debug => {
667 out.push(Action::DevAddCuques(1_000_000.0));
668 }
669 KeyCode::F(5) if ctx.debug => {
670 out.push(Action::DevForcePowerup(
671 crate::game::powerup::PowerupKind::GreenCoin,
672 ));
673 }
674 KeyCode::Char('p') | KeyCode::Char('P') => {
675 ui.prestige_confirm_pending = false;
679 ui.mode = if matches!(ui.mode, Mode::Prestige) {
680 Mode::Game
681 } else {
682 Mode::Prestige
683 };
684 }
685 KeyCode::Char('r') | KeyCode::Char('R')
694 if ui.mode == Mode::Prestige
695 && !ui.prestige_confirm_pending
696 && ctx.current.prestige_available() > 0 =>
697 {
698 ui.prestige_confirm_pending = true;
699 }
700 KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter
706 if ui.mode == Mode::Prestige
707 && ui.prestige_confirm_pending
708 && ctx.current.prestige_available() > 0 =>
709 {
710 out.push(Action::PrestigeReset);
711 ui.prestige_confirm_pending = false;
712 ui.mode = Mode::Game;
713 }
714 KeyCode::Char('n') | KeyCode::Char('N')
717 if ui.mode == Mode::Prestige && ui.prestige_confirm_pending =>
718 {
719 ui.prestige_confirm_pending = false;
720 }
721 KeyCode::Char('h') | KeyCode::Char('H') | KeyCode::Left if ui.mode == Mode::Tree => {
725 let c = ctx.current.tree.cursor;
726 out.push(Action::TreeFocus(TreeCoord::new(c.x - 1, c.y)));
727 }
728 KeyCode::Char('l') | KeyCode::Char('L') | KeyCode::Right if ui.mode == Mode::Tree => {
729 let c = ctx.current.tree.cursor;
730 out.push(Action::TreeFocus(TreeCoord::new(c.x + 1, c.y)));
731 }
732 KeyCode::Char('k') | KeyCode::Char('K') | KeyCode::Up if ui.mode == Mode::Tree => {
733 let c = ctx.current.tree.cursor;
734 out.push(Action::TreeFocus(TreeCoord::new(c.x, c.y - 1)));
735 }
736 KeyCode::Char('j') | KeyCode::Char('J') | KeyCode::Down if ui.mode == Mode::Tree => {
737 let c = ctx.current.tree.cursor;
738 out.push(Action::TreeFocus(TreeCoord::new(c.x, c.y + 1)));
739 }
740 KeyCode::Enter if ui.mode == Mode::Tree => {
741 out.push(Action::TreeBuy(ctx.current.tree.cursor));
742 }
743 KeyCode::Char('r') | KeyCode::Char('R') if ui.mode == Mode::Tree => {
744 out.push(Action::TreeRefund(ctx.current.tree.cursor));
745 }
746 KeyCode::Char('0') if ui.mode == Mode::Tree => {
747 out.push(Action::TreeFocus(TreeCoord::ORIGIN));
748 }
749 KeyCode::Char('1') if ui.mode == Mode::Tree => {
750 let target = ctx.current.tree.last_bought.unwrap_or(TreeCoord::ORIGIN);
751 out.push(Action::TreeFocus(target));
752 }
753 KeyCode::Char('+') | KeyCode::Char('=') => {
754 ui.zoom_idx = ui.zoom_idx.saturating_sub(1);
755 }
756 KeyCode::Char('-') | KeyCode::Char('_') => {
757 ui.zoom_idx = (ui.zoom_idx + 1).min(crate::ui::biscuit::level_count() - 1);
758 }
759 KeyCode::Char(' ') => {
762 out.push(Action::ClickCenter);
763 }
764 KeyCode::Char(c) => {
765 if let Some((slot, shifted_sym)) = digit_slot(c) {
766 let buy_10 = shifted_sym || mods.shift;
767 let buy_max = mods.alt || mods.ctrl;
768 match ui.mode {
769 Mode::Game => {
770 if let Some(&(fid, _)) = ctx.fingerer_rows.get(slot) {
771 let qty = if buy_max {
772 BuyQty::Max
773 } else if buy_10 {
774 BuyQty::Ten
775 } else {
776 BuyQty::One
777 };
778 out.push(Action::BuyFingerer { idx: fid, qty });
779 }
780 }
781 _ => {
784 let _ = (slot, buy_10, buy_max);
785 }
786 }
787 }
788 }
789 _ => {}
790 }
791}
792
793fn digit_slot(c: char) -> Option<(usize, bool)> {
794 match c {
795 '1' => Some((0, false)),
796 '2' => Some((1, false)),
797 '3' => Some((2, false)),
798 '4' => Some((3, false)),
799 '5' => Some((4, false)),
800 '6' => Some((5, false)),
801 '7' => Some((6, false)),
802 '8' => Some((7, false)),
803 '9' => Some((8, false)),
804 '0' => Some((9, false)),
805 '!' => Some((0, true)),
806 '@' => Some((1, true)),
807 '#' => Some((2, true)),
808 '$' => Some((3, true)),
809 '%' => Some((4, true)),
810 '^' => Some((5, true)),
811 '&' => Some((6, true)),
812 '*' => Some((7, true)),
813 '(' => Some((8, true)),
814 ')' => Some((9, true)),
815 _ => None,
816 }
817}
818
819#[cfg(test)]
820mod tests {
821 use super::*;
831 use crate::game::powerup::{Powerup, PowerupKind};
832 use crate::sim::{Action, BuyQty};
833 use ratatui::layout::Rect;
834 use std::mem::discriminant;
835
836 fn rect(x: u16, y: u16, w: u16, h: u16) -> Rect {
837 Rect::new(x, y, w, h)
838 }
839
840 #[allow(clippy::too_many_arguments)]
844 fn ctx<'a>(
845 biscuit: Rect,
846 powerup_rects: &'a [(u64, Rect)],
847 play_area: Rect,
848 prestige_reset_rect: Rect,
849 fingerer_rows: &'a [(usize, Rect)],
850 tree_node_rects: &'a [(crate::game::tree::coord::TreeCoord, Rect)],
851 help_hits: &'a [(HelpAction, Rect)],
852 debug: bool,
853 current: &'a GameState,
854 ) -> InputContext<'a> {
855 InputContext {
856 fingerer_rows,
857 tree_node_rects,
858 tree_action_button: None,
859 help_hits,
860 biscuit_rect: biscuit,
861 biscuit_focal: (0, 0),
862 powerup_rects,
863 play_area,
864 prestige_reset_rect,
865 prestige_confirm_yes_rect: Rect::default(),
866 prestige_confirm_no_rect: Rect::default(),
867 debug,
868 current,
869 }
870 }
871
872 fn empty_ctx<'a>(state: &'a GameState) -> InputContext<'a> {
873 ctx(
874 Rect::default(),
875 &[],
876 Rect::default(),
877 Rect::default(),
878 &[],
879 &[],
880 &[],
881 false,
882 state,
883 )
884 }
885
886 fn state_with_lucky() -> (GameState, u64) {
890 let mut s = GameState::default();
891 let id = s.mint_spawn_id();
892 s.powerups.push(Powerup {
893 kind: PowerupKind::Lucky,
894 spawn_id: id,
895 frac_x: 0.5,
896 frac_y: 0.5,
897 life_ticks: PowerupKind::Lucky.lifetime_ticks(),
898 });
899 (s, id)
900 }
901
902 fn state_with_prestige() -> GameState {
905 GameState {
908 lifetime_cuques: crate::bignum::Mag::from_f64(4_000_000_000.0),
909 ..GameState::default()
910 }
911 }
912
913 fn key(code: KeyCode) -> InputEvent {
914 InputEvent::KeyPress {
915 code,
916 mods: Modifiers::default(),
917 }
918 }
919
920 fn key_with(code: KeyCode, shift: bool, alt: bool, ctrl: bool) -> InputEvent {
921 InputEvent::KeyPress {
922 code,
923 mods: Modifiers { shift, alt, ctrl },
924 }
925 }
926
927 fn mouse_down(col: u16, row: u16, button: MouseButton, mods: Modifiers) -> InputEvent {
928 InputEvent::MouseDown {
929 col,
930 row,
931 button,
932 mods,
933 }
934 }
935
936 #[test]
939 fn q_key_flips_running_off() {
940 let s = GameState::default();
941 let mut ui = UiState::new();
942 let mut out = Vec::new();
943 process_input_event(key(KeyCode::Char('q')), &mut ui, &empty_ctx(&s), &mut out);
944 assert!(!ui.running);
945 assert!(out.is_empty());
946 }
947
948 #[test]
949 fn esc_from_game_is_noop() {
950 let s = GameState::default();
952 let mut ui = UiState::new();
953 let mut out = Vec::new();
954 process_input_event(key(KeyCode::Esc), &mut ui, &empty_ctx(&s), &mut out);
955 assert!(ui.running);
956 assert_eq!(ui.mode, Mode::Game);
957 assert!(out.is_empty());
958 }
959
960 #[test]
961 fn esc_from_stats_returns_to_game() {
962 let s = GameState::default();
963 let mut ui = UiState::new();
964 ui.mode = Mode::Stats;
965 let mut out = Vec::new();
966 process_input_event(key(KeyCode::Esc), &mut ui, &empty_ctx(&s), &mut out);
967 assert_eq!(ui.mode, Mode::Game);
968 assert!(out.is_empty());
969 }
970
971 #[test]
972 fn s_key_toggles_stats() {
973 let s = GameState::default();
974 let mut ui = UiState::new();
975 let mut out = Vec::new();
976 process_input_event(key(KeyCode::Char('s')), &mut ui, &empty_ctx(&s), &mut out);
977 assert_eq!(ui.mode, Mode::Stats);
978 process_input_event(key(KeyCode::Char('s')), &mut ui, &empty_ctx(&s), &mut out);
979 assert_eq!(ui.mode, Mode::Game);
980 }
981
982 #[test]
983 fn space_emits_click_center() {
984 let s = GameState::default();
985 let mut ui = UiState::new();
986 let mut out = Vec::new();
987 process_input_event(key(KeyCode::Char(' ')), &mut ui, &empty_ctx(&s), &mut out);
988 assert_eq!(out.len(), 1);
989 assert!(matches!(out[0], Action::ClickCenter));
990 }
991
992 #[test]
993 fn g_with_no_powerup_is_silent() {
994 let s = GameState::default();
995 let mut ui = UiState::new();
996 let mut out = Vec::new();
997 process_input_event(key(KeyCode::Char('g')), &mut ui, &empty_ctx(&s), &mut out);
998 assert!(out.is_empty());
999 }
1000
1001 #[test]
1002 fn g_with_powerup_emits_catch() {
1003 let (s, id) = state_with_lucky();
1004 let mut ui = UiState::new();
1005 let mut out = Vec::new();
1006 process_input_event(key(KeyCode::Char('g')), &mut ui, &empty_ctx(&s), &mut out);
1007 assert!(
1008 matches!(out.as_slice(), [Action::CatchPowerup(spawn_id)] if *spawn_id == id),
1009 "expected CatchPowerup({id}), got {out:?}"
1010 );
1011 }
1012
1013 #[test]
1014 fn fkeys_gated_by_debug() {
1015 let s = GameState::default();
1016 let mut ui = UiState::new();
1017 let mut out = Vec::new();
1019 let c = empty_ctx(&s);
1020 process_input_event(key(KeyCode::F(8)), &mut ui, &c, &mut out);
1021 process_input_event(key(KeyCode::F(2)), &mut ui, &c, &mut out);
1022 process_input_event(key(KeyCode::F(3)), &mut ui, &c, &mut out);
1023 process_input_event(key(KeyCode::F(4)), &mut ui, &c, &mut out);
1024 process_input_event(key(KeyCode::F(5)), &mut ui, &c, &mut out);
1025 assert!(out.is_empty(), "F-keys must be silent when debug=false");
1026 }
1027
1028 #[test]
1029 fn fkeys_active_when_debug() {
1030 let s = GameState::default();
1031 let mut ui = UiState::new();
1032 let c = ctx(
1033 Rect::default(),
1034 &[],
1035 Rect::default(),
1036 Rect::default(),
1037 &[],
1038 &[],
1039 &[],
1040 true, &s,
1042 );
1043 let mut out = Vec::new();
1044 process_input_event(key(KeyCode::F(8)), &mut ui, &c, &mut out);
1045 process_input_event(key(KeyCode::F(4)), &mut ui, &c, &mut out);
1046 process_input_event(key(KeyCode::F(5)), &mut ui, &c, &mut out);
1047 assert!(matches!(
1048 out[0],
1049 Action::DevForcePowerup(PowerupKind::Lucky)
1050 ));
1051 assert!(matches!(out[1], Action::DevAddCuques(_)));
1052 assert!(matches!(
1053 out[2],
1054 Action::DevForcePowerup(PowerupKind::GreenCoin)
1055 ));
1056 }
1057
1058 fn fingerer_row_ctx<'a>(state: &'a GameState, rows: &'a [(usize, Rect)]) -> InputContext<'a> {
1061 ctx(
1062 Rect::default(),
1063 &[],
1064 Rect::default(),
1065 Rect::default(),
1066 rows,
1067 &[],
1068 &[],
1069 false,
1070 state,
1071 )
1072 }
1073
1074 #[test]
1075 fn digit_1_buys_one() {
1076 let s = GameState::default();
1077 let mut ui = UiState::new();
1078 let rows = [(0_usize, rect(0, 0, 1, 1))];
1079 let mut out = Vec::new();
1080 process_input_event(
1081 key(KeyCode::Char('1')),
1082 &mut ui,
1083 &fingerer_row_ctx(&s, &rows),
1084 &mut out,
1085 );
1086 assert!(matches!(
1087 out.as_slice(),
1088 [Action::BuyFingerer {
1089 idx: 0,
1090 qty: BuyQty::One,
1091 }]
1092 ));
1093 }
1094
1095 #[test]
1096 fn shifted_digit_symbol_buys_ten() {
1097 let s = GameState::default();
1100 let mut ui = UiState::new();
1101 let rows = [(0_usize, rect(0, 0, 1, 1))];
1102 let mut out = Vec::new();
1103 process_input_event(
1104 key(KeyCode::Char('!')),
1105 &mut ui,
1106 &fingerer_row_ctx(&s, &rows),
1107 &mut out,
1108 );
1109 assert!(matches!(
1110 out.as_slice(),
1111 [Action::BuyFingerer {
1112 idx: 0,
1113 qty: BuyQty::Ten,
1114 }]
1115 ));
1116 }
1117
1118 #[test]
1119 fn shift_modifier_on_digit_buys_ten() {
1120 let s = GameState::default();
1122 let mut ui = UiState::new();
1123 let rows = [(0_usize, rect(0, 0, 1, 1))];
1124 let mut out = Vec::new();
1125 process_input_event(
1126 key_with(KeyCode::Char('1'), true, false, false),
1127 &mut ui,
1128 &fingerer_row_ctx(&s, &rows),
1129 &mut out,
1130 );
1131 assert!(matches!(
1132 out.as_slice(),
1133 [Action::BuyFingerer {
1134 qty: BuyQty::Ten,
1135 ..
1136 }]
1137 ));
1138 }
1139
1140 #[test]
1141 fn alt_or_ctrl_modifier_on_digit_buys_max() {
1142 let s = GameState::default();
1143 let rows = [(0_usize, rect(0, 0, 1, 1))];
1144 for (alt, ctrl) in [(true, false), (false, true)] {
1145 let mut ui = UiState::new();
1146 let mut out = Vec::new();
1147 process_input_event(
1148 key_with(KeyCode::Char('1'), false, alt, ctrl),
1149 &mut ui,
1150 &fingerer_row_ctx(&s, &rows),
1151 &mut out,
1152 );
1153 assert!(
1154 matches!(
1155 out.as_slice(),
1156 [Action::BuyFingerer {
1157 qty: BuyQty::Max,
1158 ..
1159 }]
1160 ),
1161 "alt={alt} ctrl={ctrl} should buy max",
1162 );
1163 }
1164 }
1165
1166 #[test]
1167 fn digit_with_no_visible_row_is_silent() {
1168 let s = GameState::default();
1171 let mut ui = UiState::new();
1172 let mut out = Vec::new();
1173 process_input_event(
1174 key(KeyCode::Char('1')),
1175 &mut ui,
1176 &fingerer_row_ctx(&s, &[]),
1177 &mut out,
1178 );
1179 assert!(out.is_empty());
1180 }
1181
1182 #[test]
1185 fn left_click_on_biscuit_emits_click() {
1186 let s = GameState::default();
1187 let mut ui = UiState::new();
1188 let c = ctx(
1189 rect(10, 5, 30, 20),
1190 &[],
1191 rect(0, 0, 100, 30),
1192 Rect::default(),
1193 &[],
1194 &[],
1195 &[],
1196 false,
1197 &s,
1198 );
1199 let mut out = Vec::new();
1200 process_input_event(
1201 mouse_down(20, 10, MouseButton::Left, Modifiers::default()),
1202 &mut ui,
1203 &c,
1204 &mut out,
1205 );
1206 assert!(
1207 matches!(out.as_slice(), [Action::Click { col: 20, row: 10 }]),
1208 "got {:?}",
1209 out
1210 );
1211 }
1212
1213 #[test]
1214 fn right_click_on_biscuit_is_noop() {
1215 let s = GameState::default();
1218 let mut ui = UiState::new();
1219 let c = ctx(
1220 rect(10, 5, 30, 20),
1221 &[],
1222 rect(0, 0, 100, 30),
1223 Rect::default(),
1224 &[],
1225 &[],
1226 &[],
1227 false,
1228 &s,
1229 );
1230 let mut out = Vec::new();
1231 process_input_event(
1232 mouse_down(20, 10, MouseButton::Right, Modifiers::default()),
1233 &mut ui,
1234 &c,
1235 &mut out,
1236 );
1237 assert!(out.is_empty(), "got {:?}", out);
1238 }
1239
1240 #[test]
1241 fn left_click_on_powerup_emits_catch() {
1242 let (s, id) = state_with_lucky();
1243 let mut ui = UiState::new();
1244 let powerup_rects = [(id, rect(50, 12, 4, 2))];
1245 let c = ctx(
1246 Rect::default(),
1247 &powerup_rects,
1248 rect(0, 0, 100, 30),
1249 Rect::default(),
1250 &[],
1251 &[],
1252 &[],
1253 false,
1254 &s,
1255 );
1256 let mut out = Vec::new();
1257 process_input_event(
1258 mouse_down(51, 13, MouseButton::Left, Modifiers::default()),
1259 &mut ui,
1260 &c,
1261 &mut out,
1262 );
1263 assert!(
1264 matches!(out.as_slice(), [Action::CatchPowerup(spawn_id)] if *spawn_id == id),
1265 "got {out:?}"
1266 );
1267 }
1268
1269 #[test]
1270 fn left_click_on_powerup_only_catches_one_under_cursor() {
1271 let mut s = GameState::default();
1275 let lucky_id = s.mint_spawn_id();
1276 s.powerups.push(Powerup {
1277 kind: PowerupKind::Lucky,
1278 spawn_id: lucky_id,
1279 frac_x: 0.5,
1280 frac_y: 0.5,
1281 life_ticks: 100,
1282 });
1283 let green_id = s.mint_spawn_id();
1284 s.powerups.push(Powerup {
1285 kind: PowerupKind::GreenCoin,
1286 spawn_id: green_id,
1287 frac_x: 0.5,
1288 frac_y: 0.5,
1289 life_ticks: 100,
1290 });
1291 let mut ui = UiState::new();
1292 let powerup_rects = [
1293 (lucky_id, rect(50, 12, 5, 3)),
1294 (green_id, rect(70, 12, 5, 3)),
1295 ];
1296 let c = ctx(
1297 Rect::default(),
1298 &powerup_rects,
1299 rect(0, 0, 100, 30),
1300 Rect::default(),
1301 &[],
1302 &[],
1303 &[],
1304 false,
1305 &s,
1306 );
1307 let mut out = Vec::new();
1308 process_input_event(
1309 mouse_down(72, 13, MouseButton::Left, Modifiers::default()),
1310 &mut ui,
1311 &c,
1312 &mut out,
1313 );
1314 assert!(
1315 matches!(out.as_slice(), [Action::CatchPowerup(id)] if *id == green_id),
1316 "got {out:?}"
1317 );
1318 }
1319
1320 #[test]
1321 fn g_with_two_kinds_picks_lower_life_ticks() {
1322 let mut s = GameState::default();
1325 let lucky_id = s.mint_spawn_id();
1326 s.powerups.push(Powerup {
1327 kind: PowerupKind::Lucky,
1328 spawn_id: lucky_id,
1329 frac_x: 0.5,
1330 frac_y: 0.5,
1331 life_ticks: 50,
1332 });
1333 let green_id = s.mint_spawn_id();
1334 s.powerups.push(Powerup {
1335 kind: PowerupKind::GreenCoin,
1336 spawn_id: green_id,
1337 frac_x: 0.5,
1338 frac_y: 0.5,
1339 life_ticks: 200,
1340 });
1341 let c = empty_ctx(&s);
1342 let mut ui = UiState::new();
1343 let mut out = Vec::new();
1344 process_input_event(key(KeyCode::Char('g')), &mut ui, &c, &mut out);
1345 assert!(
1346 matches!(out.as_slice(), [Action::CatchPowerup(id)] if *id == lucky_id),
1347 "lower-life Lucky should win, got {out:?}"
1348 );
1349
1350 let mut s = GameState::default();
1352 let lucky_id = s.mint_spawn_id();
1353 s.powerups.push(Powerup {
1354 kind: PowerupKind::Lucky,
1355 spawn_id: lucky_id,
1356 frac_x: 0.5,
1357 frac_y: 0.5,
1358 life_ticks: 200,
1359 });
1360 let green_id = s.mint_spawn_id();
1361 s.powerups.push(Powerup {
1362 kind: PowerupKind::GreenCoin,
1363 spawn_id: green_id,
1364 frac_x: 0.5,
1365 frac_y: 0.5,
1366 life_ticks: 50,
1367 });
1368 let c = empty_ctx(&s);
1369 let mut out = Vec::new();
1370 process_input_event(key(KeyCode::Char('g')), &mut ui, &c, &mut out);
1371 assert!(
1372 matches!(out.as_slice(), [Action::CatchPowerup(id)] if *id == green_id),
1373 "lower-life GreenCoin should win, got {out:?}"
1374 );
1375 }
1376
1377 #[test]
1378 fn right_click_on_powerup_also_catches() {
1379 let (s, id) = state_with_lucky();
1381 let mut ui = UiState::new();
1382 let powerup_rects = [(id, rect(50, 12, 4, 2))];
1383 let c = ctx(
1384 Rect::default(),
1385 &powerup_rects,
1386 rect(0, 0, 100, 30),
1387 Rect::default(),
1388 &[],
1389 &[],
1390 &[],
1391 false,
1392 &s,
1393 );
1394 let mut out = Vec::new();
1395 process_input_event(
1396 mouse_down(51, 13, MouseButton::Right, Modifiers::default()),
1397 &mut ui,
1398 &c,
1399 &mut out,
1400 );
1401 assert!(
1402 matches!(out.as_slice(), [Action::CatchPowerup(spawn_id)] if *spawn_id == id),
1403 "got {out:?}"
1404 );
1405 }
1406
1407 #[test]
1408 fn left_click_on_fingerer_row_buys_one() {
1409 let s = GameState::default();
1410 let mut ui = UiState::new();
1411 let rows = [(2_usize, rect(100, 5, 38, 3))];
1412 let c = ctx(
1413 Rect::default(),
1414 &[],
1415 Rect::default(),
1416 Rect::default(),
1417 &rows,
1418 &[],
1419 &[],
1420 false,
1421 &s,
1422 );
1423 let mut out = Vec::new();
1424 process_input_event(
1425 mouse_down(110, 6, MouseButton::Left, Modifiers::default()),
1426 &mut ui,
1427 &c,
1428 &mut out,
1429 );
1430 assert!(matches!(
1431 out.as_slice(),
1432 [Action::BuyFingerer {
1433 idx: 2,
1434 qty: BuyQty::One,
1435 }]
1436 ));
1437 }
1438
1439 #[test]
1440 fn right_click_on_fingerer_row_buys_max() {
1441 let s = GameState::default();
1443 let mut ui = UiState::new();
1444 let rows = [(2_usize, rect(100, 5, 38, 3))];
1445 let c = ctx(
1446 Rect::default(),
1447 &[],
1448 Rect::default(),
1449 Rect::default(),
1450 &rows,
1451 &[],
1452 &[],
1453 false,
1454 &s,
1455 );
1456 let mut out = Vec::new();
1457 process_input_event(
1458 mouse_down(110, 6, MouseButton::Right, Modifiers::default()),
1459 &mut ui,
1460 &c,
1461 &mut out,
1462 );
1463 assert!(matches!(
1464 out.as_slice(),
1465 [Action::BuyFingerer {
1466 qty: BuyQty::Max,
1467 ..
1468 }]
1469 ));
1470 }
1471
1472 #[test]
1473 fn shift_left_click_on_fingerer_row_buys_ten() {
1474 let s = GameState::default();
1475 let mut ui = UiState::new();
1476 let rows = [(2_usize, rect(100, 5, 38, 3))];
1477 let c = ctx(
1478 Rect::default(),
1479 &[],
1480 Rect::default(),
1481 Rect::default(),
1482 &rows,
1483 &[],
1484 &[],
1485 false,
1486 &s,
1487 );
1488 let mut out = Vec::new();
1489 let mods = Modifiers {
1490 shift: true,
1491 ..Modifiers::default()
1492 };
1493 process_input_event(
1494 mouse_down(110, 6, MouseButton::Left, mods),
1495 &mut ui,
1496 &c,
1497 &mut out,
1498 );
1499 assert!(matches!(
1500 out.as_slice(),
1501 [Action::BuyFingerer {
1502 qty: BuyQty::Ten,
1503 ..
1504 }]
1505 ));
1506 }
1507
1508 #[test]
1511 fn dead_zone_left_click_inside_play_area_emits_misclick() {
1512 let s = GameState::default();
1514 let mut ui = UiState::new();
1515 let c = ctx(
1516 rect(40, 10, 20, 10), &[],
1518 rect(0, 0, 100, 30), Rect::default(),
1520 &[],
1521 &[],
1522 &[],
1523 false,
1524 &s,
1525 );
1526 let mut out = Vec::new();
1527 process_input_event(
1528 mouse_down(5, 5, MouseButton::Left, Modifiers::default()),
1529 &mut ui,
1530 &c,
1531 &mut out,
1532 );
1533 assert!(
1534 matches!(out.as_slice(), [Action::Misclick { col: 5, row: 5 }]),
1535 "got {:?}",
1536 out
1537 );
1538 }
1539
1540 #[test]
1541 fn click_outside_play_area_does_not_misclick() {
1542 let s = GameState::default();
1545 let mut ui = UiState::new();
1546 let c = ctx(
1547 rect(40, 10, 20, 10),
1548 &[],
1549 rect(0, 0, 100, 30), Rect::default(),
1551 &[],
1552 &[],
1553 &[],
1554 false,
1555 &s,
1556 );
1557 let mut out = Vec::new();
1558 process_input_event(
1559 mouse_down(120, 5, MouseButton::Left, Modifiers::default()),
1560 &mut ui,
1561 &c,
1562 &mut out,
1563 );
1564 assert!(out.is_empty(), "got {:?}", out);
1565 }
1566
1567 #[test]
1568 fn right_click_in_dead_zone_is_silent() {
1569 let s = GameState::default();
1571 let mut ui = UiState::new();
1572 let c = ctx(
1573 rect(40, 10, 20, 10),
1574 &[],
1575 rect(0, 0, 100, 30),
1576 Rect::default(),
1577 &[],
1578 &[],
1579 &[],
1580 false,
1581 &s,
1582 );
1583 let mut out = Vec::new();
1584 process_input_event(
1585 mouse_down(5, 5, MouseButton::Right, Modifiers::default()),
1586 &mut ui,
1587 &c,
1588 &mut out,
1589 );
1590 assert!(out.is_empty());
1591 }
1592
1593 #[test]
1596 fn click_quit_hint_flips_running() {
1597 let s = GameState::default();
1598 let mut ui = UiState::new();
1599 let hits = [(HelpAction::Quit, rect(50, 29, 8, 1))];
1600 let c = ctx(
1601 Rect::default(),
1602 &[],
1603 rect(0, 0, 100, 30),
1604 Rect::default(),
1605 &[],
1606 &[],
1607 &hits,
1608 false,
1609 &s,
1610 );
1611 let mut out = Vec::new();
1612 process_input_event(
1613 mouse_down(53, 29, MouseButton::Left, Modifiers::default()),
1614 &mut ui,
1615 &c,
1616 &mut out,
1617 );
1618 assert!(!ui.running);
1619 assert!(out.is_empty(), "Quit is UI-only, no Action emitted");
1620 }
1621
1622 #[test]
1623 fn click_open_mode_hint_toggles_mode() {
1624 let s = GameState::default();
1625 let mut ui = UiState::new();
1626 let hits = [(HelpAction::OpenMode(Mode::Stats), rect(50, 29, 8, 1))];
1627 let c = ctx(
1628 Rect::default(),
1629 &[],
1630 rect(0, 0, 100, 30),
1631 Rect::default(),
1632 &[],
1633 &[],
1634 &hits,
1635 false,
1636 &s,
1637 );
1638 let mut out = Vec::new();
1639 process_input_event(
1640 mouse_down(53, 29, MouseButton::Left, Modifiers::default()),
1641 &mut ui,
1642 &c,
1643 &mut out,
1644 );
1645 assert_eq!(ui.mode, Mode::Stats);
1646 }
1647
1648 #[test]
1651 fn prestige_reset_rect_unavailable_does_not_reset() {
1652 let s = GameState::default(); let mut ui = UiState::new();
1658 let c = ctx(
1659 Rect::default(),
1660 &[],
1661 rect(0, 0, 100, 30),
1662 rect(40, 15, 30, 1), &[],
1664 &[],
1665 &[],
1666 false,
1667 &s,
1668 );
1669 let mut out = Vec::new();
1670 process_input_event(
1671 mouse_down(50, 15, MouseButton::Left, Modifiers::default()),
1672 &mut ui,
1673 &c,
1674 &mut out,
1675 );
1676 assert!(
1677 !out.iter().any(|a| matches!(a, Action::PrestigeReset)),
1678 "no PrestigeReset when unavailable; got {:?}",
1679 out
1680 );
1681 assert_eq!(ui.mode, Mode::Game, "mode unchanged from default Game");
1682 }
1683
1684 #[test]
1685 fn prestige_reset_rect_arms_confirmation_then_yes_emits_action() {
1686 let s = state_with_prestige();
1690 let mut ui = UiState::new();
1691 ui.mode = Mode::Prestige;
1692 let mut c = ctx(
1693 Rect::default(),
1694 &[],
1695 rect(0, 0, 100, 30),
1696 rect(40, 15, 30, 1),
1697 &[],
1698 &[],
1699 &[],
1700 false,
1701 &s,
1702 );
1703 let mut out = Vec::new();
1705 process_input_event(
1706 mouse_down(50, 15, MouseButton::Left, Modifiers::default()),
1707 &mut ui,
1708 &c,
1709 &mut out,
1710 );
1711 assert!(
1712 !out.iter().any(|a| matches!(a, Action::PrestigeReset)),
1713 "first click on reset rect must not emit PrestigeReset; got {:?}",
1714 out
1715 );
1716 assert!(
1717 ui.prestige_confirm_pending,
1718 "first click should arm the confirmation"
1719 );
1720 assert_eq!(ui.mode, Mode::Prestige, "panel stays open while confirming");
1721 c.prestige_confirm_yes_rect = rect(40, 18, 30, 1);
1723 let mut out = Vec::new();
1724 process_input_event(
1725 mouse_down(50, 18, MouseButton::Left, Modifiers::default()),
1726 &mut ui,
1727 &c,
1728 &mut out,
1729 );
1730 assert_eq!(out.len(), 1);
1731 assert_eq!(discriminant(&out[0]), discriminant(&Action::PrestigeReset));
1732 assert!(
1733 !ui.prestige_confirm_pending,
1734 "pending cleared after confirm"
1735 );
1736 assert_eq!(
1737 ui.mode,
1738 Mode::Game,
1739 "panel auto-closes after prestige confirm"
1740 );
1741 }
1742
1743 #[test]
1746 fn wheel_down_inside_play_area_increases_zoom_idx() {
1747 let s = GameState::default();
1748 let mut ui = UiState::new();
1749 let c = ctx(
1750 Rect::default(),
1751 &[],
1752 rect(0, 0, 100, 30),
1753 Rect::default(),
1754 &[],
1755 &[],
1756 &[],
1757 false,
1758 &s,
1759 );
1760 let mut out = Vec::new();
1761 process_input_event(
1762 InputEvent::Wheel {
1763 col: 50,
1764 row: 15,
1765 delta: WheelDelta::Down,
1766 },
1767 &mut ui,
1768 &c,
1769 &mut out,
1770 );
1771 assert_eq!(ui.zoom_idx, 1);
1772 }
1773
1774 #[test]
1775 fn wheel_outside_play_area_does_not_zoom() {
1776 let s = GameState::default();
1778 let mut ui = UiState::new();
1779 let c = ctx(
1780 Rect::default(),
1781 &[],
1782 rect(0, 0, 100, 30),
1783 Rect::default(),
1784 &[],
1785 &[],
1786 &[],
1787 false,
1788 &s,
1789 );
1790 let mut out = Vec::new();
1791 process_input_event(
1792 InputEvent::Wheel {
1793 col: 120,
1794 row: 10,
1795 delta: WheelDelta::Down,
1796 },
1797 &mut ui,
1798 &c,
1799 &mut out,
1800 );
1801 assert_eq!(ui.zoom_idx, 0);
1802 }
1803
1804 #[test]
1805 fn wheel_up_saturates_at_zero() {
1806 let s = GameState::default();
1807 let mut ui = UiState::new();
1808 ui.zoom_idx = 0;
1809 let c = ctx(
1810 Rect::default(),
1811 &[],
1812 rect(0, 0, 100, 30),
1813 Rect::default(),
1814 &[],
1815 &[],
1816 &[],
1817 false,
1818 &s,
1819 );
1820 let mut out = Vec::new();
1821 process_input_event(
1822 InputEvent::Wheel {
1823 col: 50,
1824 row: 15,
1825 delta: WheelDelta::Up,
1826 },
1827 &mut ui,
1828 &c,
1829 &mut out,
1830 );
1831 assert_eq!(ui.zoom_idx, 0, "saturating_sub at 0 stays 0");
1832 }
1833
1834 #[test]
1835 fn wheel_down_caps_at_last_level() {
1836 let s = GameState::default();
1837 let mut ui = UiState::new();
1838 let last = crate::ui::biscuit::level_count() - 1;
1839 ui.zoom_idx = last;
1840 let c = ctx(
1841 Rect::default(),
1842 &[],
1843 rect(0, 0, 100, 30),
1844 Rect::default(),
1845 &[],
1846 &[],
1847 &[],
1848 false,
1849 &s,
1850 );
1851 let mut out = Vec::new();
1852 process_input_event(
1853 InputEvent::Wheel {
1854 col: 50,
1855 row: 15,
1856 delta: WheelDelta::Down,
1857 },
1858 &mut ui,
1859 &c,
1860 &mut out,
1861 );
1862 assert_eq!(ui.zoom_idx, last, "min() cap at last level");
1863 }
1864
1865 #[test]
1868 fn mouse_moved_updates_last_position() {
1869 let s = GameState::default();
1870 let mut ui = UiState::new();
1871 let mut out = Vec::new();
1872 process_input_event(
1873 InputEvent::MouseMoved { col: 42, row: 7 },
1874 &mut ui,
1875 &empty_ctx(&s),
1876 &mut out,
1877 );
1878 assert_eq!(ui.last_mouse_pos, Some((42, 7)));
1879 assert!(out.is_empty());
1880 }
1881
1882 #[test]
1883 fn mouse_down_updates_last_position_before_dispatch() {
1884 let s = GameState::default();
1888 let mut ui = UiState::new();
1889 let c = ctx(
1890 rect(40, 10, 20, 10),
1891 &[],
1892 rect(0, 0, 100, 30),
1893 Rect::default(),
1894 &[],
1895 &[],
1896 &[],
1897 false,
1898 &s,
1899 );
1900 let mut out = Vec::new();
1901 process_input_event(
1902 mouse_down(7, 7, MouseButton::Left, Modifiers::default()),
1903 &mut ui,
1904 &c,
1905 &mut out,
1906 );
1907 assert_eq!(ui.last_mouse_pos, Some((7, 7)));
1908 }
1909}