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