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