ratatui_interact/components/
mouse_pointer.rs1use ratatui::{
30 buffer::Buffer,
31 layout::Rect,
32 style::{Color, Style},
33};
34
35#[derive(Debug, Clone, Default)]
39pub struct MousePointerState {
40 pub enabled: bool,
42 pub position: Option<(u16, u16)>,
44}
45
46impl MousePointerState {
47 pub fn new() -> Self {
51 Self::default()
52 }
53
54 pub fn with_enabled(enabled: bool) -> Self {
56 Self {
57 enabled,
58 position: None,
59 }
60 }
61
62 pub fn set_enabled(&mut self, enabled: bool) {
64 self.enabled = enabled;
65 }
66
67 pub fn toggle(&mut self) {
69 self.enabled = !self.enabled;
70 }
71
72 pub fn update_position(&mut self, col: u16, row: u16) {
74 self.position = Some((col, row));
75 }
76
77 pub fn clear_position(&mut self) {
79 self.position = None;
80 }
81
82 pub fn should_render(&self) -> bool {
86 self.enabled && self.position.is_some()
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct MousePointerStyle {
93 pub symbol: &'static str,
95 pub fg: Color,
97 pub bg: Option<Color>,
99}
100
101impl Default for MousePointerStyle {
102 fn default() -> Self {
103 Self {
104 symbol: "█",
105 fg: Color::Yellow,
106 bg: None,
107 }
108 }
109}
110
111impl From<&crate::theme::Theme> for MousePointerStyle {
112 fn from(theme: &crate::theme::Theme) -> Self {
113 let p = &theme.palette;
114 Self {
115 symbol: "█",
116 fg: p.primary,
117 bg: None,
118 }
119 }
120}
121
122impl MousePointerStyle {
123 pub fn crosshair() -> Self {
125 Self {
126 symbol: "┼",
127 fg: Color::Cyan,
128 bg: None,
129 }
130 }
131
132 pub fn arrow() -> Self {
134 Self {
135 symbol: "▶",
136 fg: Color::White,
137 bg: None,
138 }
139 }
140
141 pub fn dot() -> Self {
143 Self {
144 symbol: "●",
145 fg: Color::Green,
146 bg: None,
147 }
148 }
149
150 pub fn plus() -> Self {
152 Self {
153 symbol: "+",
154 fg: Color::Magenta,
155 bg: None,
156 }
157 }
158
159 pub fn custom(symbol: &'static str, fg: Color) -> Self {
161 Self {
162 symbol,
163 fg,
164 bg: None,
165 }
166 }
167
168 pub fn symbol(mut self, symbol: &'static str) -> Self {
170 self.symbol = symbol;
171 self
172 }
173
174 pub fn fg(mut self, fg: Color) -> Self {
176 self.fg = fg;
177 self
178 }
179
180 pub fn bg(mut self, bg: Color) -> Self {
182 self.bg = Some(bg);
183 self
184 }
185}
186
187#[derive(Debug, Clone)]
192pub struct MousePointer<'a> {
193 state: &'a MousePointerState,
195 style: MousePointerStyle,
197}
198
199impl<'a> MousePointer<'a> {
200 pub fn new(state: &'a MousePointerState) -> Self {
202 Self {
203 state,
204 style: MousePointerStyle::default(),
205 }
206 }
207
208 pub fn style(mut self, style: MousePointerStyle) -> Self {
210 self.style = style;
211 self
212 }
213
214 pub fn theme(self, theme: &crate::theme::Theme) -> Self {
216 self.style(MousePointerStyle::from(theme))
217 }
218
219 pub fn render(self, buf: &mut Buffer) {
224 if !self.state.should_render() {
225 return;
226 }
227
228 let (col, row) = self.state.position.unwrap();
229 self.render_at(buf, col, row);
230 }
231
232 pub fn render_in_area(self, buf: &mut Buffer, area: Rect) {
236 if !self.state.should_render() {
237 return;
238 }
239
240 let (col, row) = self.state.position.unwrap();
241
242 if col >= area.x && col < area.x + area.width && row >= area.y && row < area.y + area.height
244 {
245 self.render_at(buf, col, row);
246 }
247 }
248
249 fn render_at(&self, buf: &mut Buffer, col: u16, row: u16) {
251 let buf_area = buf.area();
252
253 if col >= buf_area.x + buf_area.width || row >= buf_area.y + buf_area.height {
255 return;
256 }
257
258 let mut cell_style = Style::default().fg(self.style.fg);
260 if let Some(bg) = self.style.bg {
261 cell_style = cell_style.bg(bg);
262 }
263
264 buf[(col, row)]
266 .set_symbol(self.style.symbol)
267 .set_style(cell_style);
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_state_default() {
277 let state = MousePointerState::default();
278 assert!(!state.enabled);
279 assert!(state.position.is_none());
280 assert!(!state.should_render());
281 }
282
283 #[test]
284 fn test_state_with_enabled() {
285 let state = MousePointerState::with_enabled(true);
286 assert!(state.enabled);
287 assert!(state.position.is_none());
288 assert!(!state.should_render()); }
290
291 #[test]
292 fn test_state_toggle() {
293 let mut state = MousePointerState::default();
294 assert!(!state.enabled);
295
296 state.toggle();
297 assert!(state.enabled);
298
299 state.toggle();
300 assert!(!state.enabled);
301 }
302
303 #[test]
304 fn test_state_position_update() {
305 let mut state = MousePointerState::default();
306 state.set_enabled(true);
307
308 assert!(state.position.is_none());
309
310 state.update_position(10, 5);
311 assert_eq!(state.position, Some((10, 5)));
312 assert!(state.should_render());
313
314 state.clear_position();
315 assert!(state.position.is_none());
316 assert!(!state.should_render());
317 }
318
319 #[test]
320 fn test_should_render() {
321 let mut state = MousePointerState::default();
322
323 assert!(!state.should_render());
325
326 state.set_enabled(true);
328 assert!(!state.should_render());
329
330 state.update_position(5, 5);
332 assert!(state.should_render());
333
334 state.set_enabled(false);
336 assert!(!state.should_render());
337 }
338
339 #[test]
340 fn test_style_default() {
341 let style = MousePointerStyle::default();
342 assert_eq!(style.symbol, "█");
343 assert_eq!(style.fg, Color::Yellow);
344 assert!(style.bg.is_none());
345 }
346
347 #[test]
348 fn test_style_presets() {
349 let crosshair = MousePointerStyle::crosshair();
350 assert_eq!(crosshair.symbol, "┼");
351 assert_eq!(crosshair.fg, Color::Cyan);
352
353 let arrow = MousePointerStyle::arrow();
354 assert_eq!(arrow.symbol, "▶");
355 assert_eq!(arrow.fg, Color::White);
356
357 let dot = MousePointerStyle::dot();
358 assert_eq!(dot.symbol, "●");
359 assert_eq!(dot.fg, Color::Green);
360
361 let plus = MousePointerStyle::plus();
362 assert_eq!(plus.symbol, "+");
363 assert_eq!(plus.fg, Color::Magenta);
364 }
365
366 #[test]
367 fn test_style_custom() {
368 let custom = MousePointerStyle::custom("X", Color::Red);
369 assert_eq!(custom.symbol, "X");
370 assert_eq!(custom.fg, Color::Red);
371 }
372
373 #[test]
374 fn test_style_builder() {
375 let style = MousePointerStyle::default()
376 .symbol("*")
377 .fg(Color::Blue)
378 .bg(Color::White);
379
380 assert_eq!(style.symbol, "*");
381 assert_eq!(style.fg, Color::Blue);
382 assert_eq!(style.bg, Some(Color::White));
383 }
384
385 #[test]
386 fn test_render_disabled() {
387 let state = MousePointerState::default();
388 let pointer = MousePointer::new(&state);
389
390 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
391 pointer.render(&mut buf);
392
393 for y in 0..10 {
395 for x in 0..10 {
396 assert_eq!(buf[(x, y)].symbol(), " ");
397 }
398 }
399 }
400
401 #[test]
402 fn test_render_enabled() {
403 let mut state = MousePointerState::default();
404 state.set_enabled(true);
405 state.update_position(5, 5);
406
407 let pointer = MousePointer::new(&state);
408
409 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
410 pointer.render(&mut buf);
411
412 assert_eq!(buf[(5, 5)].symbol(), "█");
414 }
415
416 #[test]
417 fn test_render_with_custom_style() {
418 let mut state = MousePointerState::default();
419 state.set_enabled(true);
420 state.update_position(3, 3);
421
422 let pointer = MousePointer::new(&state).style(MousePointerStyle::crosshair());
423
424 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
425 pointer.render(&mut buf);
426
427 assert_eq!(buf[(3, 3)].symbol(), "┼");
428 }
429
430 #[test]
431 fn test_render_out_of_bounds() {
432 let mut state = MousePointerState::default();
433 state.set_enabled(true);
434 state.update_position(100, 100); let pointer = MousePointer::new(&state);
437
438 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
439 pointer.render(&mut buf);
440
441 for y in 0..10 {
443 for x in 0..10 {
444 assert_eq!(buf[(x, y)].symbol(), " ");
445 }
446 }
447 }
448
449 #[test]
450 fn test_render_in_area_inside() {
451 let mut state = MousePointerState::default();
452 state.set_enabled(true);
453 state.update_position(5, 5);
454
455 let pointer = MousePointer::new(&state);
456 let area = Rect::new(0, 0, 10, 10);
457
458 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 20));
459 pointer.render_in_area(&mut buf, area);
460
461 assert_eq!(buf[(5, 5)].symbol(), "█");
462 }
463
464 #[test]
465 fn test_render_in_area_outside() {
466 let mut state = MousePointerState::default();
467 state.set_enabled(true);
468 state.update_position(15, 15); let pointer = MousePointer::new(&state);
471 let area = Rect::new(0, 0, 10, 10);
472
473 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 20));
474 pointer.render_in_area(&mut buf, area);
475
476 assert_eq!(buf[(15, 15)].symbol(), " ");
478 }
479
480 #[test]
481 fn test_render_at_boundary() {
482 let mut state = MousePointerState::default();
483 state.set_enabled(true);
484 state.update_position(9, 9); let pointer = MousePointer::new(&state);
487
488 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
489 pointer.render(&mut buf);
490
491 assert_eq!(buf[(9, 9)].symbol(), "█");
492 }
493}