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