iced_code_editor/canvas_editor/canvas_impl.rs
1//! Canvas rendering implementation using Iced's `canvas::Program`.
2
3use iced::mouse;
4use iced::widget::canvas::{self, Geometry};
5use iced::{Color, Event, Point, Rectangle, Size, Theme, keyboard};
6use syntect::easy::HighlightLines;
7use syntect::highlighting::{Style, ThemeSet};
8use syntect::parsing::SyntaxSet;
9
10fn is_cursor_in_bounds(cursor: &mouse::Cursor, bounds: Rectangle) -> bool {
11 match cursor {
12 mouse::Cursor::Available(point) => bounds.contains(*point),
13 mouse::Cursor::Levitating(point) => bounds.contains(*point),
14 mouse::Cursor::Unavailable => false,
15 }
16}
17
18use super::wrapping::WrappingCalculator;
19use super::{
20 ArrowDirection, CHAR_WIDTH, CodeEditor, FONT_SIZE, LINE_HEIGHT, Message,
21};
22use iced::widget::canvas::Action;
23
24impl canvas::Program<Message> for CodeEditor {
25 type State = ();
26
27 fn draw(
28 &self,
29 _state: &Self::State,
30 renderer: &iced::Renderer,
31 _theme: &Theme,
32 bounds: Rectangle,
33 _cursor: mouse::Cursor,
34 ) -> Vec<Geometry> {
35 let geometry = self.cache.draw(renderer, bounds.size(), |frame| {
36 // Initialize wrapping calculator
37 let wrapping_calc =
38 WrappingCalculator::new(self.wrap_enabled, self.wrap_column);
39 let visual_lines = wrapping_calc.calculate_visual_lines(
40 &self.buffer,
41 bounds.width,
42 self.gutter_width(),
43 );
44
45 // Calculate visible line range based on viewport for optimized rendering
46 // Use bounds.height as fallback when viewport_height is not yet initialized
47 let effective_viewport_height = if self.viewport_height > 0.0 {
48 self.viewport_height
49 } else {
50 bounds.height
51 };
52 let first_visible_line =
53 (self.viewport_scroll / LINE_HEIGHT).floor() as usize;
54 let visible_lines_count =
55 (effective_viewport_height / LINE_HEIGHT).ceil() as usize + 2;
56 let last_visible_line = (first_visible_line + visible_lines_count)
57 .min(visual_lines.len());
58
59 // Load syntax highlighting
60 let syntax_set = SyntaxSet::load_defaults_newlines();
61 let theme_set = ThemeSet::load_defaults();
62 let syntax_theme = &theme_set.themes["base16-ocean.dark"];
63
64 let syntax_ref = match self.syntax.as_str() {
65 "py" | "python" => syntax_set.find_syntax_by_extension("py"),
66 "lua" => syntax_set.find_syntax_by_extension("lua"),
67 "rs" | "rust" => syntax_set.find_syntax_by_extension("rs"),
68 "js" | "javascript" => {
69 syntax_set.find_syntax_by_extension("js")
70 }
71 "html" | "htm" => syntax_set.find_syntax_by_extension("html"),
72 "xml" | "svg" => syntax_set.find_syntax_by_extension("xml"),
73 "css" => syntax_set.find_syntax_by_extension("css"),
74 "json" => syntax_set.find_syntax_by_extension("json"),
75 "md" | "markdown" => syntax_set.find_syntax_by_extension("md"),
76 _ => Some(syntax_set.find_syntax_plain_text()),
77 };
78
79 // Draw only visible lines (virtual scrolling optimization)
80 for (idx, visual_line) in visual_lines
81 .iter()
82 .enumerate()
83 .skip(first_visible_line)
84 .take(last_visible_line - first_visible_line)
85 {
86 let y = idx as f32 * LINE_HEIGHT;
87
88 // Note: Gutter background is handled by a container in view.rs
89 // to ensure proper clipping when the pane is resized.
90
91 // Draw line number only for first segment
92 if self.line_numbers_enabled {
93 if visual_line.is_first_segment() {
94 let line_num = visual_line.logical_line + 1;
95 let line_num_text = format!("{}", line_num);
96 // Calculate actual text width and center in gutter
97 let digit_count = line_num_text.len() as f32;
98 let text_width = digit_count * CHAR_WIDTH;
99 let x_pos = (self.gutter_width() - text_width) / 2.0;
100 frame.fill_text(canvas::Text {
101 content: line_num_text,
102 position: Point::new(x_pos, y + 2.0),
103 color: self.style.line_number_color,
104 size: FONT_SIZE.into(),
105 font: iced::Font::MONOSPACE,
106 ..canvas::Text::default()
107 });
108 } else {
109 // Draw wrap indicator for continuation lines
110 frame.fill_text(canvas::Text {
111 content: "↪".to_string(),
112 position: Point::new(
113 self.gutter_width() - 20.0,
114 y + 2.0,
115 ),
116 color: self.style.line_number_color,
117 size: FONT_SIZE.into(),
118 font: iced::Font::MONOSPACE,
119 ..canvas::Text::default()
120 });
121 }
122 }
123
124 // Highlight current line (based on logical line)
125 if visual_line.logical_line == self.cursor.0 {
126 frame.fill_rectangle(
127 Point::new(self.gutter_width(), y),
128 Size::new(
129 bounds.width - self.gutter_width(),
130 LINE_HEIGHT,
131 ),
132 self.style.current_line_highlight,
133 );
134 }
135
136 // Draw text content with syntax highlighting
137 let full_line_content =
138 self.buffer.line(visual_line.logical_line);
139
140 // Convert character indices to byte indices for UTF-8 string slicing
141 let start_byte = full_line_content
142 .char_indices()
143 .nth(visual_line.start_col)
144 .map_or(full_line_content.len(), |(idx, _)| idx);
145 let end_byte = full_line_content
146 .char_indices()
147 .nth(visual_line.end_col)
148 .map_or(full_line_content.len(), |(idx, _)| idx);
149 let line_segment = &full_line_content[start_byte..end_byte];
150
151 if let Some(syntax) = syntax_ref {
152 let mut highlighter =
153 HighlightLines::new(syntax, syntax_theme);
154
155 // Highlight the full line to get correct token colors
156 let full_line_ranges = highlighter
157 .highlight_line(full_line_content, &syntax_set)
158 .unwrap_or_else(|_| {
159 vec![(Style::default(), full_line_content)]
160 });
161
162 // Extract only the ranges that fall within our segment
163 let mut x_offset = self.gutter_width() + 5.0;
164 let mut char_pos = 0;
165
166 for (style, text) in full_line_ranges {
167 let text_len = text.chars().count();
168 let text_end = char_pos + text_len;
169
170 // Check if this token intersects with our segment
171 if text_end > visual_line.start_col
172 && char_pos < visual_line.end_col
173 {
174 // Calculate the intersection
175 let segment_start =
176 char_pos.max(visual_line.start_col);
177 let segment_end = text_end.min(visual_line.end_col);
178
179 let text_start_offset =
180 segment_start.saturating_sub(char_pos);
181 let text_end_offset = text_start_offset
182 + (segment_end - segment_start);
183
184 // Convert character offsets to byte offsets for UTF-8 slicing
185 let start_byte = text
186 .char_indices()
187 .nth(text_start_offset)
188 .map_or(text.len(), |(idx, _)| idx);
189 let end_byte = text
190 .char_indices()
191 .nth(text_end_offset)
192 .map_or(text.len(), |(idx, _)| idx);
193
194 let segment_text = &text[start_byte..end_byte];
195
196 let color = Color::from_rgb(
197 f32::from(style.foreground.r) / 255.0,
198 f32::from(style.foreground.g) / 255.0,
199 f32::from(style.foreground.b) / 255.0,
200 );
201
202 frame.fill_text(canvas::Text {
203 content: segment_text.to_string(),
204 position: Point::new(x_offset, y + 2.0),
205 color,
206 size: FONT_SIZE.into(),
207 font: iced::Font::MONOSPACE,
208 ..canvas::Text::default()
209 });
210
211 x_offset += segment_text.chars().count() as f32
212 * CHAR_WIDTH;
213 }
214
215 char_pos = text_end;
216 }
217 } else {
218 // Fallback to plain text
219 frame.fill_text(canvas::Text {
220 content: line_segment.to_string(),
221 position: Point::new(
222 self.gutter_width() + 5.0,
223 y + 2.0,
224 ),
225 color: self.style.text_color,
226 size: FONT_SIZE.into(),
227 font: iced::Font::MONOSPACE,
228 ..canvas::Text::default()
229 });
230 }
231 }
232
233 // Draw search match highlights
234 if self.search_state.is_open && !self.search_state.query.is_empty()
235 {
236 let query_len = self.search_state.query.chars().count();
237
238 for (match_idx, search_match) in
239 self.search_state.matches.iter().enumerate()
240 {
241 // Determine if this is the current match
242 let is_current = self.search_state.current_match_index
243 == Some(match_idx);
244
245 let highlight_color = if is_current {
246 // Orange for current match
247 Color { r: 1.0, g: 0.6, b: 0.0, a: 0.4 }
248 } else {
249 // Yellow for other matches
250 Color { r: 1.0, g: 1.0, b: 0.0, a: 0.3 }
251 };
252
253 // Convert logical position to visual line
254 let start_visual = WrappingCalculator::logical_to_visual(
255 &visual_lines,
256 search_match.line,
257 search_match.col,
258 );
259 let end_visual = WrappingCalculator::logical_to_visual(
260 &visual_lines,
261 search_match.line,
262 search_match.col + query_len,
263 );
264
265 if let (Some(start_v), Some(end_v)) =
266 (start_visual, end_visual)
267 {
268 if start_v == end_v {
269 // Match within same visual line
270 let y = start_v as f32 * LINE_HEIGHT;
271 let x_start = self.gutter_width()
272 + 5.0
273 + search_match.col as f32 * CHAR_WIDTH;
274 let x_end = self.gutter_width()
275 + 5.0
276 + (search_match.col + query_len) as f32
277 * CHAR_WIDTH;
278
279 frame.fill_rectangle(
280 Point::new(x_start, y + 2.0),
281 Size::new(x_end - x_start, LINE_HEIGHT - 4.0),
282 highlight_color,
283 );
284 } else {
285 // Match spans multiple visual lines
286 for (v_idx, vl) in visual_lines
287 .iter()
288 .enumerate()
289 .skip(start_v)
290 .take(end_v - start_v + 1)
291 {
292 let y = v_idx as f32 * LINE_HEIGHT;
293
294 let match_start_col = search_match.col;
295 let match_end_col =
296 search_match.col + query_len;
297
298 let sel_start_col = if v_idx == start_v {
299 match_start_col
300 } else {
301 vl.start_col
302 };
303 let sel_end_col = if v_idx == end_v {
304 match_end_col
305 } else {
306 vl.end_col
307 };
308
309 let x_start = self.gutter_width()
310 + 5.0
311 + (sel_start_col - vl.start_col) as f32
312 * CHAR_WIDTH;
313 let x_end = self.gutter_width()
314 + 5.0
315 + (sel_end_col - vl.start_col) as f32
316 * CHAR_WIDTH;
317
318 frame.fill_rectangle(
319 Point::new(x_start, y + 2.0),
320 Size::new(
321 x_end - x_start,
322 LINE_HEIGHT - 4.0,
323 ),
324 highlight_color,
325 );
326 }
327 }
328 }
329 }
330 }
331
332 // Draw selection highlight
333 if let Some((start, end)) = self.get_selection_range()
334 && start != end
335 {
336 let selection_color = Color { r: 0.3, g: 0.5, b: 0.8, a: 0.3 };
337
338 if start.0 == end.0 {
339 // Single line selection - need to handle wrapped segments
340 let start_visual = WrappingCalculator::logical_to_visual(
341 &visual_lines,
342 start.0,
343 start.1,
344 );
345 let end_visual = WrappingCalculator::logical_to_visual(
346 &visual_lines,
347 end.0,
348 end.1,
349 );
350
351 if let (Some(start_v), Some(end_v)) =
352 (start_visual, end_visual)
353 {
354 if start_v == end_v {
355 // Selection within same visual line
356 let y = start_v as f32 * LINE_HEIGHT;
357 let x_start = self.gutter_width()
358 + 5.0
359 + start.1 as f32 * CHAR_WIDTH;
360 let x_end = self.gutter_width()
361 + 5.0
362 + end.1 as f32 * CHAR_WIDTH;
363
364 frame.fill_rectangle(
365 Point::new(x_start, y + 2.0),
366 Size::new(x_end - x_start, LINE_HEIGHT - 4.0),
367 selection_color,
368 );
369 } else {
370 // Selection spans multiple visual lines (same logical line)
371 for (v_idx, vl) in visual_lines
372 .iter()
373 .enumerate()
374 .skip(start_v)
375 .take(end_v - start_v + 1)
376 {
377 let y = v_idx as f32 * LINE_HEIGHT;
378
379 let sel_start_col = if v_idx == start_v {
380 start.1
381 } else {
382 vl.start_col
383 };
384 let sel_end_col = if v_idx == end_v {
385 end.1
386 } else {
387 vl.end_col
388 };
389
390 let x_start = self.gutter_width()
391 + 5.0
392 + (sel_start_col - vl.start_col) as f32
393 * CHAR_WIDTH;
394 let x_end = self.gutter_width()
395 + 5.0
396 + (sel_end_col - vl.start_col) as f32
397 * CHAR_WIDTH;
398
399 frame.fill_rectangle(
400 Point::new(x_start, y + 2.0),
401 Size::new(
402 x_end - x_start,
403 LINE_HEIGHT - 4.0,
404 ),
405 selection_color,
406 );
407 }
408 }
409 }
410 } else {
411 // Multi-line selection
412 let start_visual = WrappingCalculator::logical_to_visual(
413 &visual_lines,
414 start.0,
415 start.1,
416 );
417 let end_visual = WrappingCalculator::logical_to_visual(
418 &visual_lines,
419 end.0,
420 end.1,
421 );
422
423 if let (Some(start_v), Some(end_v)) =
424 (start_visual, end_visual)
425 {
426 for (v_idx, vl) in visual_lines
427 .iter()
428 .enumerate()
429 .skip(start_v)
430 .take(end_v - start_v + 1)
431 {
432 let y = v_idx as f32 * LINE_HEIGHT;
433
434 let sel_start_col = if vl.logical_line == start.0
435 && v_idx == start_v
436 {
437 start.1
438 } else {
439 vl.start_col
440 };
441
442 let sel_end_col =
443 if vl.logical_line == end.0 && v_idx == end_v {
444 end.1
445 } else {
446 vl.end_col
447 };
448
449 let x_start = self.gutter_width()
450 + 5.0
451 + (sel_start_col - vl.start_col) as f32
452 * CHAR_WIDTH;
453 let x_end = self.gutter_width()
454 + 5.0
455 + (sel_end_col - vl.start_col) as f32
456 * CHAR_WIDTH;
457
458 frame.fill_rectangle(
459 Point::new(x_start, y + 2.0),
460 Size::new(x_end - x_start, LINE_HEIGHT - 4.0),
461 selection_color,
462 );
463 }
464 }
465 }
466 }
467
468 // Draw cursor (only when editor has focus)
469 if self.show_cursor && self.cursor_visible && self.is_focused() {
470 // Find the visual line containing the cursor
471 if let Some(cursor_visual) =
472 WrappingCalculator::logical_to_visual(
473 &visual_lines,
474 self.cursor.0,
475 self.cursor.1,
476 )
477 {
478 let vl = &visual_lines[cursor_visual];
479 let cursor_x = self.gutter_width()
480 + 5.0
481 + (self.cursor.1 - vl.start_col) as f32 * CHAR_WIDTH;
482 let cursor_y = cursor_visual as f32 * LINE_HEIGHT;
483
484 frame.fill_rectangle(
485 Point::new(cursor_x, cursor_y + 2.0),
486 Size::new(2.0, LINE_HEIGHT - 4.0),
487 self.style.text_color,
488 );
489 }
490 }
491 });
492
493 vec![geometry]
494 }
495
496 fn update(
497 &self,
498 _state: &mut Self::State,
499 event: &Event,
500 bounds: Rectangle,
501 cursor: mouse::Cursor,
502 ) -> Option<Action<Message>> {
503 match event {
504 Event::Keyboard(keyboard::Event::KeyPressed {
505 key,
506 modifiers,
507 text,
508 ..
509 }) => {
510 // Only process keyboard events if this editor has focus
511 let focused_id = super::FOCUSED_EDITOR_ID
512 .load(std::sync::atomic::Ordering::Relaxed);
513 if focused_id != self.editor_id {
514 return None;
515 }
516
517 // Cursor outside canvas bounds
518 if !is_cursor_in_bounds(&cursor, bounds) {
519 return None;
520 }
521
522 // Only process keyboard events if canvas has focus
523 if !self.has_canvas_focus {
524 return None;
525 }
526
527 // Handle Ctrl+C / Ctrl+Insert (copy)
528 if (modifiers.control()
529 && matches!(key, keyboard::Key::Character(c) if c.as_str() == "c"))
530 || (modifiers.control()
531 && matches!(
532 key,
533 keyboard::Key::Named(keyboard::key::Named::Insert)
534 ))
535 {
536 return Some(Action::publish(Message::Copy).and_capture());
537 }
538
539 // Handle Ctrl+Z (undo)
540 if modifiers.control()
541 && matches!(key, keyboard::Key::Character(z) if z.as_str() == "z")
542 {
543 return Some(Action::publish(Message::Undo).and_capture());
544 }
545
546 // Handle Ctrl+Y (redo)
547 if modifiers.control()
548 && matches!(key, keyboard::Key::Character(y) if y.as_str() == "y")
549 {
550 return Some(Action::publish(Message::Redo).and_capture());
551 }
552
553 // Handle Ctrl+F (open search)
554 if modifiers.control()
555 && matches!(key, keyboard::Key::Character(f) if f.as_str() == "f")
556 && self.search_replace_enabled
557 {
558 return Some(
559 Action::publish(Message::OpenSearch).and_capture(),
560 );
561 }
562
563 // Handle Ctrl+H (open search and replace)
564 if modifiers.control()
565 && matches!(key, keyboard::Key::Character(h) if h.as_str() == "h")
566 && self.search_replace_enabled
567 {
568 return Some(
569 Action::publish(Message::OpenSearchReplace)
570 .and_capture(),
571 );
572 }
573
574 // Handle Escape (close search dialog if open)
575 if matches!(
576 key,
577 keyboard::Key::Named(keyboard::key::Named::Escape)
578 ) {
579 return Some(
580 Action::publish(Message::CloseSearch).and_capture(),
581 );
582 }
583
584 // Handle Tab (cycle forward in search dialog if open)
585 if matches!(
586 key,
587 keyboard::Key::Named(keyboard::key::Named::Tab)
588 ) && self.search_state.is_open
589 {
590 if modifiers.shift() {
591 // Shift+Tab: cycle backward
592 return Some(
593 Action::publish(Message::SearchDialogShiftTab)
594 .and_capture(),
595 );
596 } else {
597 // Tab: cycle forward
598 return Some(
599 Action::publish(Message::SearchDialogTab)
600 .and_capture(),
601 );
602 }
603 }
604
605 // Handle F3 (find next) and Shift+F3 (find previous)
606 if matches!(key, keyboard::Key::Named(keyboard::key::Named::F3))
607 && self.search_replace_enabled
608 {
609 if modifiers.shift() {
610 return Some(
611 Action::publish(Message::FindPrevious)
612 .and_capture(),
613 );
614 } else {
615 return Some(
616 Action::publish(Message::FindNext).and_capture(),
617 );
618 }
619 }
620
621 // Handle Ctrl+V / Shift+Insert (paste) - read clipboard and send paste message
622 if (modifiers.control()
623 && matches!(key, keyboard::Key::Character(v) if v.as_str() == "v"))
624 || (modifiers.shift()
625 && matches!(
626 key,
627 keyboard::Key::Named(keyboard::key::Named::Insert)
628 ))
629 {
630 // Return an action that requests clipboard read
631 return Some(Action::publish(
632 Message::Paste(String::new()),
633 ));
634 }
635
636 // Handle Ctrl+Home (go to start of document)
637 if modifiers.control()
638 && matches!(
639 key,
640 keyboard::Key::Named(keyboard::key::Named::Home)
641 )
642 {
643 return Some(
644 Action::publish(Message::CtrlHome).and_capture(),
645 );
646 }
647
648 // Handle Ctrl+End (go to end of document)
649 if modifiers.control()
650 && matches!(
651 key,
652 keyboard::Key::Named(keyboard::key::Named::End)
653 )
654 {
655 return Some(
656 Action::publish(Message::CtrlEnd).and_capture(),
657 );
658 }
659
660 // Handle Shift+Delete (delete selection)
661 if modifiers.shift()
662 && matches!(
663 key,
664 keyboard::Key::Named(keyboard::key::Named::Delete)
665 )
666 {
667 return Some(
668 Action::publish(Message::DeleteSelection).and_capture(),
669 );
670 }
671
672 // PRIORITY 1: Check if 'text' field has valid printable character
673 // This handles:
674 // - Numpad keys with NumLock ON (key=Named(ArrowDown), text=Some("2"))
675 // - Regular typing with shift, accents, international layouts
676 if let Some(text_content) = text
677 && !text_content.is_empty()
678 && !modifiers.control()
679 && !modifiers.alt()
680 {
681 // Check if it's a printable character (not a control character)
682 // This filters out Enter (\n), Tab (\t), Delete (U+007F), etc.
683 if let Some(first_char) = text_content.chars().next()
684 && !first_char.is_control()
685 {
686 return Some(
687 Action::publish(Message::CharacterInput(
688 first_char,
689 ))
690 .and_capture(),
691 );
692 }
693 }
694
695 // PRIORITY 2: Handle special named keys (navigation, editing)
696 // These are only processed if text didn't contain a printable character
697 let message = match key {
698 keyboard::Key::Named(keyboard::key::Named::Backspace) => {
699 Some(Message::Backspace)
700 }
701 keyboard::Key::Named(keyboard::key::Named::Delete) => {
702 Some(Message::Delete)
703 }
704 keyboard::Key::Named(keyboard::key::Named::Enter) => {
705 Some(Message::Enter)
706 }
707 keyboard::Key::Named(keyboard::key::Named::Tab) => {
708 // Insert 4 spaces for Tab
709 Some(Message::Tab)
710 }
711 keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
712 Some(Message::ArrowKey(
713 ArrowDirection::Up,
714 modifiers.shift(),
715 ))
716 }
717 keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
718 Some(Message::ArrowKey(
719 ArrowDirection::Down,
720 modifiers.shift(),
721 ))
722 }
723 keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
724 Some(Message::ArrowKey(
725 ArrowDirection::Left,
726 modifiers.shift(),
727 ))
728 }
729 keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
730 Some(Message::ArrowKey(
731 ArrowDirection::Right,
732 modifiers.shift(),
733 ))
734 }
735 keyboard::Key::Named(keyboard::key::Named::PageUp) => {
736 Some(Message::PageUp)
737 }
738 keyboard::Key::Named(keyboard::key::Named::PageDown) => {
739 Some(Message::PageDown)
740 }
741 keyboard::Key::Named(keyboard::key::Named::Home) => {
742 Some(Message::Home(modifiers.shift()))
743 }
744 keyboard::Key::Named(keyboard::key::Named::End) => {
745 Some(Message::End(modifiers.shift()))
746 }
747 // PRIORITY 3: Fallback to extracting from 'key' if text was empty/control char
748 // This handles edge cases where text field is not populated
749 _ => {
750 if !modifiers.control()
751 && !modifiers.alt()
752 && let keyboard::Key::Character(c) = key
753 && !c.is_empty()
754 {
755 return c
756 .chars()
757 .next()
758 .map(Message::CharacterInput)
759 .map(|msg| Action::publish(msg).and_capture());
760 }
761 None
762 }
763 };
764
765 message.map(|msg| Action::publish(msg).and_capture())
766 }
767 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
768 cursor.position_in(bounds).map(|position| {
769 // Don't capture the event so it can bubble up for focus management
770 Action::publish(Message::MouseClick(position))
771 })
772 }
773 Event::Mouse(mouse::Event::CursorMoved { .. }) => {
774 // Handle mouse drag for selection only when cursor is within bounds
775 cursor.position_in(bounds).map(|position| {
776 Action::publish(Message::MouseDrag(position)).and_capture()
777 })
778 }
779 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
780 // Only handle mouse release when cursor is within bounds
781 // This prevents capturing events meant for other widgets
782 if cursor.is_over(bounds) {
783 Some(Action::publish(Message::MouseRelease).and_capture())
784 } else {
785 None
786 }
787 }
788 _ => None,
789 }
790 }
791}