ratatui_toolkit/widgets/markdown_widget/widget/methods/handle_toc_click.rs
1//! Handle TOC click events for scroll-to-heading navigation.
2
3use crossterm::event::{MouseButton, 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 a click on the TOC to scroll to the selected heading.
12 ///
13 /// # Arguments
14 ///
15 /// * `event` - The mouse event
16 /// * `area` - The total widget area
17 ///
18 /// # Returns
19 ///
20 /// `true` if the click was handled (was on a TOC entry), `false` otherwise.
21 ///
22 /// # Example
23 ///
24 /// ```rust,no_run
25 /// // In your event loop:
26 /// if let Event::Mouse(mouse_event) = event {
27 /// if matches!(mouse_event.kind, MouseEventKind::Down(MouseButton::Left)) {
28 /// if widget.handle_toc_click(&mouse_event, area) {
29 /// // Click was handled - you may want to redraw
30 /// }
31 /// }
32 /// }
33 /// ```
34 pub fn handle_toc_click(&mut self, event: &MouseEvent, area: Rect) -> bool {
35 // Only handle left clicks
36 if !matches!(event.kind, MouseEventKind::Down(MouseButton::Left)) {
37 return false;
38 }
39
40 // Get the TOC area
41 let toc_area = match self.calculate_toc_area(area) {
42 Some(t_area) => t_area,
43 None => return false,
44 };
45
46 // Check horizontal bounds and if above TOC
47 // Don't check lower vertical bound - let entry_at_position handle that
48 // based on actual entry count
49 if event.column < toc_area.x
50 || event.column >= toc_area.x + toc_area.width
51 || event.row < toc_area.y
52 {
53 return false;
54 }
55
56 // Create state from content with entries
57 let auto_state = TocState::from_content(self.content);
58 let toc_state = if let Some(provided) = self.toc_state {
59 if provided.entries.is_empty() {
60 &auto_state
61 } else {
62 provided
63 }
64 } else {
65 &auto_state
66 };
67
68 // Create a TOC to find the clicked entry
69 let toc = Toc::new(toc_state)
70 .expanded(self.toc_hovered) // Use current expansion state
71 .config(self.toc_config.clone());
72
73 // Find which entry was clicked
74 if let Some(entry_idx) = toc.entry_at_position(event.column, event.row, toc_area) {
75 // Get the target line number
76 if let Some(target_line) = toc.click_to_line(entry_idx) {
77 // Scroll to the heading (a bit above for context)
78 let new_offset = target_line.saturating_sub(2);
79
80 // Clamp to valid range
81 let max_offset = self
82 .scroll
83 .total_lines
84 .saturating_sub(self.scroll.viewport_height);
85 self.scroll.scroll_offset = new_offset.min(max_offset);
86
87 // Update current line
88 self.scroll.current_line = target_line.saturating_add(1); // 1-indexed
89
90 // Update hovered entry to match the clicked entry
91 self.toc_hovered_entry = Some(entry_idx);
92
93 return true;
94 }
95 }
96
97 false
98 }
99
100 /// Handle a click on the TOC in a specific area (for when area is pre-calculated).
101 ///
102 /// # Arguments
103 ///
104 /// * `event` - The mouse event
105 /// * `toc_area` - The pre-calculated TOC area
106 ///
107 /// # Returns
108 ///
109 /// `true` if the click was handled (was on a TOC entry), `false` otherwise.
110 pub fn handle_toc_click_in_area(&mut self, event: &MouseEvent, toc_area: Rect) -> bool {
111 // Only handle left clicks
112 if !matches!(event.kind, MouseEventKind::Down(MouseButton::Left)) {
113 return false;
114 }
115
116 // Check horizontal bounds and if above TOC
117 // Don't check lower vertical bound - let entry_at_position handle that
118 if event.column < toc_area.x
119 || event.column >= toc_area.x + toc_area.width
120 || event.row < toc_area.y
121 {
122 return false;
123 }
124
125 // Create state from content with entries
126 let auto_state = TocState::from_content(self.content);
127 let toc_state = if let Some(provided) = self.toc_state {
128 if provided.entries.is_empty() {
129 &auto_state
130 } else {
131 provided
132 }
133 } else {
134 &auto_state
135 };
136
137 // Create a TOC to find the clicked entry
138 let toc = Toc::new(toc_state)
139 .expanded(self.toc_hovered)
140 .config(self.toc_config.clone());
141
142 // Find which entry was clicked
143 if let Some(entry_idx) = toc.entry_at_position(event.column, event.row, toc_area) {
144 // Get the target line number
145 if let Some(target_line) = toc.click_to_line(entry_idx) {
146 // Scroll to the heading
147 let new_offset = target_line.saturating_sub(2);
148 let max_offset = self
149 .scroll
150 .total_lines
151 .saturating_sub(self.scroll.viewport_height);
152 self.scroll.scroll_offset = new_offset.min(max_offset);
153 self.scroll.current_line = target_line.saturating_add(1);
154
155 // Update hovered entry to match the clicked entry
156 self.toc_hovered_entry = Some(entry_idx);
157
158 return true;
159 }
160 }
161
162 false
163 }
164}