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 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    /// Create a crosshair style pointer.
124    pub fn crosshair() -> Self {
125        Self {
126            symbol: "┼",
127            fg: Color::Cyan,
128            bg: None,
129        }
130    }
131
132    /// Create an arrow style pointer.
133    pub fn arrow() -> Self {
134        Self {
135            symbol: "▶",
136            fg: Color::White,
137            bg: None,
138        }
139    }
140
141    /// Create a dot style pointer.
142    pub fn dot() -> Self {
143        Self {
144            symbol: "●",
145            fg: Color::Green,
146            bg: None,
147        }
148    }
149
150    /// Create a plus style pointer.
151    pub fn plus() -> Self {
152        Self {
153            symbol: "+",
154            fg: Color::Magenta,
155            bg: None,
156        }
157    }
158
159    /// Create a custom style pointer.
160    pub fn custom(symbol: &'static str, fg: Color) -> Self {
161        Self {
162            symbol,
163            fg,
164            bg: None,
165        }
166    }
167
168    /// Set the symbol.
169    pub fn symbol(mut self, symbol: &'static str) -> Self {
170        self.symbol = symbol;
171        self
172    }
173
174    /// Set the foreground color.
175    pub fn fg(mut self, fg: Color) -> Self {
176        self.fg = fg;
177        self
178    }
179
180    /// Set the background color.
181    pub fn bg(mut self, bg: Color) -> Self {
182        self.bg = Some(bg);
183        self
184    }
185}
186
187/// A widget that renders a mouse pointer indicator.
188///
189/// This widget should be rendered last (on top of other widgets) to ensure
190/// the pointer is visible above all other content.
191#[derive(Debug, Clone)]
192pub struct MousePointer<'a> {
193    /// Reference to the pointer state.
194    state: &'a MousePointerState,
195    /// Style configuration.
196    style: MousePointerStyle,
197}
198
199impl<'a> MousePointer<'a> {
200    /// Create a new mouse pointer widget.
201    pub fn new(state: &'a MousePointerState) -> Self {
202        Self {
203            state,
204            style: MousePointerStyle::default(),
205        }
206    }
207
208    /// Set the style.
209    pub fn style(mut self, style: MousePointerStyle) -> Self {
210        self.style = style;
211        self
212    }
213
214    /// Apply a theme to derive the style.
215    pub fn theme(self, theme: &crate::theme::Theme) -> Self {
216        self.style(MousePointerStyle::from(theme))
217    }
218
219    /// Render the pointer to the buffer at the stored position.
220    ///
221    /// This method renders directly to the buffer without area constraints.
222    /// The pointer will only render if enabled and position is set.
223    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    /// Render the pointer within a constrained area.
233    ///
234    /// The pointer will only render if it falls within the given area.
235    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        // Check if position is within the area
243        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    /// Internal method to render at a specific position.
250    fn render_at(&self, buf: &mut Buffer, col: u16, row: u16) {
251        let buf_area = buf.area();
252
253        // Check bounds
254        if col >= buf_area.x + buf_area.width || row >= buf_area.y + buf_area.height {
255            return;
256        }
257
258        // Build style
259        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        // Set the cell
265        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()); // No position yet
289    }
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        // Not enabled, no position
324        assert!(!state.should_render());
325
326        // Enabled, no position
327        state.set_enabled(true);
328        assert!(!state.should_render());
329
330        // Enabled, has position
331        state.update_position(5, 5);
332        assert!(state.should_render());
333
334        // Not enabled, has position
335        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        // Buffer should be unchanged (all cells should be default)
394        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        // Check that the pointer was rendered at position (5, 5)
413        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); // Way outside the buffer
435
436        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        // Buffer should be unchanged - no panic
442        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); // Outside the constrained area
469
470        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        // Position is outside the area, so nothing should be rendered
477        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); // At the edge
485
486        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}