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