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}