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}