rsnaker/graphics/menus/
edge_snake.rs1use ratatui::layout::Rect;
2use ratatui::widgets::Paragraph;
3use std::time::{Duration, Instant};
4
5pub const SPEED_MOVING_SNAKE_SLEEP_TIME_MS: u64 = 50;
6const SEGMENT_GAP: i32 = 1;
9const TOTAL_SEGMENTS: usize = 5;
11
12pub struct EdgeSnake {
15 pub x: u16,
17 pub y: u16,
19 last_update: Instant,
21 frame_duration: Duration,
23}
24
25impl Default for EdgeSnake {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl EdgeSnake {
32 #[must_use]
34 pub fn new() -> Self {
35 Self {
36 x: 0,
37 y: 0,
38 last_update: Instant::now(),
39 frame_duration: Duration::from_millis(SPEED_MOVING_SNAKE_SLEEP_TIME_MS),
40 }
41 }
42
43 pub fn update(&mut self, area: &Rect) {
47 if self.last_update.elapsed() < self.frame_duration {
48 return;
49 }
50 self.last_update = Instant::now();
51
52 let (max_x, max_y) = Self::get_limits(area.width, area.height);
53 if max_x == 0 && max_y == 0 {
54 return;
55 }
56 (self.x, self.y) = Self::step_along_edge(self.x, self.y, max_x, max_y, 1);
57 }
58 pub fn render(&self, frame: &mut ratatui::Frame, area: &Rect) {
59 for (x, y) in self.get_positions(area.width, area.height) {
60 frame.render_widget(Paragraph::new("🐍"), Rect::new(x, y, 2, 1));
61 }
62 }
63
64 fn get_limits(width: u16, height: u16) -> (u16, u16) {
68 let max_x = width.saturating_sub(2);
70 let max_y = height.saturating_sub(1);
71
72 if width <= 2 || height <= 1 {
73 return (0, 0);
74 }
75
76 (max_x, max_y)
77 }
78 #[must_use]
82 pub fn step_along_edge(x: u16, y: u16, max_x: u16, max_y: u16, delta: i32) -> (u16, u16) {
83 let x = x.min(max_x);
86 let mut y = y.min(max_y);
87
88 if x > 0 && x < max_x && y > 0 && y < max_y {
91 y = 0;
92 }
93 let perimeter = 2 * (max_x + max_y);
95 if perimeter == 0 {
96 return (x, y);
97 }
98
99 let index = if y == 0 {
101 x
102 } else if x == max_x {
103 max_x + y
104 } else if y == max_y {
105 max_x + max_y + (max_x - x)
106 } else if x == 0 {
107 2 * max_x + max_y + (max_y - y)
108 } else {
109 return (x, y); };
111
112 let new_index = u16::try_from((i32::from(index) + delta).rem_euclid(i32::from(perimeter)))
114 .expect("Maths error");
115
116 if new_index <= max_x {
118 (new_index, 0)
119 } else if new_index <= max_x + max_y {
120 (max_x, new_index - max_x)
121 } else if new_index <= 2 * max_x + max_y {
122 (max_x - (new_index - max_x - max_y), max_y)
123 } else {
124 (0, max_y - (new_index - 2 * max_x - max_y))
125 }
126 }
127
128 fn is_vertical(x: u16, y: u16, max_x: u16, max_y: u16) -> bool {
130 (x == max_x && y > 0) || (x == 0 && y > 0 && y < max_y)
132 }
133
134 #[must_use]
137 pub fn get_positions(&self, width: u16, height: u16) -> Vec<(u16, u16)> {
138 let (max_x, max_y) = Self::get_limits(width, height);
139 if max_x == 0 && max_y == 0 {
140 return vec![(0, 0); TOTAL_SEGMENTS];
141 }
142
143 let mut positions = Vec::with_capacity(TOTAL_SEGMENTS);
144 let (mut curr_x, mut curr_y) = (self.x, self.y);
145
146 curr_x = curr_x.min(max_x);
148 curr_y = curr_y.min(max_y);
149
150 for i in 0..TOTAL_SEGMENTS {
151 positions.push((curr_x, curr_y));
152
153 if i < TOTAL_SEGMENTS - 1 {
154 let offset: i32 = if Self::is_vertical(curr_x, curr_y, max_x, max_y) {
156 1 + SEGMENT_GAP
157 } else {
158 2 + (SEGMENT_GAP * 2)
159 };
160 (curr_x, curr_y) = Self::step_along_edge(curr_x, curr_y, max_x, max_y, -offset);
162 }
163 }
164
165 positions
166 }
167}