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
//! Handle TOC click events for scroll-to-heading navigation.
use crossterm::event::{MouseButton, 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 a click on the TOC to scroll to the selected heading.
///
/// # Arguments
///
/// * `event` - The mouse event
/// * `area` - The total widget area
///
/// # Returns
///
/// `true` if the click was handled (was on a TOC entry), `false` otherwise.
///
/// # Example
///
/// ```rust,no_run
/// // In your event loop:
/// if let Event::Mouse(mouse_event) = event {
/// if matches!(mouse_event.kind, MouseEventKind::Down(MouseButton::Left)) {
/// if widget.handle_toc_click(&mouse_event, area) {
/// // Click was handled - you may want to redraw
/// }
/// }
/// }
/// ```
pub fn handle_toc_click(&mut self, event: &MouseEvent, area: Rect) -> bool {
// Only handle left clicks
if !matches!(event.kind, MouseEventKind::Down(MouseButton::Left)) {
return false;
}
// Get the TOC area
let toc_area = match self.calculate_toc_area(area) {
Some(t_area) => t_area,
None => return false,
};
// Check horizontal bounds and if above TOC
// Don't check lower vertical bound - let entry_at_position handle that
// based on actual entry count
if event.column < toc_area.x
|| event.column >= toc_area.x + toc_area.width
|| event.row < toc_area.y
{
return false;
}
// 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
};
// Create a TOC to find the clicked entry
let toc = Toc::new(toc_state)
.expanded(self.toc_hovered) // Use current expansion state
.config(self.toc_config.clone());
// Find which entry was clicked
if let Some(entry_idx) = toc.entry_at_position(event.column, event.row, toc_area) {
// Get the target line number
if let Some(target_line) = toc.click_to_line(entry_idx) {
// Scroll to the heading (a bit above for context)
let new_offset = target_line.saturating_sub(2);
// Clamp to valid range
let max_offset = self
.scroll
.total_lines
.saturating_sub(self.scroll.viewport_height);
self.scroll.scroll_offset = new_offset.min(max_offset);
// Update current line
self.scroll.current_line = target_line.saturating_add(1); // 1-indexed
// Update hovered entry to match the clicked entry
self.toc_hovered_entry = Some(entry_idx);
return true;
}
}
false
}
/// Handle a click on the TOC in a specific area (for when area is pre-calculated).
///
/// # Arguments
///
/// * `event` - The mouse event
/// * `toc_area` - The pre-calculated TOC area
///
/// # Returns
///
/// `true` if the click was handled (was on a TOC entry), `false` otherwise.
pub fn handle_toc_click_in_area(&mut self, event: &MouseEvent, toc_area: Rect) -> bool {
// Only handle left clicks
if !matches!(event.kind, MouseEventKind::Down(MouseButton::Left)) {
return false;
}
// Check horizontal bounds and if above TOC
// Don't check lower vertical bound - let entry_at_position handle that
if event.column < toc_area.x
|| event.column >= toc_area.x + toc_area.width
|| event.row < toc_area.y
{
return false;
}
// 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
};
// Create a TOC to find the clicked entry
let toc = Toc::new(toc_state)
.expanded(self.toc_hovered)
.config(self.toc_config.clone());
// Find which entry was clicked
if let Some(entry_idx) = toc.entry_at_position(event.column, event.row, toc_area) {
// Get the target line number
if let Some(target_line) = toc.click_to_line(entry_idx) {
// Scroll to the heading
let new_offset = target_line.saturating_sub(2);
let max_offset = self
.scroll
.total_lines
.saturating_sub(self.scroll.viewport_height);
self.scroll.scroll_offset = new_offset.min(max_offset);
self.scroll.current_line = target_line.saturating_add(1);
// Update hovered entry to match the clicked entry
self.toc_hovered_entry = Some(entry_idx);
return true;
}
}
false
}
}