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}