Skip to main content

ratatui_interact/components/
mouse_pointer.rs

1//! Mouse Pointer Indicator Widget
2//!
3//! A visual indicator that displays at the current mouse cursor position.
4//! Useful for debugging mouse interactions or providing visual feedback.
5//!
6//! # Example
7//!
8//! ```rust
9//! use ratatui_interact::components::{MousePointer, MousePointerState, MousePointerStyle};
10//! use ratatui::buffer::Buffer;
11//! use ratatui::layout::Rect;
12//!
13//! // Create state (disabled by default)
14//! let mut state = MousePointerState::default();
15//!
16//! // Enable and update position from mouse event
17//! state.set_enabled(true);
18//! state.update_position(10, 5);
19//!
20//! // Create widget with custom style
21//! let pointer = MousePointer::new(&state)
22//!     .style(MousePointerStyle::crosshair());
23//!
24//! // Render to buffer (usually called last to be on top)
25//! let mut buf = Buffer::empty(Rect::new(0, 0, 80, 24));
26//! pointer.render(&mut buf);
27//! ```
28
29use ratatui::{
30    buffer::Buffer,
31    layout::Rect,
32    style::{Color, Style},
33};
34
35/// State for the mouse pointer indicator.
36///
37/// Tracks whether the pointer is enabled and its current position.
38#[derive(Debug, Clone, Default)]
39pub struct MousePointerState {
40    /// Whether the pointer indicator is visible.
41    pub enabled: bool,
42    /// Current mouse position (column, row). None if not yet set.
43    pub position: Option<(u16, u16)>,
44}
45
46impl MousePointerState {
47    /// Create a new mouse pointer state.
48    ///
49    /// By default, the pointer is disabled.
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Create a new mouse pointer state with enabled status.
55    pub fn with_enabled(enabled: bool) -> Self {
56        Self {
57            enabled,
58            position: None,
59        }
60    }
61
62    /// Set whether the pointer indicator is enabled.
63    pub fn set_enabled(&mut self, enabled: bool) {
64        self.enabled = enabled;
65    }
66
67    /// Toggle the enabled state.
68    pub fn toggle(&mut self) {
69        self.enabled = !self.enabled;
70    }
71
72    /// Update the mouse position.
73    pub fn update_position(&mut self, col: u16, row: u16) {
74        self.position = Some((col, row));
75    }
76
77    /// Clear the stored position.
78    pub fn clear_position(&mut self) {
79        self.position = None;
80    }
81
82    /// Check if the pointer should be rendered.
83    ///
84    /// Returns true if enabled and position is set.
85    pub fn should_render(&self) -> bool {
86        self.enabled && self.position.is_some()
87    }
88}
89
90/// Style configuration for the mouse pointer indicator.
91#[derive(Debug, Clone)]
92pub struct MousePointerStyle {
93    /// Character to display at the pointer position.
94    pub symbol: &'static str,
95    /// Foreground color.
96    pub fg: Color,
97    /// Optional background color.
98    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 MousePointerStyle {
112    /// Create a crosshair style pointer.
113    pub fn crosshair() -> Self {
114        Self {
115            symbol: "┼",
116            fg: Color::Cyan,
117            bg: None,
118        }
119    }
120
121    /// Create an arrow style pointer.
122    pub fn arrow() -> Self {
123        Self {
124            symbol: "▶",
125            fg: Color::White,
126            bg: None,
127        }
128    }
129
130    /// Create a dot style pointer.
131    pub fn dot() -> Self {
132        Self {
133            symbol: "●",
134            fg: Color::Green,
135            bg: None,
136        }
137    }
138
139    /// Create a plus style pointer.
140    pub fn plus() -> Self {
141        Self {
142            symbol: "+",
143            fg: Color::Magenta,
144            bg: None,
145        }
146    }
147
148    /// Create a custom style pointer.
149    pub fn custom(symbol: &'static str, fg: Color) -> Self {
150        Self {
151            symbol,
152            fg,
153            bg: None,
154        }
155    }
156
157    /// Set the symbol.
158    pub fn symbol(mut self, symbol: &'static str) -> Self {
159        self.symbol = symbol;
160        self
161    }
162
163    /// Set the foreground color.
164    pub fn fg(mut self, fg: Color) -> Self {
165        self.fg = fg;
166        self
167    }
168
169    /// Set the background color.
170    pub fn bg(mut self, bg: Color) -> Self {
171        self.bg = Some(bg);
172        self
173    }
174}
175
176/// A widget that renders a mouse pointer indicator.
177///
178/// This widget should be rendered last (on top of other widgets) to ensure
179/// the pointer is visible above all other content.
180#[derive(Debug, Clone)]
181pub struct MousePointer<'a> {
182    /// Reference to the pointer state.
183    state: &'a MousePointerState,
184    /// Style configuration.
185    style: MousePointerStyle,
186}
187
188impl<'a> MousePointer<'a> {
189    /// Create a new mouse pointer widget.
190    pub fn new(state: &'a MousePointerState) -> Self {
191        Self {
192            state,
193            style: MousePointerStyle::default(),
194        }
195    }
196
197    /// Set the style.
198    pub fn style(mut self, style: MousePointerStyle) -> Self {
199        self.style = style;
200        self
201    }
202
203    /// Render the pointer to the buffer at the stored position.
204    ///
205    /// This method renders directly to the buffer without area constraints.
206    /// The pointer will only render if enabled and position is set.
207    pub fn render(self, buf: &mut Buffer) {
208        if !self.state.should_render() {
209            return;
210        }
211
212        let (col, row) = self.state.position.unwrap();
213        self.render_at(buf, col, row);
214    }
215
216    /// Render the pointer within a constrained area.
217    ///
218    /// The pointer will only render if it falls within the given area.
219    pub fn render_in_area(self, buf: &mut Buffer, area: Rect) {
220        if !self.state.should_render() {
221            return;
222        }
223
224        let (col, row) = self.state.position.unwrap();
225
226        // Check if position is within the area
227        if col >= area.x
228            && col < area.x + area.width
229            && row >= area.y
230            && row < area.y + area.height
231        {
232            self.render_at(buf, col, row);
233        }
234    }
235
236    /// Internal method to render at a specific position.
237    fn render_at(&self, buf: &mut Buffer, col: u16, row: u16) {
238        let buf_area = buf.area();
239
240        // Check bounds
241        if col >= buf_area.x + buf_area.width || row >= buf_area.y + buf_area.height {
242            return;
243        }
244
245        // Build style
246        let mut cell_style = Style::default().fg(self.style.fg);
247        if let Some(bg) = self.style.bg {
248            cell_style = cell_style.bg(bg);
249        }
250
251        // Set the cell
252        buf[(col, row)].set_symbol(self.style.symbol).set_style(cell_style);
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_state_default() {
262        let state = MousePointerState::default();
263        assert!(!state.enabled);
264        assert!(state.position.is_none());
265        assert!(!state.should_render());
266    }
267
268    #[test]
269    fn test_state_with_enabled() {
270        let state = MousePointerState::with_enabled(true);
271        assert!(state.enabled);
272        assert!(state.position.is_none());
273        assert!(!state.should_render()); // No position yet
274    }
275
276    #[test]
277    fn test_state_toggle() {
278        let mut state = MousePointerState::default();
279        assert!(!state.enabled);
280
281        state.toggle();
282        assert!(state.enabled);
283
284        state.toggle();
285        assert!(!state.enabled);
286    }
287
288    #[test]
289    fn test_state_position_update() {
290        let mut state = MousePointerState::default();
291        state.set_enabled(true);
292
293        assert!(state.position.is_none());
294
295        state.update_position(10, 5);
296        assert_eq!(state.position, Some((10, 5)));
297        assert!(state.should_render());
298
299        state.clear_position();
300        assert!(state.position.is_none());
301        assert!(!state.should_render());
302    }
303
304    #[test]
305    fn test_should_render() {
306        let mut state = MousePointerState::default();
307
308        // Not enabled, no position
309        assert!(!state.should_render());
310
311        // Enabled, no position
312        state.set_enabled(true);
313        assert!(!state.should_render());
314
315        // Enabled, has position
316        state.update_position(5, 5);
317        assert!(state.should_render());
318
319        // Not enabled, has position
320        state.set_enabled(false);
321        assert!(!state.should_render());
322    }
323
324    #[test]
325    fn test_style_default() {
326        let style = MousePointerStyle::default();
327        assert_eq!(style.symbol, "█");
328        assert_eq!(style.fg, Color::Yellow);
329        assert!(style.bg.is_none());
330    }
331
332    #[test]
333    fn test_style_presets() {
334        let crosshair = MousePointerStyle::crosshair();
335        assert_eq!(crosshair.symbol, "┼");
336        assert_eq!(crosshair.fg, Color::Cyan);
337
338        let arrow = MousePointerStyle::arrow();
339        assert_eq!(arrow.symbol, "▶");
340        assert_eq!(arrow.fg, Color::White);
341
342        let dot = MousePointerStyle::dot();
343        assert_eq!(dot.symbol, "●");
344        assert_eq!(dot.fg, Color::Green);
345
346        let plus = MousePointerStyle::plus();
347        assert_eq!(plus.symbol, "+");
348        assert_eq!(plus.fg, Color::Magenta);
349    }
350
351    #[test]
352    fn test_style_custom() {
353        let custom = MousePointerStyle::custom("X", Color::Red);
354        assert_eq!(custom.symbol, "X");
355        assert_eq!(custom.fg, Color::Red);
356    }
357
358    #[test]
359    fn test_style_builder() {
360        let style = MousePointerStyle::default()
361            .symbol("*")
362            .fg(Color::Blue)
363            .bg(Color::White);
364
365        assert_eq!(style.symbol, "*");
366        assert_eq!(style.fg, Color::Blue);
367        assert_eq!(style.bg, Some(Color::White));
368    }
369
370    #[test]
371    fn test_render_disabled() {
372        let state = MousePointerState::default();
373        let pointer = MousePointer::new(&state);
374
375        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
376        pointer.render(&mut buf);
377
378        // Buffer should be unchanged (all cells should be default)
379        for y in 0..10 {
380            for x in 0..10 {
381                assert_eq!(buf[(x, y)].symbol(), " ");
382            }
383        }
384    }
385
386    #[test]
387    fn test_render_enabled() {
388        let mut state = MousePointerState::default();
389        state.set_enabled(true);
390        state.update_position(5, 5);
391
392        let pointer = MousePointer::new(&state);
393
394        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
395        pointer.render(&mut buf);
396
397        // Check that the pointer was rendered at position (5, 5)
398        assert_eq!(buf[(5, 5)].symbol(), "█");
399    }
400
401    #[test]
402    fn test_render_with_custom_style() {
403        let mut state = MousePointerState::default();
404        state.set_enabled(true);
405        state.update_position(3, 3);
406
407        let pointer = MousePointer::new(&state).style(MousePointerStyle::crosshair());
408
409        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
410        pointer.render(&mut buf);
411
412        assert_eq!(buf[(3, 3)].symbol(), "┼");
413    }
414
415    #[test]
416    fn test_render_out_of_bounds() {
417        let mut state = MousePointerState::default();
418        state.set_enabled(true);
419        state.update_position(100, 100); // Way outside the buffer
420
421        let pointer = MousePointer::new(&state);
422
423        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
424        pointer.render(&mut buf);
425
426        // Buffer should be unchanged - no panic
427        for y in 0..10 {
428            for x in 0..10 {
429                assert_eq!(buf[(x, y)].symbol(), " ");
430            }
431        }
432    }
433
434    #[test]
435    fn test_render_in_area_inside() {
436        let mut state = MousePointerState::default();
437        state.set_enabled(true);
438        state.update_position(5, 5);
439
440        let pointer = MousePointer::new(&state);
441        let area = Rect::new(0, 0, 10, 10);
442
443        let mut buf = Buffer::empty(Rect::new(0, 0, 20, 20));
444        pointer.render_in_area(&mut buf, area);
445
446        assert_eq!(buf[(5, 5)].symbol(), "█");
447    }
448
449    #[test]
450    fn test_render_in_area_outside() {
451        let mut state = MousePointerState::default();
452        state.set_enabled(true);
453        state.update_position(15, 15); // Outside the constrained area
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        // Position is outside the area, so nothing should be rendered
462        assert_eq!(buf[(15, 15)].symbol(), " ");
463    }
464
465    #[test]
466    fn test_render_at_boundary() {
467        let mut state = MousePointerState::default();
468        state.set_enabled(true);
469        state.update_position(9, 9); // At the edge
470
471        let pointer = MousePointer::new(&state);
472
473        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
474        pointer.render(&mut buf);
475
476        assert_eq!(buf[(9, 9)].symbol(), "█");
477    }
478}