Skip to main content

iced_code_editor/canvas_editor/
canvas_impl.rs

1//! Canvas rendering implementation using Iced's `canvas::Program`.
2
3use iced::advanced::input_method;
4use iced::mouse;
5use iced::widget::canvas::{self, Geometry};
6use iced::{Color, Event, Point, Rectangle, Size, Theme, keyboard};
7use std::rc::Rc;
8use std::sync::OnceLock;
9use syntect::easy::HighlightLines;
10use syntect::highlighting::{Style, ThemeSet};
11use syntect::parsing::SyntaxSet;
12
13/// Computes geometry (x start and width) for a text segment used in rendering or highlighting.
14///
15/// # Arguments
16///
17/// * `line_content`: full text content of the current line.
18/// * `visual_start_col`: start column index of the current visual line.
19/// * `segment_start_col`: start column index of the target segment (e.g. highlight).
20/// * `segment_end_col`: end column index of the target segment.
21/// * `base_offset`: base X offset (usually gutter_width + padding).
22///
23/// # Returns
24///
25/// x_start, width
26///
27/// # Remark
28///
29/// This function handles CJK character widths correctly to keep highlights accurate.
30fn calculate_segment_geometry(
31    line_content: &str,
32    visual_start_col: usize,
33    segment_start_col: usize,
34    segment_end_col: usize,
35    base_offset: f32,
36    full_char_width: f32,
37    char_width: f32,
38) -> (f32, f32) {
39    // Clamp the segment to the current visual line so callers can safely pass
40    // logical selection/match columns without worrying about wrapping boundaries.
41    let segment_start_col = segment_start_col.max(visual_start_col);
42    let segment_end_col = segment_end_col.max(segment_start_col);
43
44    let mut prefix_width = 0.0;
45    let mut segment_width = 0.0;
46
47    // Compute widths directly from the source string to avoid allocating
48    // intermediate `String` slices for prefix/segment.
49    for (i, c) in line_content.chars().enumerate() {
50        if i >= segment_end_col {
51            break;
52        }
53
54        let w = super::measure_char_width(c, full_char_width, char_width);
55
56        if i >= visual_start_col && i < segment_start_col {
57            prefix_width += w;
58        } else if i >= segment_start_col {
59            segment_width += w;
60        }
61    }
62
63    (base_offset + prefix_width, segment_width)
64}
65
66use super::wrapping::{VisualLine, WrappingCalculator};
67use super::{ArrowDirection, CodeEditor, Message, measure_text_width};
68use iced::widget::canvas::Action;
69
70static SYNTAX_SET: OnceLock<SyntaxSet> = OnceLock::new();
71static THEME_SET: OnceLock<ThemeSet> = OnceLock::new();
72
73/// Context for canvas rendering operations.
74///
75/// This struct packages commonly used rendering parameters to reduce
76/// method signature complexity and improve code maintainability.
77struct RenderContext<'a> {
78    /// Visual lines calculated from wrapping
79    visual_lines: &'a [VisualLine],
80    /// Width of the canvas bounds
81    bounds_width: f32,
82    /// Width of the line number gutter
83    gutter_width: f32,
84    /// Height of each line in pixels
85    line_height: f32,
86    /// Font size in pixels
87    font_size: f32,
88    /// Full character width for wide characters (e.g., CJK)
89    full_char_width: f32,
90    /// Character width for narrow characters
91    char_width: f32,
92    /// Font to use for rendering text
93    font: iced::Font,
94}
95
96impl CodeEditor {
97    /// Draws line numbers and wrap indicators in the gutter area.
98    ///
99    /// # Arguments
100    ///
101    /// * `frame` - The canvas frame to draw on
102    /// * `ctx` - Rendering context containing visual lines and metrics
103    /// * `visual_line` - The visual line to render
104    /// * `y` - Y position for rendering
105    fn draw_line_numbers(
106        &self,
107        frame: &mut canvas::Frame,
108        ctx: &RenderContext,
109        visual_line: &VisualLine,
110        y: f32,
111    ) {
112        if !self.line_numbers_enabled {
113            return;
114        }
115
116        if visual_line.is_first_segment() {
117            // Draw line number for first segment
118            let line_num = visual_line.logical_line + 1;
119            let line_num_text = format!("{}", line_num);
120            // Calculate actual text width and center in gutter
121            let text_width = measure_text_width(
122                &line_num_text,
123                ctx.full_char_width,
124                ctx.char_width,
125            );
126            let x_pos = (ctx.gutter_width - text_width) / 2.0;
127            frame.fill_text(canvas::Text {
128                content: line_num_text,
129                position: Point::new(x_pos, y + 2.0),
130                color: self.style.line_number_color,
131                size: ctx.font_size.into(),
132                font: ctx.font,
133                ..canvas::Text::default()
134            });
135        } else {
136            // Draw wrap indicator for continuation lines
137            frame.fill_text(canvas::Text {
138                content: "↪".to_string(),
139                position: Point::new(ctx.gutter_width - 20.0, y + 2.0),
140                color: self.style.line_number_color,
141                size: ctx.font_size.into(),
142                font: ctx.font,
143                ..canvas::Text::default()
144            });
145        }
146    }
147
148    /// Draws the background highlight for the current line.
149    ///
150    /// # Arguments
151    ///
152    /// * `frame` - The canvas frame to draw on
153    /// * `ctx` - Rendering context containing visual lines and metrics
154    /// * `visual_line` - The visual line to check
155    /// * `y` - Y position for rendering
156    fn draw_current_line_highlight(
157        &self,
158        frame: &mut canvas::Frame,
159        ctx: &RenderContext,
160        visual_line: &VisualLine,
161        y: f32,
162    ) {
163        if visual_line.logical_line == self.cursor.0 {
164            frame.fill_rectangle(
165                Point::new(ctx.gutter_width, y),
166                Size::new(ctx.bounds_width - ctx.gutter_width, ctx.line_height),
167                self.style.current_line_highlight,
168            );
169        }
170    }
171
172    /// Draws text content with syntax highlighting or plain text fallback.
173    ///
174    /// # Arguments
175    ///
176    /// * `frame` - The canvas frame to draw on
177    /// * `ctx` - Rendering context containing visual lines and metrics
178    /// * `visual_line` - The visual line to render
179    /// * `y` - Y position for rendering
180    /// * `syntax_ref` - Optional syntax reference for highlighting
181    /// * `syntax_set` - Syntax set for highlighting
182    /// * `syntax_theme` - Theme for syntax highlighting
183    #[allow(clippy::too_many_arguments)]
184    fn draw_text_with_syntax_highlighting(
185        &self,
186        frame: &mut canvas::Frame,
187        ctx: &RenderContext,
188        visual_line: &VisualLine,
189        y: f32,
190        syntax_ref: Option<&syntect::parsing::SyntaxReference>,
191        syntax_set: &SyntaxSet,
192        syntax_theme: Option<&syntect::highlighting::Theme>,
193    ) {
194        let full_line_content = self.buffer.line(visual_line.logical_line);
195
196        // Convert character indices to byte indices for UTF-8 string slicing
197        let start_byte = full_line_content
198            .char_indices()
199            .nth(visual_line.start_col)
200            .map_or(full_line_content.len(), |(idx, _)| idx);
201        let end_byte = full_line_content
202            .char_indices()
203            .nth(visual_line.end_col)
204            .map_or(full_line_content.len(), |(idx, _)| idx);
205        let line_segment = &full_line_content[start_byte..end_byte];
206
207        if let (Some(syntax), Some(syntax_theme)) = (syntax_ref, syntax_theme) {
208            let mut highlighter = HighlightLines::new(syntax, syntax_theme);
209
210            // Highlight the full line to get correct token colors
211            let full_line_ranges = highlighter
212                .highlight_line(full_line_content, syntax_set)
213                .unwrap_or_else(|_| {
214                    vec![(Style::default(), full_line_content)]
215                });
216
217            // Extract only the ranges that fall within our segment
218            let mut x_offset = ctx.gutter_width + 5.0;
219            let mut char_pos = 0;
220
221            for (style, text) in full_line_ranges {
222                let text_len = text.chars().count();
223                let text_end = char_pos + text_len;
224
225                // Check if this token intersects with our segment
226                if text_end > visual_line.start_col
227                    && char_pos < visual_line.end_col
228                {
229                    // Calculate the intersection
230                    let segment_start = char_pos.max(visual_line.start_col);
231                    let segment_end = text_end.min(visual_line.end_col);
232
233                    let text_start_offset =
234                        segment_start.saturating_sub(char_pos);
235                    let text_end_offset =
236                        text_start_offset + (segment_end - segment_start);
237
238                    // Convert character offsets to byte offsets for UTF-8 slicing
239                    let start_byte = text
240                        .char_indices()
241                        .nth(text_start_offset)
242                        .map_or(text.len(), |(idx, _)| idx);
243                    let end_byte = text
244                        .char_indices()
245                        .nth(text_end_offset)
246                        .map_or(text.len(), |(idx, _)| idx);
247
248                    let segment_text = &text[start_byte..end_byte];
249
250                    let color = Color::from_rgb(
251                        f32::from(style.foreground.r) / 255.0,
252                        f32::from(style.foreground.g) / 255.0,
253                        f32::from(style.foreground.b) / 255.0,
254                    );
255
256                    frame.fill_text(canvas::Text {
257                        content: segment_text.to_string(),
258                        position: Point::new(x_offset, y + 2.0),
259                        color,
260                        size: ctx.font_size.into(),
261                        font: ctx.font,
262                        ..canvas::Text::default()
263                    });
264
265                    x_offset += measure_text_width(
266                        segment_text,
267                        ctx.full_char_width,
268                        ctx.char_width,
269                    );
270                }
271
272                char_pos = text_end;
273            }
274        } else {
275            // Fallback to plain text
276            frame.fill_text(canvas::Text {
277                content: line_segment.to_string(),
278                position: Point::new(ctx.gutter_width + 5.0, y + 2.0),
279                color: self.style.text_color,
280                size: ctx.font_size.into(),
281                font: ctx.font,
282                ..canvas::Text::default()
283            });
284        }
285    }
286
287    /// Draws search match highlights for all visible matches.
288    ///
289    /// # Arguments
290    ///
291    /// * `frame` - The canvas frame to draw on
292    /// * `ctx` - Rendering context containing visual lines and metrics
293    /// * `first_visible_line` - First visible visual line index
294    /// * `last_visible_line` - Last visible visual line index
295    fn draw_search_highlights(
296        &self,
297        frame: &mut canvas::Frame,
298        ctx: &RenderContext,
299        start_visual_idx: usize,
300        end_visual_idx: usize,
301    ) {
302        if !self.search_state.is_open || self.search_state.query.is_empty() {
303            return;
304        }
305
306        let query_len = self.search_state.query.chars().count();
307
308        let start_visual_idx = start_visual_idx.min(ctx.visual_lines.len());
309        let end_visual_idx = end_visual_idx.min(ctx.visual_lines.len());
310
311        let end_visual_inclusive = end_visual_idx
312            .saturating_sub(1)
313            .min(ctx.visual_lines.len().saturating_sub(1));
314
315        if let (Some(start_vl), Some(end_vl)) = (
316            ctx.visual_lines.get(start_visual_idx),
317            ctx.visual_lines.get(end_visual_inclusive),
318        ) {
319            let min_logical_line = start_vl.logical_line;
320            let max_logical_line = end_vl.logical_line;
321
322            // Optimization: Use get_visible_match_range to find matches in view
323            // This uses binary search + early termination for O(log N) performance
324            let match_range = super::search::get_visible_match_range(
325                &self.search_state.matches,
326                min_logical_line,
327                max_logical_line,
328            );
329
330            for (match_idx, search_match) in self
331                .search_state
332                .matches
333                .iter()
334                .enumerate()
335                .skip(match_range.start)
336                .take(match_range.len())
337            {
338                // Determine if this is the current match
339                let is_current =
340                    self.search_state.current_match_index == Some(match_idx);
341
342                let highlight_color = if is_current {
343                    // Orange for current match
344                    Color { r: 1.0, g: 0.6, b: 0.0, a: 0.4 }
345                } else {
346                    // Yellow for other matches
347                    Color { r: 1.0, g: 1.0, b: 0.0, a: 0.3 }
348                };
349
350                // Convert logical position to visual line
351                let start_visual = WrappingCalculator::logical_to_visual(
352                    ctx.visual_lines,
353                    search_match.line,
354                    search_match.col,
355                );
356                let end_visual = WrappingCalculator::logical_to_visual(
357                    ctx.visual_lines,
358                    search_match.line,
359                    search_match.col + query_len,
360                );
361
362                if let (Some(start_v), Some(end_v)) = (start_visual, end_visual)
363                {
364                    if start_v == end_v {
365                        // Match within same visual line
366                        let y = start_v as f32 * ctx.line_height;
367                        let vl = &ctx.visual_lines[start_v];
368                        let line_content = self.buffer.line(vl.logical_line);
369
370                        // Use calculate_segment_geometry to compute match position and width
371                        let (x_start, match_width) = calculate_segment_geometry(
372                            line_content,
373                            vl.start_col,
374                            search_match.col,
375                            search_match.col + query_len,
376                            ctx.gutter_width + 5.0,
377                            ctx.full_char_width,
378                            ctx.char_width,
379                        );
380                        let x_end = x_start + match_width;
381
382                        frame.fill_rectangle(
383                            Point::new(x_start, y + 2.0),
384                            Size::new(x_end - x_start, ctx.line_height - 4.0),
385                            highlight_color,
386                        );
387                    } else {
388                        // Match spans multiple visual lines
389                        for (v_idx, vl) in ctx
390                            .visual_lines
391                            .iter()
392                            .enumerate()
393                            .skip(start_v)
394                            .take(end_v - start_v + 1)
395                        {
396                            let y = v_idx as f32 * ctx.line_height;
397
398                            let match_start_col = search_match.col;
399                            let match_end_col = search_match.col + query_len;
400
401                            let sel_start_col = if v_idx == start_v {
402                                match_start_col
403                            } else {
404                                vl.start_col
405                            };
406                            let sel_end_col = if v_idx == end_v {
407                                match_end_col
408                            } else {
409                                vl.end_col
410                            };
411
412                            let line_content =
413                                self.buffer.line(vl.logical_line);
414
415                            let (x_start, sel_width) =
416                                calculate_segment_geometry(
417                                    line_content,
418                                    vl.start_col,
419                                    sel_start_col,
420                                    sel_end_col,
421                                    ctx.gutter_width + 5.0,
422                                    ctx.full_char_width,
423                                    ctx.char_width,
424                                );
425                            let x_end = x_start + sel_width;
426
427                            frame.fill_rectangle(
428                                Point::new(x_start, y + 2.0),
429                                Size::new(
430                                    x_end - x_start,
431                                    ctx.line_height - 4.0,
432                                ),
433                                highlight_color,
434                            );
435                        }
436                    }
437                }
438            }
439        }
440    }
441
442    /// Draws text selection highlights.
443    ///
444    /// # Arguments
445    ///
446    /// * `frame` - The canvas frame to draw on
447    /// * `ctx` - Rendering context containing visual lines and metrics
448    fn draw_selection_highlight(
449        &self,
450        frame: &mut canvas::Frame,
451        ctx: &RenderContext,
452    ) {
453        if let Some((start, end)) = self.get_selection_range()
454            && start != end
455        {
456            let selection_color = Color { r: 0.3, g: 0.5, b: 0.8, a: 0.3 };
457
458            if start.0 == end.0 {
459                // Single line selection - need to handle wrapped segments
460                let start_visual = WrappingCalculator::logical_to_visual(
461                    ctx.visual_lines,
462                    start.0,
463                    start.1,
464                );
465                let end_visual = WrappingCalculator::logical_to_visual(
466                    ctx.visual_lines,
467                    end.0,
468                    end.1,
469                );
470
471                if let (Some(start_v), Some(end_v)) = (start_visual, end_visual)
472                {
473                    if start_v == end_v {
474                        // Selection within same visual line
475                        let y = start_v as f32 * ctx.line_height;
476                        let vl = &ctx.visual_lines[start_v];
477                        let line_content = self.buffer.line(vl.logical_line);
478
479                        let (x_start, sel_width) = calculate_segment_geometry(
480                            line_content,
481                            vl.start_col,
482                            start.1,
483                            end.1,
484                            ctx.gutter_width + 5.0,
485                            ctx.full_char_width,
486                            ctx.char_width,
487                        );
488                        let x_end = x_start + sel_width;
489
490                        frame.fill_rectangle(
491                            Point::new(x_start, y + 2.0),
492                            Size::new(x_end - x_start, ctx.line_height - 4.0),
493                            selection_color,
494                        );
495                    } else {
496                        // Selection spans multiple visual lines (same logical line)
497                        for (v_idx, vl) in ctx
498                            .visual_lines
499                            .iter()
500                            .enumerate()
501                            .skip(start_v)
502                            .take(end_v - start_v + 1)
503                        {
504                            let y = v_idx as f32 * ctx.line_height;
505
506                            let sel_start_col = if v_idx == start_v {
507                                start.1
508                            } else {
509                                vl.start_col
510                            };
511                            let sel_end_col =
512                                if v_idx == end_v { end.1 } else { vl.end_col };
513
514                            let line_content =
515                                self.buffer.line(vl.logical_line);
516
517                            let (x_start, sel_width) =
518                                calculate_segment_geometry(
519                                    line_content,
520                                    vl.start_col,
521                                    sel_start_col,
522                                    sel_end_col,
523                                    ctx.gutter_width + 5.0,
524                                    ctx.full_char_width,
525                                    ctx.char_width,
526                                );
527                            let x_end = x_start + sel_width;
528
529                            frame.fill_rectangle(
530                                Point::new(x_start, y + 2.0),
531                                Size::new(
532                                    x_end - x_start,
533                                    ctx.line_height - 4.0,
534                                ),
535                                selection_color,
536                            );
537                        }
538                    }
539                }
540            } else {
541                // Multi-line selection
542                let start_visual = WrappingCalculator::logical_to_visual(
543                    ctx.visual_lines,
544                    start.0,
545                    start.1,
546                );
547                let end_visual = WrappingCalculator::logical_to_visual(
548                    ctx.visual_lines,
549                    end.0,
550                    end.1,
551                );
552
553                if let (Some(start_v), Some(end_v)) = (start_visual, end_visual)
554                {
555                    for (v_idx, vl) in ctx
556                        .visual_lines
557                        .iter()
558                        .enumerate()
559                        .skip(start_v)
560                        .take(end_v - start_v + 1)
561                    {
562                        let y = v_idx as f32 * ctx.line_height;
563
564                        let sel_start_col =
565                            if vl.logical_line == start.0 && v_idx == start_v {
566                                start.1
567                            } else {
568                                vl.start_col
569                            };
570
571                        let sel_end_col =
572                            if vl.logical_line == end.0 && v_idx == end_v {
573                                end.1
574                            } else {
575                                vl.end_col
576                            };
577
578                        let line_content = self.buffer.line(vl.logical_line);
579
580                        let (x_start, sel_width) = calculate_segment_geometry(
581                            line_content,
582                            vl.start_col,
583                            sel_start_col,
584                            sel_end_col,
585                            ctx.gutter_width + 5.0,
586                            ctx.full_char_width,
587                            ctx.char_width,
588                        );
589                        let x_end = x_start + sel_width;
590
591                        frame.fill_rectangle(
592                            Point::new(x_start, y + 2.0),
593                            Size::new(x_end - x_start, ctx.line_height - 4.0),
594                            selection_color,
595                        );
596                    }
597                }
598            }
599        }
600    }
601
602    /// Draws the cursor (normal caret or IME preedit cursor).
603    ///
604    /// # Arguments
605    ///
606    /// * `frame` - The canvas frame to draw on
607    /// * `ctx` - Rendering context containing visual lines and metrics
608    fn draw_cursor(&self, frame: &mut canvas::Frame, ctx: &RenderContext) {
609        // Cursor drawing logic (only when the editor has focus)
610        // -------------------------------------------------------------------------
611        // Core notes:
612        // 1. Choose the drawing path based on whether IME preedit is present.
613        // 2. Require both `is_focused()` (Iced focus) and `has_canvas_focus()` (internal focus)
614        //    so the cursor is drawn only in the active editor, avoiding multiple cursors.
615        // 3. Use `WrappingCalculator` to map logical (line, col) to visual (x, y)
616        //    for correct cursor positioning with line wrapping.
617        // -------------------------------------------------------------------------
618        if self.show_cursor
619            && self.cursor_visible
620            && self.has_focus()
621            && self.ime_preedit.is_some()
622        {
623            // [Branch A] IME preedit rendering mode
624            // ---------------------------------------------------------------------
625            // When the user is composing with an IME (e.g. pinyin before commit),
626            // draw a preedit region instead of the normal caret, including:
627            // - preedit background (highlighting the composing text)
628            // - preedit text content (preedit.content)
629            // - preedit selection (underline or selection background)
630            // - preedit caret
631            // ---------------------------------------------------------------------
632            if let Some(cursor_visual) = WrappingCalculator::logical_to_visual(
633                ctx.visual_lines,
634                self.cursor.0,
635                self.cursor.1,
636            ) {
637                let vl = &ctx.visual_lines[cursor_visual];
638                let line_content = self.buffer.line(vl.logical_line);
639
640                // Compute the preedit region start X
641                // Use calculate_segment_geometry to ensure correct CJK width handling
642                let (cursor_x, _) = calculate_segment_geometry(
643                    line_content,
644                    vl.start_col,
645                    self.cursor.1,
646                    self.cursor.1,
647                    ctx.gutter_width + 5.0,
648                    ctx.full_char_width,
649                    ctx.char_width,
650                );
651                let cursor_y = cursor_visual as f32 * ctx.line_height;
652
653                if let Some(preedit) = self.ime_preedit.as_ref() {
654                    let preedit_width = measure_text_width(
655                        &preedit.content,
656                        ctx.full_char_width,
657                        ctx.char_width,
658                    );
659
660                    // 1. Draw preedit background (light translucent)
661                    // This indicates the text is not committed yet
662                    frame.fill_rectangle(
663                        Point::new(cursor_x, cursor_y + 2.0),
664                        Size::new(preedit_width, ctx.line_height - 4.0),
665                        Color { r: 1.0, g: 1.0, b: 1.0, a: 0.08 },
666                    );
667
668                    // 2. Draw preedit selection (if any)
669                    // IME may mark a selection inside preedit text (e.g. segmentation)
670                    // The range uses UTF-8 byte indices, so slices must be safe
671                    if let Some(range) = preedit.selection.as_ref()
672                        && range.start != range.end
673                    {
674                        // Validate indices before slicing to prevent panic
675                        if let Some((start, end)) = validate_selection_indices(
676                            &preedit.content,
677                            range.start,
678                            range.end,
679                        ) {
680                            let selected_prefix = &preedit.content[..start];
681                            let selected_text = &preedit.content[start..end];
682
683                            let selection_x = cursor_x
684                                + measure_text_width(
685                                    selected_prefix,
686                                    ctx.full_char_width,
687                                    ctx.char_width,
688                                );
689                            let selection_w = measure_text_width(
690                                selected_text,
691                                ctx.full_char_width,
692                                ctx.char_width,
693                            );
694
695                            frame.fill_rectangle(
696                                Point::new(selection_x, cursor_y + 2.0),
697                                Size::new(selection_w, ctx.line_height - 4.0),
698                                Color { r: 0.3, g: 0.5, b: 0.8, a: 0.3 },
699                            );
700                        }
701                    }
702
703                    // 3. Draw preedit text itself
704                    frame.fill_text(canvas::Text {
705                        content: preedit.content.clone(),
706                        position: Point::new(cursor_x, cursor_y + 2.0),
707                        color: self.style.text_color,
708                        size: ctx.font_size.into(),
709                        font: ctx.font,
710                        ..canvas::Text::default()
711                    });
712
713                    // 4. Draw bottom underline (IME state indicator)
714                    frame.fill_rectangle(
715                        Point::new(cursor_x, cursor_y + ctx.line_height - 3.0),
716                        Size::new(preedit_width, 1.0),
717                        self.style.text_color,
718                    );
719
720                    // 5. Draw preedit caret
721                    // If IME provides a caret position (usually selection end), draw a thin bar
722                    if let Some(range) = preedit.selection.as_ref() {
723                        let caret_end = range.end.min(preedit.content.len());
724
725                        // Validate caret position to avoid panic on invalid UTF-8 boundary
726                        if caret_end <= preedit.content.len()
727                            && preedit.content.is_char_boundary(caret_end)
728                        {
729                            let caret_prefix = &preedit.content[..caret_end];
730                            let caret_x = cursor_x
731                                + measure_text_width(
732                                    caret_prefix,
733                                    ctx.full_char_width,
734                                    ctx.char_width,
735                                );
736
737                            frame.fill_rectangle(
738                                Point::new(caret_x, cursor_y + 2.0),
739                                Size::new(2.0, ctx.line_height - 4.0),
740                                self.style.text_color,
741                            );
742                        }
743                    }
744                }
745            }
746        } else if self.show_cursor && self.cursor_visible && self.has_focus() {
747            // [Branch B] Normal caret rendering mode
748            // ---------------------------------------------------------------------
749            // When there is no IME preedit, draw the standard editor caret.
750            // Key checks:
751            // - is_focused(): the widget has Iced focus
752            // - has_canvas_focus: internal focus state (mouse clicks, etc.)
753            // - draw only when both are true to avoid ghost cursors
754            // ---------------------------------------------------------------------
755
756            // Map logical cursor position (Line, Col) to visual line index
757            // to handle line wrapping changes
758            if let Some(cursor_visual) = WrappingCalculator::logical_to_visual(
759                ctx.visual_lines,
760                self.cursor.0,
761                self.cursor.1,
762            ) {
763                let vl = &ctx.visual_lines[cursor_visual];
764                let line_content = self.buffer.line(vl.logical_line);
765
766                // Compute exact caret X position
767                // Account for gutter width, left padding, and rendered prefix width
768                let (cursor_x, _) = calculate_segment_geometry(
769                    line_content,
770                    vl.start_col,
771                    self.cursor.1,
772                    self.cursor.1,
773                    ctx.gutter_width + 5.0,
774                    ctx.full_char_width,
775                    ctx.char_width,
776                );
777                let cursor_y = cursor_visual as f32 * ctx.line_height;
778
779                // Draw standard caret (2px vertical bar)
780                frame.fill_rectangle(
781                    Point::new(cursor_x, cursor_y + 2.0),
782                    Size::new(2.0, ctx.line_height - 4.0),
783                    self.style.text_color,
784                );
785            }
786        }
787    }
788
789    /// Checks if the editor has focus (both Iced focus and internal canvas focus).
790    ///
791    /// # Returns
792    ///
793    /// `true` if the editor has both Iced focus and internal canvas focus and is not focus-locked; `false` otherwise
794    pub(crate) fn has_focus(&self) -> bool {
795        // Check if this editor has Iced focus
796        let focused_id =
797            super::FOCUSED_EDITOR_ID.load(std::sync::atomic::Ordering::Relaxed);
798        focused_id == self.editor_id
799            && self.has_canvas_focus
800            && !self.focus_locked
801    }
802
803    /// Handles keyboard shortcut combinations (Ctrl+C, Ctrl+Z, etc.).
804    ///
805    /// This implementation includes focus chain management for Tab and Shift+Tab
806    /// navigation between editors.
807    ///
808    /// # Arguments
809    ///
810    /// * `key` - The keyboard key that was pressed
811    /// * `modifiers` - The keyboard modifiers (Ctrl, Shift, Alt, etc.)
812    ///
813    /// # Returns
814    ///
815    /// `Some(Action<Message>)` if a shortcut was matched, `None` otherwise
816    fn handle_keyboard_shortcuts(
817        &self,
818        key: &keyboard::Key,
819        modifiers: &keyboard::Modifiers,
820    ) -> Option<Action<Message>> {
821        // Handle Tab for focus navigation when search dialog is not open
822        // This implements focus chain management between multiple editors
823        if matches!(key, keyboard::Key::Named(keyboard::key::Named::Tab))
824            && !self.search_state.is_open
825        {
826            if modifiers.shift() {
827                // Shift+Tab: focus navigation backward
828                return Some(
829                    Action::publish(Message::FocusNavigationShiftTab)
830                        .and_capture(),
831                );
832            } else {
833                // Tab: focus navigation forward
834                return Some(
835                    Action::publish(Message::FocusNavigationTab).and_capture(),
836                );
837            }
838        }
839
840        // Handle Ctrl+C / Ctrl+Insert (copy)
841        if (modifiers.control()
842            && matches!(key, keyboard::Key::Character(c) if c.as_str() == "c"))
843            || (modifiers.control()
844                && matches!(
845                    key,
846                    keyboard::Key::Named(keyboard::key::Named::Insert)
847                ))
848        {
849            return Some(Action::publish(Message::Copy).and_capture());
850        }
851
852        // Handle Ctrl+Z (undo)
853        if modifiers.control()
854            && matches!(key, keyboard::Key::Character(z) if z.as_str() == "z")
855        {
856            return Some(Action::publish(Message::Undo).and_capture());
857        }
858
859        // Handle Ctrl+Y (redo)
860        if modifiers.control()
861            && matches!(key, keyboard::Key::Character(y) if y.as_str() == "y")
862        {
863            return Some(Action::publish(Message::Redo).and_capture());
864        }
865
866        // Handle Ctrl+F (open search)
867        if modifiers.control()
868            && matches!(key, keyboard::Key::Character(f) if f.as_str() == "f")
869            && self.search_replace_enabled
870        {
871            return Some(Action::publish(Message::OpenSearch).and_capture());
872        }
873
874        // Handle Ctrl+H (open search and replace)
875        if modifiers.control()
876            && matches!(key, keyboard::Key::Character(h) if h.as_str() == "h")
877            && self.search_replace_enabled
878        {
879            return Some(
880                Action::publish(Message::OpenSearchReplace).and_capture(),
881            );
882        }
883
884        // Handle Escape (close search dialog if open)
885        if matches!(key, keyboard::Key::Named(keyboard::key::Named::Escape)) {
886            return Some(Action::publish(Message::CloseSearch).and_capture());
887        }
888
889        // Handle Tab (cycle forward in search dialog if open)
890        if matches!(key, keyboard::Key::Named(keyboard::key::Named::Tab))
891            && self.search_state.is_open
892        {
893            if modifiers.shift() {
894                // Shift+Tab: cycle backward
895                return Some(
896                    Action::publish(Message::SearchDialogShiftTab)
897                        .and_capture(),
898                );
899            } else {
900                // Tab: cycle forward
901                return Some(
902                    Action::publish(Message::SearchDialogTab).and_capture(),
903                );
904            }
905        }
906
907        // Handle F3 (find next) and Shift+F3 (find previous)
908        if matches!(key, keyboard::Key::Named(keyboard::key::Named::F3))
909            && self.search_replace_enabled
910        {
911            if modifiers.shift() {
912                return Some(
913                    Action::publish(Message::FindPrevious).and_capture(),
914                );
915            } else {
916                return Some(Action::publish(Message::FindNext).and_capture());
917            }
918        }
919
920        // Handle Ctrl+V / Shift+Insert (paste) - read clipboard and send paste message
921        if (modifiers.control()
922            && matches!(key, keyboard::Key::Character(v) if v.as_str() == "v"))
923            || (modifiers.shift()
924                && matches!(
925                    key,
926                    keyboard::Key::Named(keyboard::key::Named::Insert)
927                ))
928        {
929            // Return an action that requests clipboard read
930            return Some(Action::publish(Message::Paste(String::new())));
931        }
932
933        // Handle Ctrl+Home (go to start of document)
934        if modifiers.control()
935            && matches!(key, keyboard::Key::Named(keyboard::key::Named::Home))
936        {
937            return Some(Action::publish(Message::CtrlHome).and_capture());
938        }
939
940        // Handle Ctrl+End (go to end of document)
941        if modifiers.control()
942            && matches!(key, keyboard::Key::Named(keyboard::key::Named::End))
943        {
944            return Some(Action::publish(Message::CtrlEnd).and_capture());
945        }
946
947        // Handle Shift+Delete (delete selection)
948        if modifiers.shift()
949            && matches!(key, keyboard::Key::Named(keyboard::key::Named::Delete))
950        {
951            return Some(
952                Action::publish(Message::DeleteSelection).and_capture(),
953            );
954        }
955
956        None
957    }
958
959    /// Handles character input and special navigation keys.
960    ///
961    /// This implementation includes focus event propagation and focus chain management
962    /// for proper focus handling without mouse bounds checking.
963    ///
964    /// # Arguments
965    ///
966    /// * `key` - The keyboard key that was pressed
967    /// * `modifiers` - The keyboard modifiers (Ctrl, Shift, Alt, etc.)
968    /// * `text` - Optional text content from the keyboard event
969    ///
970    /// # Returns
971    ///
972    /// `Some(Action<Message>)` if input should be processed, `None` otherwise
973    #[allow(clippy::unused_self)]
974    fn handle_character_input(
975        &self,
976        key: &keyboard::Key,
977        modifiers: &keyboard::Modifiers,
978        text: Option<&str>,
979    ) -> Option<Action<Message>> {
980        // Early exit: Only process character input when editor has focus
981        // This prevents focus stealing where characters typed in other input fields
982        // appear in the editor
983        if !self.has_focus() {
984            return None;
985        }
986
987        // PRIORITY 1: Check if 'text' field has valid printable character
988        // This handles:
989        // - Numpad keys with NumLock ON (key=Named(ArrowDown), text=Some("2"))
990        // - Regular typing with shift, accents, international layouts
991        if let Some(text_content) = text
992            && !text_content.is_empty()
993            && !modifiers.control()
994            && !modifiers.alt()
995        {
996            // Check if it's a printable character (not a control character)
997            // This filters out Enter (\n), Tab (\t), Delete (U+007F), etc.
998            if let Some(first_char) = text_content.chars().next()
999                && !first_char.is_control()
1000            {
1001                return Some(
1002                    Action::publish(Message::CharacterInput(first_char))
1003                        .and_capture(),
1004                );
1005            }
1006        }
1007
1008        // PRIORITY 2: Handle special named keys (navigation, editing)
1009        // These are only processed if text didn't contain a printable character
1010        let message = match key {
1011            keyboard::Key::Named(keyboard::key::Named::Backspace) => {
1012                Some(Message::Backspace)
1013            }
1014            keyboard::Key::Named(keyboard::key::Named::Delete) => {
1015                Some(Message::Delete)
1016            }
1017            keyboard::Key::Named(keyboard::key::Named::Enter) => {
1018                Some(Message::Enter)
1019            }
1020            keyboard::Key::Named(keyboard::key::Named::Tab) => {
1021                // Handle Tab for focus navigation or text insertion
1022                // This implements focus event propagation and focus chain management
1023                if modifiers.shift() {
1024                    // Shift+Tab: focus navigation backward through widget hierarchy
1025                    Some(Message::FocusNavigationShiftTab)
1026                } else {
1027                    // Regular Tab: check if search dialog is open
1028                    if self.search_state.is_open {
1029                        Some(Message::SearchDialogTab)
1030                    } else {
1031                        // Insert 4 spaces for Tab when not in search dialog
1032                        Some(Message::Tab)
1033                    }
1034                }
1035            }
1036            keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
1037                Some(Message::ArrowKey(ArrowDirection::Up, modifiers.shift()))
1038            }
1039            keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
1040                Some(Message::ArrowKey(ArrowDirection::Down, modifiers.shift()))
1041            }
1042            keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
1043                Some(Message::ArrowKey(ArrowDirection::Left, modifiers.shift()))
1044            }
1045            keyboard::Key::Named(keyboard::key::Named::ArrowRight) => Some(
1046                Message::ArrowKey(ArrowDirection::Right, modifiers.shift()),
1047            ),
1048            keyboard::Key::Named(keyboard::key::Named::PageUp) => {
1049                Some(Message::PageUp)
1050            }
1051            keyboard::Key::Named(keyboard::key::Named::PageDown) => {
1052                Some(Message::PageDown)
1053            }
1054            keyboard::Key::Named(keyboard::key::Named::Home) => {
1055                Some(Message::Home(modifiers.shift()))
1056            }
1057            keyboard::Key::Named(keyboard::key::Named::End) => {
1058                Some(Message::End(modifiers.shift()))
1059            }
1060            // PRIORITY 3: Fallback to extracting from 'key' if text was empty/control char
1061            // This handles edge cases where text field is not populated
1062            _ => {
1063                if !modifiers.control()
1064                    && !modifiers.alt()
1065                    && let keyboard::Key::Character(c) = key
1066                    && !c.is_empty()
1067                {
1068                    return c
1069                        .chars()
1070                        .next()
1071                        .map(Message::CharacterInput)
1072                        .map(|msg| Action::publish(msg).and_capture());
1073                }
1074                None
1075            }
1076        };
1077
1078        message.map(|msg| Action::publish(msg).and_capture())
1079    }
1080
1081    /// Handles keyboard events with focus event propagation through widget hierarchy.
1082    ///
1083    /// This implementation completes focus handling without mouse bounds checking
1084    /// and ensures proper focus chain management.
1085    ///
1086    /// # Arguments
1087    ///
1088    /// * `key` - The keyboard key that was pressed
1089    /// * `modifiers` - The keyboard modifiers (Ctrl, Shift, Alt, etc.)
1090    /// * `text` - Optional text content from the keyboard event
1091    /// * `bounds` - The rectangle bounds of the canvas widget (unused in this implementation)
1092    /// * `cursor` - The current mouse cursor position and status (unused in this implementation)
1093    ///
1094    /// # Returns
1095    ///
1096    /// `Some(Action<Message>)` if the event was handled, `None` otherwise
1097    fn handle_keyboard_event(
1098        &self,
1099        key: &keyboard::Key,
1100        modifiers: &keyboard::Modifiers,
1101        text: &Option<iced::advanced::graphics::core::SmolStr>,
1102        _bounds: Rectangle,
1103        _cursor: &mouse::Cursor,
1104    ) -> Option<Action<Message>> {
1105        // Early exit: Check if editor has focus and is not focus-locked
1106        // This prevents focus stealing where keyboard input meant for other widgets
1107        // is incorrectly processed by this editor during focus transitions
1108        if !self.has_focus() || self.focus_locked {
1109            return None;
1110        }
1111
1112        // Skip if IME is active (unless Ctrl/Command is pressed)
1113        if self.ime_preedit.is_some()
1114            && !(modifiers.control() || modifiers.command())
1115        {
1116            return None;
1117        }
1118
1119        // Try keyboard shortcuts first
1120        if let Some(action) = self.handle_keyboard_shortcuts(key, modifiers) {
1121            return Some(action);
1122        }
1123
1124        // Handle character input and special keys
1125        // Convert Option<SmolStr> to Option<&str>
1126        let text_str = text.as_ref().map(|s| s.as_str());
1127        self.handle_character_input(key, modifiers, text_str)
1128    }
1129
1130    /// Handles mouse events (button presses, movement, releases).
1131    ///
1132    /// # Arguments
1133    ///
1134    /// * `event` - The mouse event to handle
1135    /// * `bounds` - The rectangle bounds of the canvas widget
1136    /// * `cursor` - The current mouse cursor position and status
1137    ///
1138    /// # Returns
1139    ///
1140    /// `Some(Action<Message>)` if the event was handled, `None` otherwise
1141    #[allow(clippy::unused_self)]
1142    fn handle_mouse_event(
1143        &self,
1144        event: &mouse::Event,
1145        bounds: Rectangle,
1146        cursor: &mouse::Cursor,
1147    ) -> Option<Action<Message>> {
1148        match event {
1149            mouse::Event::ButtonPressed(mouse::Button::Left) => {
1150                cursor.position_in(bounds).map(|position| {
1151                    // Don't capture the event so it can bubble up for focus management
1152                    // This implements focus event propagation through the widget hierarchy
1153                    Action::publish(Message::MouseClick(position))
1154                })
1155            }
1156            mouse::Event::CursorMoved { .. } => {
1157                // Handle mouse drag for selection only when cursor is within bounds
1158                cursor.position_in(bounds).map(|position| {
1159                    Action::publish(Message::MouseDrag(position)).and_capture()
1160                })
1161            }
1162            mouse::Event::ButtonReleased(mouse::Button::Left) => {
1163                // Only handle mouse release when cursor is within bounds
1164                // This prevents capturing events meant for other widgets
1165                if cursor.is_over(bounds) {
1166                    Some(Action::publish(Message::MouseRelease).and_capture())
1167                } else {
1168                    None
1169                }
1170            }
1171            _ => None,
1172        }
1173    }
1174
1175    /// Handles IME (Input Method Editor) events for complex text input.
1176    ///
1177    /// # Arguments
1178    ///
1179    /// * `event` - The IME event to handle
1180    /// * `bounds` - The rectangle bounds of the canvas widget
1181    /// * `cursor` - The current mouse cursor position and status
1182    ///
1183    /// # Returns
1184    ///
1185    /// `Some(Action<Message>)` if the event was handled, `None` otherwise
1186    fn handle_ime_event(
1187        &self,
1188        event: &input_method::Event,
1189        _bounds: Rectangle,
1190        _cursor: &mouse::Cursor,
1191    ) -> Option<Action<Message>> {
1192        // Early exit: Check if editor has focus and is not focus-locked
1193        // This prevents focus stealing where IME events meant for other widgets
1194        // are incorrectly processed by this editor during focus transitions
1195        if !self.has_focus() || self.focus_locked {
1196            return None;
1197        }
1198
1199        // IME event handling
1200        // ---------------------------------------------------------------------
1201        // Core mapping: convert Iced IME events into editor Messages
1202        //
1203        // Flow:
1204        // 1. Opened: IME activated (e.g. switching input method). Clear old preedit state.
1205        // 2. Preedit: User is composing (e.g. typing "nihao" before commit).
1206        //    - content: current candidate text
1207        //    - selection: selection range within the text, in bytes
1208        // 3. Commit: User confirms a candidate and commits text into the buffer.
1209        // 4. Closed: IME closed or lost focus.
1210        //
1211        // Safety checks:
1212        // - handle only when `focused_id` matches this editor ID
1213        // - handle only when `has_canvas_focus` is true
1214        // This ensures IME events are not delivered to the wrong widget.
1215        // ---------------------------------------------------------------------
1216        let message = match event {
1217            input_method::Event::Opened => Message::ImeOpened,
1218            input_method::Event::Preedit(content, selection) => {
1219                Message::ImePreedit(content.clone(), selection.clone())
1220            }
1221            input_method::Event::Commit(content) => {
1222                Message::ImeCommit(content.clone())
1223            }
1224            input_method::Event::Closed => Message::ImeClosed,
1225        };
1226
1227        Some(Action::publish(message).and_capture())
1228    }
1229}
1230
1231impl canvas::Program<Message> for CodeEditor {
1232    type State = ();
1233
1234    /// Renders the code editor's visual elements on the canvas, including text layout, syntax highlighting,
1235    /// cursor positioning, and other graphical aspects.
1236    ///
1237    /// # Arguments
1238    ///
1239    /// * `state` - The current state of the canvas
1240    /// * `renderer` - The renderer used for drawing
1241    /// * `theme` - The theme for styling
1242    /// * `bounds` - The rectangle bounds of the canvas
1243    /// * `cursor` - The mouse cursor position
1244    ///
1245    /// # Returns
1246    ///
1247    /// A vector of `Geometry` objects representing the drawn elements
1248    fn draw(
1249        &self,
1250        _state: &Self::State,
1251        renderer: &iced::Renderer,
1252        _theme: &Theme,
1253        bounds: Rectangle,
1254        _cursor: mouse::Cursor,
1255    ) -> Vec<Geometry> {
1256        let visual_lines: Rc<Vec<VisualLine>> =
1257            self.visual_lines_cached(bounds.width);
1258
1259        // Prefer the tracked viewport height when available, but fall back to
1260        // the current bounds during initial layout when viewport metrics have
1261        // not been populated yet.
1262        let effective_viewport_height = if self.viewport_height > 0.0 {
1263            self.viewport_height
1264        } else {
1265            bounds.height
1266        };
1267        let first_visible_line =
1268            (self.viewport_scroll / self.line_height).floor() as usize;
1269        let visible_lines_count =
1270            (effective_viewport_height / self.line_height).ceil() as usize + 2;
1271        let last_visible_line =
1272            (first_visible_line + visible_lines_count).min(visual_lines.len());
1273
1274        let (start_idx, end_idx) =
1275            if self.cache_window_end_line > self.cache_window_start_line {
1276                let s = self.cache_window_start_line.min(visual_lines.len());
1277                let e = self.cache_window_end_line.min(visual_lines.len());
1278                (s, e)
1279            } else {
1280                (first_visible_line, last_visible_line)
1281            };
1282
1283        // Split rendering into two cached layers:
1284        // - content: expensive, mostly static text/gutter rendering
1285        // - overlay: frequently changing highlights/cursor/IME
1286        //
1287        // This keeps selection dragging and cursor blinking smooth by avoiding
1288        // invalidation of the text layer on every overlay update.
1289        let visual_lines_for_content = visual_lines.clone();
1290        let content_geometry =
1291            self.content_cache.draw(renderer, bounds.size(), |frame| {
1292                // syntect initialization is relatively expensive; keep it global.
1293                let syntax_set =
1294                    SYNTAX_SET.get_or_init(SyntaxSet::load_defaults_newlines);
1295                let theme_set = THEME_SET.get_or_init(ThemeSet::load_defaults);
1296                let syntax_theme = theme_set
1297                    .themes
1298                    .get("base16-ocean.dark")
1299                    .or_else(|| theme_set.themes.values().next());
1300
1301                // Normalize common language aliases/extensions used by consumers.
1302                let syntax_ref = match self.syntax.as_str() {
1303                    "python" => syntax_set.find_syntax_by_extension("py"),
1304                    "rust" => syntax_set.find_syntax_by_extension("rs"),
1305                    "javascript" => syntax_set.find_syntax_by_extension("js"),
1306                    "htm" => syntax_set.find_syntax_by_extension("html"),
1307                    "svg" => syntax_set.find_syntax_by_extension("xml"),
1308                    "markdown" => syntax_set.find_syntax_by_extension("md"),
1309                    "text" => Some(syntax_set.find_syntax_plain_text()),
1310                    _ => syntax_set
1311                        .find_syntax_by_extension(self.syntax.as_str()),
1312                }
1313                .or(Some(syntax_set.find_syntax_plain_text()));
1314
1315                let ctx = RenderContext {
1316                    visual_lines: visual_lines_for_content.as_ref(),
1317                    bounds_width: bounds.width,
1318                    gutter_width: self.gutter_width(),
1319                    line_height: self.line_height,
1320                    font_size: self.font_size,
1321                    full_char_width: self.full_char_width,
1322                    char_width: self.char_width,
1323                    font: self.font,
1324                };
1325
1326                for (idx, visual_line) in visual_lines_for_content
1327                    .iter()
1328                    .enumerate()
1329                    .skip(start_idx)
1330                    .take(end_idx.saturating_sub(start_idx))
1331                {
1332                    let y = idx as f32 * self.line_height;
1333
1334                    self.draw_line_numbers(frame, &ctx, visual_line, y);
1335                    self.draw_text_with_syntax_highlighting(
1336                        frame,
1337                        &ctx,
1338                        visual_line,
1339                        y,
1340                        syntax_ref,
1341                        syntax_set,
1342                        syntax_theme,
1343                    );
1344                }
1345            });
1346
1347        let visual_lines_for_overlay = visual_lines;
1348        let overlay_geometry =
1349            self.overlay_cache.draw(renderer, bounds.size(), |frame| {
1350                // The overlay layer shares the same visual lines, but draws only
1351                // elements that change without modifying the buffer content.
1352                let ctx = RenderContext {
1353                    visual_lines: visual_lines_for_overlay.as_ref(),
1354                    bounds_width: bounds.width,
1355                    gutter_width: self.gutter_width(),
1356                    line_height: self.line_height,
1357                    font_size: self.font_size,
1358                    full_char_width: self.full_char_width,
1359                    char_width: self.char_width,
1360                    font: self.font,
1361                };
1362
1363                for (idx, visual_line) in visual_lines_for_overlay
1364                    .iter()
1365                    .enumerate()
1366                    .skip(start_idx)
1367                    .take(end_idx.saturating_sub(start_idx))
1368                {
1369                    let y = idx as f32 * self.line_height;
1370                    self.draw_current_line_highlight(
1371                        frame,
1372                        &ctx,
1373                        visual_line,
1374                        y,
1375                    );
1376                }
1377
1378                self.draw_search_highlights(frame, &ctx, start_idx, end_idx);
1379                self.draw_selection_highlight(frame, &ctx);
1380                self.draw_cursor(frame, &ctx);
1381            });
1382
1383        vec![content_geometry, overlay_geometry]
1384    }
1385
1386    /// Handles Canvas trait events, specifically keyboard input events and focus management for the code editor widget.
1387    ///
1388    /// # Arguments
1389    ///
1390    /// * `_state` - The mutable state of the canvas (unused in this implementation)
1391    /// * `event` - The input event to handle, such as keyboard presses
1392    /// * `bounds` - The rectangle bounds of the canvas widget
1393    /// * `cursor` - The current mouse cursor position and status
1394    ///
1395    /// # Returns
1396    ///
1397    /// An optional `Action<Message>` to perform, such as sending a message or redrawing the canvas
1398    fn update(
1399        &self,
1400        _state: &mut Self::State,
1401        event: &Event,
1402        bounds: Rectangle,
1403        cursor: mouse::Cursor,
1404    ) -> Option<Action<Message>> {
1405        match event {
1406            Event::Keyboard(keyboard::Event::KeyPressed {
1407                key,
1408                modifiers,
1409                text,
1410                ..
1411            }) => self
1412                .handle_keyboard_event(key, modifiers, text, bounds, &cursor),
1413            Event::Mouse(mouse_event) => {
1414                self.handle_mouse_event(mouse_event, bounds, &cursor)
1415            }
1416            Event::InputMethod(ime_event) => {
1417                self.handle_ime_event(ime_event, bounds, &cursor)
1418            }
1419            _ => None,
1420        }
1421    }
1422}
1423
1424/// Validates that the selection indices fall on valid UTF-8 character boundaries
1425/// to prevent panics during string slicing.
1426///
1427/// # Arguments
1428///
1429/// * `content` - The string content to check against
1430/// * `start` - The start byte index
1431/// * `end` - The end byte index
1432///
1433/// # Returns
1434///
1435/// `Some((start, end))` if indices are valid, `None` otherwise.
1436fn validate_selection_indices(
1437    content: &str,
1438    start: usize,
1439    end: usize,
1440) -> Option<(usize, usize)> {
1441    let len = content.len();
1442    // Clamp indices to content length
1443    let start = start.min(len);
1444    let end = end.min(len);
1445
1446    // Ensure start is not greater than end
1447    if start > end {
1448        return None;
1449    }
1450
1451    // Verify that indices fall on valid UTF-8 character boundaries
1452    if content.is_char_boundary(start) && content.is_char_boundary(end) {
1453        Some((start, end))
1454    } else {
1455        None
1456    }
1457}
1458
1459#[cfg(test)]
1460mod tests {
1461    use super::*;
1462    use crate::canvas_editor::{CHAR_WIDTH, FONT_SIZE, compare_floats};
1463    use std::cmp::Ordering;
1464
1465    #[test]
1466    fn test_calculate_segment_geometry_ascii() {
1467        // "Hello World"
1468        // "Hello " (6 chars) -> prefix
1469        // "World" (5 chars) -> segment
1470        // width("Hello ") = 6 * CHAR_WIDTH
1471        // width("World") = 5 * CHAR_WIDTH
1472        let content = "Hello World";
1473        let (x, w) = calculate_segment_geometry(
1474            content, 0, 6, 11, 0.0, FONT_SIZE, CHAR_WIDTH,
1475        );
1476
1477        let expected_x = CHAR_WIDTH * 6.0;
1478        let expected_w = CHAR_WIDTH * 5.0;
1479
1480        assert_eq!(
1481            compare_floats(x, expected_x),
1482            Ordering::Equal,
1483            "X position mismatch for ASCII"
1484        );
1485        assert_eq!(
1486            compare_floats(w, expected_w),
1487            Ordering::Equal,
1488            "Width mismatch for ASCII"
1489        );
1490    }
1491
1492    #[test]
1493    fn test_calculate_segment_geometry_cjk() {
1494        // "你好世界"
1495        // "你好" (2 chars) -> prefix
1496        // "世界" (2 chars) -> segment
1497        // width("你好") = 2 * FONT_SIZE
1498        // width("世界") = 2 * FONT_SIZE
1499        let content = "你好世界";
1500        let (x, w) = calculate_segment_geometry(
1501            content, 0, 2, 4, 10.0, FONT_SIZE, CHAR_WIDTH,
1502        );
1503
1504        let expected_x = 10.0 + FONT_SIZE * 2.0;
1505        let expected_w = FONT_SIZE * 2.0;
1506
1507        assert_eq!(
1508            compare_floats(x, expected_x),
1509            Ordering::Equal,
1510            "X position mismatch for CJK"
1511        );
1512        assert_eq!(
1513            compare_floats(w, expected_w),
1514            Ordering::Equal,
1515            "Width mismatch for CJK"
1516        );
1517    }
1518
1519    #[test]
1520    fn test_calculate_segment_geometry_mixed() {
1521        // "Hi你好"
1522        // "Hi" (2 chars) -> prefix
1523        // "你好" (2 chars) -> segment
1524        // width("Hi") = 2 * CHAR_WIDTH
1525        // width("你好") = 2 * FONT_SIZE
1526        let content = "Hi你好";
1527        let (x, w) = calculate_segment_geometry(
1528            content, 0, 2, 4, 0.0, FONT_SIZE, CHAR_WIDTH,
1529        );
1530
1531        let expected_x = CHAR_WIDTH * 2.0;
1532        let expected_w = FONT_SIZE * 2.0;
1533
1534        assert_eq!(
1535            compare_floats(x, expected_x),
1536            Ordering::Equal,
1537            "X position mismatch for mixed content"
1538        );
1539        assert_eq!(
1540            compare_floats(w, expected_w),
1541            Ordering::Equal,
1542            "Width mismatch for mixed content"
1543        );
1544    }
1545
1546    #[test]
1547    fn test_calculate_segment_geometry_empty_range() {
1548        let content = "Hello";
1549        let (x, w) = calculate_segment_geometry(
1550            content, 0, 0, 0, 0.0, FONT_SIZE, CHAR_WIDTH,
1551        );
1552        assert!((x - 0.0).abs() < f32::EPSILON);
1553        assert!((w - 0.0).abs() < f32::EPSILON);
1554    }
1555
1556    #[test]
1557    fn test_calculate_segment_geometry_with_visual_offset() {
1558        // content: "0123456789"
1559        // visual_start_col: 2 (starts at '2')
1560        // segment: "34" (indices 3 to 5)
1561        // prefix: from visual start (2) to segment start (3) -> "2" (length 1)
1562        // prefix width: 1 * CHAR_WIDTH
1563        // segment width: 2 * CHAR_WIDTH
1564        let content = "0123456789";
1565        let (x, w) = calculate_segment_geometry(
1566            content, 2, 3, 5, 5.0, FONT_SIZE, CHAR_WIDTH,
1567        );
1568
1569        let expected_x = 5.0 + CHAR_WIDTH * 1.0;
1570        let expected_w = CHAR_WIDTH * 2.0;
1571
1572        assert_eq!(
1573            compare_floats(x, expected_x),
1574            Ordering::Equal,
1575            "X position mismatch with visual offset"
1576        );
1577        assert_eq!(
1578            compare_floats(w, expected_w),
1579            Ordering::Equal,
1580            "Width mismatch with visual offset"
1581        );
1582    }
1583
1584    #[test]
1585    fn test_calculate_segment_geometry_out_of_bounds() {
1586        // Content length is 5 ("Hello")
1587        // Request start at 10, end at 15
1588        // visual_start 0
1589        // Prefix should consume whole string ("Hello") and stop.
1590        // Segment should be empty.
1591        let content = "Hello";
1592        let (x, w) = calculate_segment_geometry(
1593            content, 0, 10, 15, 0.0, FONT_SIZE, CHAR_WIDTH,
1594        );
1595
1596        let expected_x = CHAR_WIDTH * 5.0; // Width of "Hello"
1597        let expected_w = 0.0;
1598
1599        assert_eq!(
1600            compare_floats(x, expected_x),
1601            Ordering::Equal,
1602            "X position mismatch for out of bounds start"
1603        );
1604        assert!(
1605            (w - expected_w).abs() < f32::EPSILON,
1606            "Width should be 0 for out of bounds segment"
1607        );
1608    }
1609
1610    #[test]
1611    fn test_calculate_segment_geometry_special_chars() {
1612        // Emoji "👋" (width > 1 => FONT_SIZE)
1613        // Tab "\t" (width None => 0.0)
1614        let content = "A👋\tB";
1615        // Measure "👋" (index 1 to 2)
1616        // Indices in chars: 'A' (0), '👋' (1), '\t' (2), 'B' (3)
1617
1618        // Segment covering Emoji
1619        let (x, w) = calculate_segment_geometry(
1620            content, 0, 1, 2, 0.0, FONT_SIZE, CHAR_WIDTH,
1621        );
1622        let expected_x_emoji = CHAR_WIDTH; // 'A'
1623        let expected_w_emoji = FONT_SIZE; // '👋'
1624
1625        assert_eq!(
1626            compare_floats(x, expected_x_emoji),
1627            Ordering::Equal,
1628            "X pos for emoji"
1629        );
1630        assert_eq!(
1631            compare_floats(w, expected_w_emoji),
1632            Ordering::Equal,
1633            "Width for emoji"
1634        );
1635
1636        // Segment covering Tab
1637        let (x_tab, w_tab) = calculate_segment_geometry(
1638            content, 0, 2, 3, 0.0, FONT_SIZE, CHAR_WIDTH,
1639        );
1640        let expected_x_tab = CHAR_WIDTH + FONT_SIZE; // 'A' + '👋'
1641        let expected_w_tab = 0.0; // Tab width is 0 in this implementation
1642
1643        assert_eq!(
1644            compare_floats(x_tab, expected_x_tab),
1645            Ordering::Equal,
1646            "X pos for tab"
1647        );
1648        assert_eq!(
1649            compare_floats(w_tab, expected_w_tab),
1650            Ordering::Equal,
1651            "Width for tab"
1652        );
1653    }
1654
1655    #[test]
1656    fn test_calculate_segment_geometry_inverted_range() {
1657        // Start 5, End 3
1658        // Should result in empty segment at start 5
1659        let content = "0123456789";
1660        let (x, w) = calculate_segment_geometry(
1661            content, 0, 5, 3, 0.0, FONT_SIZE, CHAR_WIDTH,
1662        );
1663
1664        let expected_x = CHAR_WIDTH * 5.0;
1665        let expected_w = 0.0;
1666
1667        assert_eq!(
1668            compare_floats(x, expected_x),
1669            Ordering::Equal,
1670            "X pos for inverted range"
1671        );
1672        assert!(
1673            (w - expected_w).abs() < f32::EPSILON,
1674            "Width for inverted range"
1675        );
1676    }
1677
1678    #[test]
1679    fn test_validate_selection_indices() {
1680        // Test valid ASCII indices
1681        let content = "Hello";
1682        assert_eq!(validate_selection_indices(content, 0, 5), Some((0, 5)));
1683        assert_eq!(validate_selection_indices(content, 1, 3), Some((1, 3)));
1684
1685        // Test valid multi-byte indices (Chinese "你好")
1686        // "你" is 3 bytes (0-3), "好" is 3 bytes (3-6)
1687        let content = "你好";
1688        assert_eq!(validate_selection_indices(content, 0, 6), Some((0, 6)));
1689        assert_eq!(validate_selection_indices(content, 0, 3), Some((0, 3)));
1690        assert_eq!(validate_selection_indices(content, 3, 6), Some((3, 6)));
1691
1692        // Test invalid indices (splitting multi-byte char)
1693        assert_eq!(validate_selection_indices(content, 1, 3), None); // Split first char
1694        assert_eq!(validate_selection_indices(content, 0, 4), None); // Split second char
1695
1696        // Test out of bounds (should be clamped if on boundary, but here len is 6)
1697        // If we pass start=0, end=100 -> clamped to 0, 6. 6 is boundary.
1698        assert_eq!(validate_selection_indices(content, 0, 100), Some((0, 6)));
1699
1700        // Test inverted range
1701        assert_eq!(validate_selection_indices(content, 3, 0), None);
1702    }
1703}