ratatui_toolkit/widgets/markdown_widget/widget/methods/
handle_mouse_event.rs1use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
4use ratatui::layout::Rect;
5
6use crate::widgets::markdown_widget::extensions::scrollbar::{
7 click_to_offset, is_in_scrollbar_area,
8};
9use crate::widgets::markdown_widget::extensions::selection::should_render_line;
10use crate::widgets::markdown_widget::foundation::elements::render;
11use crate::widgets::markdown_widget::foundation::elements::ElementKind;
12use crate::widgets::markdown_widget::foundation::events::MarkdownEvent;
13use crate::widgets::markdown_widget::foundation::helpers::is_in_area;
14use crate::widgets::markdown_widget::foundation::parser::render_markdown_to_elements;
15use crate::widgets::markdown_widget::foundation::types::SelectionPos;
16use crate::widgets::markdown_widget::widget::enums::MarkdownWidgetMode;
17use crate::widgets::markdown_widget::widget::MarkdownWidget;
18
19impl<'a> MarkdownWidget<'a> {
20 pub fn handle_mouse_event(&mut self, event: &MouseEvent, area: Rect) -> MarkdownEvent {
41 if !is_in_area(event.column, event.row, area) {
42 if self.selection.is_active() {
44 self.selection.exit();
45 return MarkdownEvent::SelectionEnded;
46 }
47 return MarkdownEvent::None;
48 }
49
50 let relative_y = event.row.saturating_sub(area.y) as usize;
51 let relative_x = event.column.saturating_sub(area.x) as usize;
52 let width = area.width as usize;
53
54 let document_y = (relative_y + self.scroll.scroll_offset) as i32;
56 let document_x = relative_x as i32;
57
58 if self.show_toc {
60 if let Some(toc_area) = self.calculate_toc_area(area) {
61 let is_over_toc = event.column >= toc_area.x
62 && event.column < toc_area.x + toc_area.width
63 && event.row >= toc_area.y
64 && event.row < toc_area.y + toc_area.height;
65
66 if is_over_toc {
67 match event.kind {
69 MouseEventKind::ScrollUp => {
70 self.toc_scroll_offset = self.toc_scroll_offset.saturating_sub(1);
71 self.update_toc_hovered_entry(event.column, event.row, toc_area);
73 return MarkdownEvent::None;
74 }
75 MouseEventKind::ScrollDown => {
76 let entry_count = self.toc_state.map(|s| s.entry_count()).unwrap_or(0);
78 let visible_height = toc_area.height as usize;
79 let max_offset = entry_count.saturating_sub(visible_height);
80 if self.toc_scroll_offset < max_offset {
81 self.toc_scroll_offset += 1;
82 }
83 self.update_toc_hovered_entry(event.column, event.row, toc_area);
85 return MarkdownEvent::None;
86 }
87 MouseEventKind::Down(MouseButton::Left) => {
88 return MarkdownEvent::None;
91 }
92 _ => {}
93 }
94 }
95 }
96 }
97
98 if let Some(scrollbar_area) = self.calculate_scrollbar_area(area) {
100 if is_in_scrollbar_area(event.column, event.row, scrollbar_area) {
101 match event.kind {
102 MouseEventKind::Down(MouseButton::Left)
103 | MouseEventKind::Drag(MouseButton::Left) => {
104 let new_offset = click_to_offset(event.row, scrollbar_area, self.scroll);
106 self.scroll.scroll_offset = new_offset;
107 return MarkdownEvent::Scrolled {
108 offset: new_offset,
109 direction: 0,
110 };
111 }
112 MouseEventKind::ScrollUp => {
113 let old_offset = self.scroll.scroll_offset;
114 self.scroll.scroll_up(5);
115 return MarkdownEvent::Scrolled {
116 offset: self.scroll.scroll_offset,
117 direction: -(old_offset.saturating_sub(self.scroll.scroll_offset)
118 as i32),
119 };
120 }
121 MouseEventKind::ScrollDown => {
122 let old_offset = self.scroll.scroll_offset;
123 self.scroll.scroll_down(5);
124 return MarkdownEvent::Scrolled {
125 offset: self.scroll.scroll_offset,
126 direction: (self.scroll.scroll_offset.saturating_sub(old_offset)
127 as i32),
128 };
129 }
130 _ => {}
131 }
132 }
133 }
134
135 match event.kind {
136 MouseEventKind::Down(MouseButton::Left) => {
137 if self.selection.is_active() {
139 self.selection.exit();
140 }
141
142 let (is_double, _should_process_pending) = self.double_click.process_click(
145 event.column,
146 event.row,
147 self.scroll.scroll_offset,
148 );
149
150 if is_double {
151 if let Some(evt) = self.get_line_info_at_position(relative_y, width) {
153 self.last_double_click = Some((evt.0, evt.1, evt.2));
154 }
155 return MarkdownEvent::None;
156 }
157
158 let clicked_line = self.scroll.scroll_offset + relative_y + 1; if clicked_line <= self.scroll.total_lines {
161 self.scroll.set_current_line(clicked_line);
162 }
163
164 MarkdownEvent::FocusedLine { line: clicked_line }
165 }
166 MouseEventKind::Drag(MouseButton::Left) => {
167 let event_result = if !self.selection.is_active() {
168 self.selection.enter(
170 document_x,
171 document_y,
172 self.rendered_lines.clone(),
173 width,
174 );
175 self.selection.anchor = Some(SelectionPos::new(document_x, document_y));
176 self.mode = MarkdownWidgetMode::Drag;
177 MarkdownEvent::SelectionStarted
178 } else {
179 MarkdownEvent::None
180 };
181
182 self.selection.update_cursor(document_x, document_y);
184
185 event_result
186 }
187 MouseEventKind::Up(MouseButton::Left) => {
188 if self.selection.is_active() && self.selection.has_selection() {
190 self.selection.frozen_lines = Some(self.rendered_lines.clone());
192 self.selection.frozen_width = width;
193
194 if let Some(text) = self.selection.get_selected_text() {
196 if !text.is_empty() {
197 if let Ok(mut clipboard) = arboard::Clipboard::new() {
198 if clipboard.set_text(&text).is_ok() {
199 self.selection.last_copied_text = Some(text.clone());
201 return MarkdownEvent::Copied { text };
202 }
203 }
204 }
205 }
206 }
207 MarkdownEvent::None
208 }
209 MouseEventKind::ScrollUp => {
210 let old_offset = self.scroll.scroll_offset;
211 self.scroll.scroll_up(5);
212 MarkdownEvent::Scrolled {
213 offset: self.scroll.scroll_offset,
214 direction: -(old_offset.saturating_sub(self.scroll.scroll_offset) as i32),
215 }
216 }
217 MouseEventKind::ScrollDown => {
218 let old_offset = self.scroll.scroll_offset;
219 self.scroll.scroll_down(5);
220 MarkdownEvent::Scrolled {
221 offset: self.scroll.scroll_offset,
222 direction: (self.scroll.scroll_offset.saturating_sub(old_offset) as i32),
223 }
224 }
225 _ => MarkdownEvent::None,
226 }
227 }
228
229 pub fn check_pending_click(&mut self, area: Rect) -> MarkdownEvent {
240 if let Some((x, y, click_scroll_offset)) = self.double_click.check_pending_timeout() {
241 let relative_y = y.saturating_sub(area.y) as usize;
243 let relative_x = x.saturating_sub(area.x) as usize;
244 let width = area.width as usize;
245
246 let clicked_line = click_scroll_offset + relative_y + 1;
249 if clicked_line <= self.scroll.total_lines {
250 self.scroll.set_current_line(clicked_line);
251 }
252
253 if self.handle_click_collapse(relative_x, relative_y, width) {
255 if let Some((_, line_kind, text)) =
257 self.get_line_info_at_position(relative_y, width)
258 {
259 if line_kind == "Heading" {
260 return MarkdownEvent::HeadingToggled {
261 level: 1, text,
263 collapsed: true, };
265 }
266 }
267 }
268
269 return MarkdownEvent::FocusedLine { line: clicked_line };
270 }
271
272 MarkdownEvent::None
273 }
274
275 fn handle_click_collapse(&mut self, _x: usize, y: usize, width: usize) -> bool {
279 use crate::widgets::markdown_widget::foundation::elements::ElementKind;
280
281 let elements = render_markdown_to_elements(self.content, true);
282
283 let document_y = y + self.scroll.scroll_offset;
285 let mut line_idx = 0;
286
287 for (idx, element) in elements.iter().enumerate() {
288 if !should_render_line(element, idx, self.collapse) {
290 continue;
291 }
292
293 let rendered = render(element, width);
294 let line_count = rendered.len();
295
296 if document_y >= line_idx && document_y < line_idx + line_count {
297 match &element.kind {
298 ElementKind::Heading { section_id, .. } => {
299 if self.display.show_heading_collapse {
301 self.collapse.toggle_section(*section_id);
302 self.cache.invalidate();
303 return true;
304 }
305 }
306 ElementKind::Frontmatter { .. } => {
307 self.collapse.toggle_section(0);
308 self.cache.invalidate();
309 return true;
310 }
311 ElementKind::FrontmatterStart { .. } => {
312 self.collapse.toggle_section(0);
313 self.cache.invalidate();
314 return true;
315 }
316 ElementKind::ExpandToggle { content_id, .. } => {
317 self.expandable.toggle(content_id);
318 self.cache.invalidate();
319 return true;
320 }
321 _ => {}
322 }
323 }
324
325 line_idx += line_count;
326 }
327
328 false
329 }
330
331 pub fn get_line_info_at_position(
335 &self,
336 y: usize,
337 width: usize,
338 ) -> Option<(usize, String, String)> {
339 use crate::widgets::markdown_widget::foundation::elements::ElementKind;
340
341 let elements = render_markdown_to_elements(self.content, true);
342 let document_y = y + self.scroll.scroll_offset;
343 let mut visual_line_idx = 0;
344 let mut logical_line_num = 0;
345
346 for (idx, element) in elements.iter().enumerate() {
347 if !should_render_line(element, idx, self.collapse) {
348 continue;
349 }
350
351 logical_line_num += 1;
352
353 let rendered = render(element, width);
354 let line_count = rendered.len();
355
356 if document_y >= visual_line_idx && document_y < visual_line_idx + line_count {
357 let line_kind = match &element.kind {
358 ElementKind::Heading { .. } => "Heading",
359 ElementKind::Paragraph(_) => "Paragraph",
360 ElementKind::CodeBlockHeader { .. } => "CodeBlockHeader",
361 ElementKind::CodeBlockContent { .. } => "CodeBlockContent",
362 ElementKind::CodeBlockBorder { .. } => "CodeBlockBorder",
363 ElementKind::ListItem { .. } => "ListItem",
364 ElementKind::Blockquote { .. } => "Blockquote",
365 ElementKind::Empty => "Empty",
366 ElementKind::HorizontalRule => "HorizontalRule",
367 ElementKind::Frontmatter { .. } => "Frontmatter",
368 ElementKind::FrontmatterStart { .. } => "FrontmatterStart",
369 ElementKind::FrontmatterField { .. } => "FrontmatterField",
370 ElementKind::FrontmatterEnd => "FrontmatterEnd",
371 ElementKind::Expandable { .. } => "Expandable",
372 ElementKind::ExpandToggle { .. } => "ExpandToggle",
373 ElementKind::TableRow { .. } => "TableRow",
374 ElementKind::TableBorder(_) => "TableBorder",
375 ElementKind::HeadingBorder { .. } => "HeadingBorder",
376 };
377
378 let text_content = self.get_element_text(&element.kind);
379
380 return Some((logical_line_num, line_kind.to_string(), text_content));
381 }
382
383 visual_line_idx += line_count;
384 }
385
386 None
387 }
388
389 fn get_element_text(
391 &self,
392 kind: &crate::widgets::markdown_widget::foundation::elements::ElementKind,
393 ) -> String {
394 use crate::widgets::markdown_widget::foundation::elements::{ElementKind, TextSegment};
395
396 fn segment_to_text(seg: &TextSegment) -> &str {
397 match seg {
398 TextSegment::Plain(s) => s,
399 TextSegment::Bold(s) => s,
400 TextSegment::Italic(s) => s,
401 TextSegment::BoldItalic(s) => s,
402 TextSegment::InlineCode(s) => s,
403 TextSegment::Link { text, .. } => text,
404 TextSegment::Strikethrough(s) => s,
405 TextSegment::Html(s) => s,
406 TextSegment::Checkbox(_) => "",
407 }
408 }
409
410 match kind {
411 ElementKind::Heading { text, .. } => text.iter().map(segment_to_text).collect(),
412 ElementKind::Paragraph(segments) => segments.iter().map(segment_to_text).collect(),
413 ElementKind::CodeBlockContent { content, .. } => content.clone(),
414 ElementKind::CodeBlockHeader { language, .. } => language.clone(),
415 ElementKind::ListItem { content, .. } => content.iter().map(segment_to_text).collect(),
416 ElementKind::Blockquote { content, .. } => {
417 content.iter().map(segment_to_text).collect()
418 }
419 ElementKind::Frontmatter { fields, .. } => fields
420 .iter()
421 .map(|(k, v)| format!("{}: {}", k, v))
422 .collect::<Vec<_>>()
423 .join(", "),
424 ElementKind::FrontmatterField { key, value } => format!("{}: {}", key, value),
425 ElementKind::TableRow { cells, .. } => cells.join(" | "),
426 _ => String::new(),
427 }
428 }
429
430 pub fn set_rendered_lines(&mut self, lines: Vec<ratatui::text::Line<'static>>) {
434 self.rendered_lines = lines;
435 }
436
437 pub fn is_selection_active(&self) -> bool {
439 self.selection.is_active()
440 }
441
442 pub fn selection(
444 &self,
445 ) -> &crate::widgets::markdown_widget::state::selection_state::SelectionState {
446 self.selection
447 }
448
449 pub fn get_current_line_info(&self, width: usize) -> Option<(usize, String, String)> {
453 let document_y = self.scroll.current_line.saturating_sub(1);
458 let elements = render_markdown_to_elements(self.content, true);
459 let mut visual_line_idx = 0;
460 let mut logical_line_num = 0;
461
462 for (idx, element) in elements.iter().enumerate() {
463 if !should_render_line(element, idx, self.collapse) {
464 continue;
465 }
466
467 logical_line_num += 1;
468
469 let rendered = render(element, width);
470 let line_count = rendered.len();
471
472 if document_y >= visual_line_idx && document_y < visual_line_idx + line_count {
473 let line_kind = match &element.kind {
474 ElementKind::Heading { .. } => "Heading",
475 ElementKind::Paragraph(_) => "Paragraph",
476 ElementKind::CodeBlockHeader { .. } => "CodeBlockHeader",
477 ElementKind::CodeBlockContent { .. } => "CodeBlockContent",
478 ElementKind::CodeBlockBorder { .. } => "CodeBlockBorder",
479 ElementKind::ListItem { .. } => "ListItem",
480 ElementKind::Blockquote { .. } => "Blockquote",
481 ElementKind::Empty => "Empty",
482 ElementKind::HorizontalRule => "HorizontalRule",
483 ElementKind::Frontmatter { .. } => "Frontmatter",
484 ElementKind::FrontmatterStart { .. } => "FrontmatterStart",
485 ElementKind::FrontmatterField { .. } => "FrontmatterField",
486 ElementKind::FrontmatterEnd => "FrontmatterEnd",
487 ElementKind::Expandable { .. } => "Expandable",
488 ElementKind::ExpandToggle { .. } => "ExpandToggle",
489 ElementKind::TableRow { .. } => "TableRow",
490 ElementKind::TableBorder(_) => "TableBorder",
491 ElementKind::HeadingBorder { .. } => "HeadingBorder",
492 };
493
494 let text_content = self.get_element_text(&element.kind);
495
496 return Some((logical_line_num, line_kind.to_string(), text_content));
497 }
498
499 visual_line_idx += line_count;
500 }
501
502 None
503 }
504}