ratatui_toolkit/widgets/markdown_widget/widget/methods/
handle_toc_hover.rs

1//! Handle TOC hover events for interactive expansion and entry highlight.
2
3use crossterm::event::{MouseEvent, MouseEventKind};
4use ratatui::layout::Rect;
5
6use crate::widgets::markdown_widget::extensions::toc::Toc;
7use crate::widgets::markdown_widget::state::toc_state::TocState;
8use crate::widgets::markdown_widget::widget::MarkdownWidget;
9
10impl<'a> MarkdownWidget<'a> {
11    /// Handle mouse move events to detect TOC hover.
12    ///
13    /// Call this method with `MouseEventKind::Moved` events to track
14    /// whether the mouse is hovering over the TOC area and which entry
15    /// is being hovered.
16    ///
17    /// # Arguments
18    ///
19    /// * `event` - The mouse event (should be a Moved event)
20    /// * `area` - The total widget area
21    ///
22    /// # Returns
23    ///
24    /// `true` if the hover state changed (entered/exited hover or hovered entry changed),
25    /// `false` otherwise.
26    ///
27    /// # Example
28    ///
29    /// ```rust,no_run
30    /// // In your event loop:
31    /// if let Event::Mouse(mouse_event) = event {
32    ///     if matches!(mouse_event.kind, MouseEventKind::Moved) {
33    ///         if widget.handle_toc_hover(&mouse_event, area) {
34    ///             // Hover state changed - you may want to redraw
35    ///         }
36    ///     }
37    /// }
38    /// ```
39    pub fn handle_toc_hover(&mut self, event: &MouseEvent, area: Rect) -> bool {
40        // Only process move events
41        if !matches!(event.kind, MouseEventKind::Moved) {
42            return false;
43        }
44
45        // Get the TOC area
46        let toc_area = match self.calculate_toc_area(area) {
47            Some(t_area) => t_area,
48            None => {
49                // TOC not visible, ensure not hovered
50                let changed = self.toc_hovered || self.toc_hovered_entry.is_some();
51                if changed {
52                    self.toc_hovered = false;
53                    self.toc_hovered_entry = None;
54                }
55                return changed;
56            }
57        };
58
59        // Check if mouse is within TOC area horizontally and at or below top
60        // Don't check lower vertical bound - let entry_at_position handle that
61        // based on actual entry count
62        let is_potentially_over_toc = event.column >= toc_area.x
63            && event.column < toc_area.x + toc_area.width
64            && event.row >= toc_area.y;
65
66        let prev_hovered = self.toc_hovered;
67        let prev_entry = self.toc_hovered_entry;
68
69        if is_potentially_over_toc {
70            // Create state from content with entries
71            let auto_state = TocState::from_content(self.content);
72            let toc_state = if let Some(provided) = self.toc_state {
73                if provided.entries.is_empty() {
74                    &auto_state
75                } else {
76                    provided
77                }
78            } else {
79                &auto_state
80            };
81
82            // Try to find an entry at this position
83            // Use compact mode when not hovered, expanded mode when hovered
84            let toc = Toc::new(toc_state)
85                .expanded(self.toc_hovered)
86                .config(self.toc_config.clone());
87
88            let entry = toc.entry_at_position(event.column, event.row, toc_area);
89
90            // Only consider hovering if we found an entry
91            if entry.is_some() {
92                self.toc_hovered = true;
93                self.toc_hovered_entry = entry;
94            } else {
95                self.toc_hovered = false;
96                self.toc_hovered_entry = None;
97            }
98        } else {
99            self.toc_hovered = false;
100            self.toc_hovered_entry = None;
101        }
102
103        // Check if any state changed
104        prev_hovered != self.toc_hovered || prev_entry != self.toc_hovered_entry
105    }
106
107    /// Check if the TOC is currently being hovered.
108    ///
109    /// # Returns
110    ///
111    /// `true` if the mouse is over the TOC, `false` otherwise.
112    pub fn is_toc_hovered(&self) -> bool {
113        self.toc_hovered
114    }
115
116    /// Get the currently hovered TOC entry index.
117    ///
118    /// # Returns
119    ///
120    /// The index of the hovered entry, or `None` if no entry is hovered.
121    pub fn get_toc_hovered_entry(&self) -> Option<usize> {
122        self.toc_hovered_entry
123    }
124
125    /// Set the TOC hover state directly.
126    ///
127    /// Useful for manually controlling hover state in tests or special scenarios.
128    ///
129    /// # Arguments
130    ///
131    /// * `hovered` - Whether the TOC should be considered hovered.
132    pub fn set_toc_hovered(&mut self, hovered: bool) {
133        self.toc_hovered = hovered;
134        if !hovered {
135            self.toc_hovered_entry = None;
136        }
137    }
138
139    /// Get the current TOC scroll offset.
140    ///
141    /// # Returns
142    ///
143    /// The current scroll offset for the TOC list.
144    pub fn get_toc_scroll_offset(&self) -> usize {
145        self.toc_scroll_offset
146    }
147
148    /// Set the TOC scroll offset directly.
149    ///
150    /// # Arguments
151    ///
152    /// * `offset` - The scroll offset for the TOC list.
153    pub fn set_toc_scroll_offset(&mut self, offset: usize) {
154        self.toc_scroll_offset = offset;
155    }
156
157    /// Update the hovered entry based on current mouse position and scroll offset.
158    ///
159    /// Call this after scrolling the TOC to recalculate which entry is under the cursor.
160    ///
161    /// # Arguments
162    ///
163    /// * `x` - Mouse X coordinate
164    /// * `y` - Mouse Y coordinate
165    /// * `toc_area` - The TOC area rect
166    pub fn update_toc_hovered_entry(&mut self, x: u16, y: u16, toc_area: Rect) {
167        // Create state from content with entries
168        let auto_state = TocState::from_content(self.content);
169        let toc_state = if let Some(provided) = self.toc_state {
170            if provided.entries.is_empty() {
171                &auto_state
172            } else {
173                provided
174            }
175        } else {
176            &auto_state
177        };
178
179        let toc = Toc::new(toc_state)
180            .expanded(true) // Use expanded mode for entry detection when hovered
181            .config(self.toc_config.clone()); // Use same config as rendering
182
183        self.toc_hovered_entry = toc.entry_at_position(x, y, toc_area);
184    }
185}