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