Skip to main content

iced_code_editor/canvas_editor/
mod.rs

1//! Canvas-based text editor widget for maximum performance.
2//!
3//! This module provides a custom Canvas widget that handles all text rendering
4//! and input directly, bypassing Iced's higher-level widgets for optimal speed.
5
6use iced::advanced::text::{
7    Alignment, Paragraph, Renderer as TextRenderer, Text,
8};
9use iced::widget::operation::{RelativeOffset, snap_to};
10use iced::widget::{Id, canvas};
11use std::cell::RefCell;
12use std::cmp::Ordering as CmpOrdering;
13use std::ops::Range;
14use std::rc::Rc;
15use std::sync::atomic::{AtomicU64, Ordering};
16#[cfg(not(target_arch = "wasm32"))]
17use std::time::Instant;
18use unicode_width::UnicodeWidthChar;
19
20use crate::i18n::Translations;
21use crate::text_buffer::TextBuffer;
22use crate::theme::Style;
23pub use history::CommandHistory;
24
25#[cfg(target_arch = "wasm32")]
26use web_time::Instant;
27
28/// Global counter for generating unique editor IDs (starts at 1)
29static EDITOR_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
30
31/// ID of the currently focused editor (0 = no editor focused)
32static FOCUSED_EDITOR_ID: AtomicU64 = AtomicU64::new(0);
33
34// Re-export submodules
35mod canvas_impl;
36mod clipboard;
37pub mod command;
38mod cursor;
39pub mod history;
40pub mod ime_requester;
41mod search;
42mod search_dialog;
43mod selection;
44mod update;
45mod view;
46mod wrapping;
47
48/// Canvas-based text editor constants
49pub(crate) const FONT_SIZE: f32 = 14.0;
50pub(crate) const LINE_HEIGHT: f32 = 20.0;
51pub(crate) const CHAR_WIDTH: f32 = 8.4; // Monospace character width
52pub(crate) const GUTTER_WIDTH: f32 = 45.0;
53pub(crate) const CURSOR_BLINK_INTERVAL: std::time::Duration =
54    std::time::Duration::from_millis(530);
55
56/// Measures the width of a single character.
57///
58/// # Arguments
59///
60/// * `c` - The character to measure
61/// * `full_char_width` - The width of a full-width character
62/// * `char_width` - The width of the character
63///
64/// # Returns
65///
66/// The calculated width of the character as a `f32`
67pub(crate) fn measure_char_width(
68    c: char,
69    full_char_width: f32,
70    char_width: f32,
71) -> f32 {
72    match c.width() {
73        Some(w) if w > 1 => full_char_width,
74        Some(_) => char_width,
75        None => 0.0,
76    }
77}
78
79/// Measures rendered text width, accounting for CJK wide characters.
80///
81/// - Wide characters (e.g. Chinese) use FONT_SIZE.
82/// - Narrow characters (e.g. Latin) use CHAR_WIDTH.
83/// - Control characters have width 0.
84///
85/// # Arguments
86///
87/// * `text` - The text string to measure
88/// * `full_char_width` - The width of a full-width character
89/// * `char_width` - The width of a regular character
90///
91/// # Returns
92///
93/// The total calculated width of the text as a `f32`
94pub(crate) fn measure_text_width(
95    text: &str,
96    full_char_width: f32,
97    char_width: f32,
98) -> f32 {
99    text.chars()
100        .map(|c| measure_char_width(c, full_char_width, char_width))
101        .sum()
102}
103
104/// Epsilon value for floating-point comparisons in text layout.
105pub(crate) const EPSILON: f32 = 0.001;
106/// Multiplier used to extend the cached render window beyond the visible range.
107/// The cache window margin is computed as:
108///     margin = visible_lines_count * CACHE_WINDOW_MARGIN_MULTIPLIER
109/// A larger margin reduces how often we clear and rebuild the canvas cache when
110/// scrolling, improving performance on very large files while still ensuring
111/// correct initial rendering during the first scroll.
112pub(crate) const CACHE_WINDOW_MARGIN_MULTIPLIER: usize = 2;
113
114/// Compares two floating point numbers with a small epsilon tolerance.
115///
116/// # Arguments
117///
118/// * `a` - first float number
119/// * `b` - second float number
120///
121/// # Returns
122///
123/// * `Ordering::Equal` if `abs(a - b) < EPSILON`
124/// * `Ordering::Greater` if `a > b` (and not equal)
125/// * `Ordering::Less` if `a < b` (and not equal)
126pub(crate) fn compare_floats(a: f32, b: f32) -> CmpOrdering {
127    if (a - b).abs() < EPSILON {
128        CmpOrdering::Equal
129    } else if a > b {
130        CmpOrdering::Greater
131    } else {
132        CmpOrdering::Less
133    }
134}
135
136#[derive(Debug, Clone)]
137pub(crate) struct ImePreedit {
138    pub(crate) content: String,
139    pub(crate) selection: Option<Range<usize>>,
140}
141
142/// Canvas-based high-performance text editor.
143pub struct CodeEditor {
144    /// Unique ID for this editor instance (for focus management)
145    pub(crate) editor_id: u64,
146    /// Text buffer
147    pub(crate) buffer: TextBuffer,
148    /// Cursor position (line, column)
149    pub(crate) cursor: (usize, usize),
150    /// Scroll offset in pixels
151    pub(crate) scroll_offset: f32,
152    /// Editor theme style
153    pub(crate) style: Style,
154    /// Syntax highlighting language
155    pub(crate) syntax: String,
156    /// Last cursor blink time
157    pub(crate) last_blink: Instant,
158    /// Cursor visible state
159    pub(crate) cursor_visible: bool,
160    /// Selection start (if any)
161    pub(crate) selection_start: Option<(usize, usize)>,
162    /// Selection end (if any) - cursor position during selection
163    pub(crate) selection_end: Option<(usize, usize)>,
164    /// Mouse is currently dragging for selection
165    pub(crate) is_dragging: bool,
166    /// Cached geometry for the "content" layer.
167    ///
168    /// This layer includes expensive-to-build, mostly static visuals such as:
169    /// - syntax-highlighted text glyphs
170    /// - line numbers / gutter text
171    ///
172    /// It is intentionally kept stable across selection/cursor movement so
173    /// that mouse-drag selection feels smooth.
174    pub(crate) content_cache: canvas::Cache,
175    /// Cached geometry for the "overlay" layer.
176    ///
177    /// This layer includes visuals that change frequently without modifying the
178    /// underlying buffer, such as:
179    /// - cursor and current-line highlight
180    /// - selection highlight
181    /// - search match highlights
182    /// - IME preedit decorations
183    ///
184    /// Keeping overlays in a separate cache avoids invalidating the content
185    /// layer on every cursor blink or selection drag.
186    pub(crate) overlay_cache: canvas::Cache,
187    /// Scrollable ID for programmatic scrolling
188    pub(crate) scrollable_id: Id,
189    /// Current viewport scroll position (Y offset)
190    pub(crate) viewport_scroll: f32,
191    /// Viewport height (visible area)
192    pub(crate) viewport_height: f32,
193    /// Viewport width (visible area)
194    pub(crate) viewport_width: f32,
195    /// Command history for undo/redo
196    pub(crate) history: CommandHistory,
197    /// Whether we're currently grouping commands (for smart undo)
198    pub(crate) is_grouping: bool,
199    /// Line wrapping enabled
200    pub(crate) wrap_enabled: bool,
201    /// Wrap column (None = wrap at viewport width)
202    pub(crate) wrap_column: Option<usize>,
203    /// Search state
204    pub(crate) search_state: search::SearchState,
205    /// Translations for UI text
206    pub(crate) translations: Translations,
207    /// Whether search/replace functionality is enabled
208    pub(crate) search_replace_enabled: bool,
209    /// Whether line numbers are displayed
210    pub(crate) line_numbers_enabled: bool,
211    /// Whether the canvas has user input focus (for keyboard events)
212    pub(crate) has_canvas_focus: bool,
213    /// Whether input processing is locked to prevent focus stealing
214    pub(crate) focus_locked: bool,
215    /// Whether to show the cursor (for rendering)
216    pub(crate) show_cursor: bool,
217    /// The font used for rendering text
218    pub(crate) font: iced::Font,
219    /// IME pre-edit state (for CJK input)
220    pub(crate) ime_preedit: Option<ImePreedit>,
221    /// Font size in pixels
222    pub(crate) font_size: f32,
223    /// Full character width (wide chars like CJK) in pixels
224    pub(crate) full_char_width: f32,
225    /// Line height in pixels
226    pub(crate) line_height: f32,
227    /// Character width in pixels
228    pub(crate) char_width: f32,
229    /// Cached render window: the first visual line index included in the cache.
230    /// We keep a larger window than the currently visible range to avoid clearing
231    /// the canvas cache on every small scroll. Only when scrolling crosses the
232    /// window boundary do we re-window and clear the cache.
233    pub(crate) last_first_visible_line: usize,
234    /// Cached render window start line (inclusive)
235    pub(crate) cache_window_start_line: usize,
236    /// Cached render window end line (exclusive)
237    pub(crate) cache_window_end_line: usize,
238    /// Monotonic revision counter for buffer content.
239    ///
240    /// Any operation that changes the buffer must bump this counter to
241    /// invalidate derived layout caches (e.g. wrapping / visual lines). The
242    /// exact value is not semantically meaningful, so `wrapping_add` is used to
243    /// avoid overflow panics while still producing a different key.
244    pub(crate) buffer_revision: u64,
245    /// Cached result of line wrapping ("visual lines") for the current layout key.
246    ///
247    /// This is stored behind a `RefCell` because wrapping is needed during
248    /// rendering (where we only have `&self`), but we still want to memoize the
249    /// expensive computation without forcing external mutability.
250    visual_lines_cache: RefCell<Option<VisualLinesCache>>,
251}
252
253#[derive(Clone, Copy, PartialEq, Eq)]
254struct VisualLinesKey {
255    buffer_revision: u64,
256    /// `f32::to_bits()` is used so the cache key is stable and exact:
257    /// - no epsilon comparisons are required
258    /// - NaN payloads (if any) do not collapse unexpectedly
259    viewport_width_bits: u32,
260    gutter_width_bits: u32,
261    wrap_enabled: bool,
262    wrap_column: Option<usize>,
263    full_char_width_bits: u32,
264    char_width_bits: u32,
265}
266
267struct VisualLinesCache {
268    key: VisualLinesKey,
269    visual_lines: Rc<Vec<wrapping::VisualLine>>,
270}
271
272/// Messages emitted by the code editor
273#[derive(Debug, Clone)]
274pub enum Message {
275    /// Character typed
276    CharacterInput(char),
277    /// Backspace pressed
278    Backspace,
279    /// Delete pressed
280    Delete,
281    /// Enter pressed
282    Enter,
283    /// Tab pressed (inserts 4 spaces)
284    Tab,
285    /// Arrow key pressed (direction, shift_pressed)
286    ArrowKey(ArrowDirection, bool),
287    /// Mouse clicked at position
288    MouseClick(iced::Point),
289    /// Mouse drag for selection
290    MouseDrag(iced::Point),
291    /// Mouse released
292    MouseRelease,
293    /// Copy selected text (Ctrl+C)
294    Copy,
295    /// Paste text from clipboard (Ctrl+V)
296    Paste(String),
297    /// Delete selected text (Shift+Delete)
298    DeleteSelection,
299    /// Request redraw for cursor blink
300    Tick,
301    /// Page Up pressed
302    PageUp,
303    /// Page Down pressed
304    PageDown,
305    /// Home key pressed (move to start of line, shift_pressed)
306    Home(bool),
307    /// End key pressed (move to end of line, shift_pressed)
308    End(bool),
309    /// Ctrl+Home pressed (move to start of document)
310    CtrlHome,
311    /// Ctrl+End pressed (move to end of document)
312    CtrlEnd,
313    /// Viewport scrolled - track scroll position
314    Scrolled(iced::widget::scrollable::Viewport),
315    /// Undo last operation (Ctrl+Z)
316    Undo,
317    /// Redo last undone operation (Ctrl+Y)
318    Redo,
319    /// Open search dialog (Ctrl+F)
320    OpenSearch,
321    /// Open search and replace dialog (Ctrl+H)
322    OpenSearchReplace,
323    /// Close search dialog (Escape)
324    CloseSearch,
325    /// Search query text changed
326    SearchQueryChanged(String),
327    /// Replace text changed
328    ReplaceQueryChanged(String),
329    /// Toggle case sensitivity
330    ToggleCaseSensitive,
331    /// Find next match (F3)
332    FindNext,
333    /// Find previous match (Shift+F3)
334    FindPrevious,
335    /// Replace current match
336    ReplaceNext,
337    /// Replace all matches
338    ReplaceAll,
339    /// Tab pressed in search dialog (cycle forward)
340    SearchDialogTab,
341    /// Shift+Tab pressed in search dialog (cycle backward)
342    SearchDialogShiftTab,
343    /// Tab pressed for focus navigation (when search dialog is not open)
344    FocusNavigationTab,
345    /// Shift+Tab pressed for focus navigation (when search dialog is not open)
346    FocusNavigationShiftTab,
347    /// Canvas gained focus (mouse click)
348    CanvasFocusGained,
349    /// Canvas lost focus (external widget interaction)
350    CanvasFocusLost,
351    /// IME input method opened
352    ImeOpened,
353    /// IME pre-edit update (content, selection range)
354    ImePreedit(String, Option<Range<usize>>),
355    /// IME commit text
356    ImeCommit(String),
357    /// IME input method closed
358    ImeClosed,
359}
360
361/// Arrow key directions
362#[derive(Debug, Clone, Copy)]
363pub enum ArrowDirection {
364    Up,
365    Down,
366    Left,
367    Right,
368}
369
370impl CodeEditor {
371    /// Creates a new canvas-based text editor.
372    ///
373    /// # Arguments
374    ///
375    /// * `content` - Initial text content
376    /// * `syntax` - Syntax highlighting language (e.g., "py", "lua", "rs")
377    ///
378    /// # Returns
379    ///
380    /// A new `CodeEditor` instance
381    pub fn new(content: &str, syntax: &str) -> Self {
382        // Generate a unique ID for this editor instance
383        let editor_id = EDITOR_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
384
385        // Give focus to the first editor created (ID == 1)
386        if editor_id == 1 {
387            FOCUSED_EDITOR_ID.store(editor_id, Ordering::Relaxed);
388        }
389
390        let mut editor = Self {
391            editor_id,
392            buffer: TextBuffer::new(content),
393            cursor: (0, 0),
394            scroll_offset: 0.0,
395            style: crate::theme::from_iced_theme(&iced::Theme::TokyoNightStorm),
396            syntax: syntax.to_string(),
397            last_blink: Instant::now(),
398            cursor_visible: true,
399            selection_start: None,
400            selection_end: None,
401            is_dragging: false,
402            content_cache: canvas::Cache::default(),
403            overlay_cache: canvas::Cache::default(),
404            scrollable_id: Id::unique(),
405            viewport_scroll: 0.0,
406            viewport_height: 600.0, // Default, will be updated
407            viewport_width: 800.0,  // Default, will be updated
408            history: CommandHistory::new(100),
409            is_grouping: false,
410            wrap_enabled: true,
411            wrap_column: None,
412            search_state: search::SearchState::new(),
413            translations: Translations::default(),
414            search_replace_enabled: true,
415            line_numbers_enabled: true,
416            has_canvas_focus: false,
417            focus_locked: false,
418            show_cursor: false,
419            font: iced::Font::MONOSPACE,
420            ime_preedit: None,
421            font_size: FONT_SIZE,
422            full_char_width: CHAR_WIDTH * 2.0,
423            line_height: LINE_HEIGHT,
424            char_width: CHAR_WIDTH,
425            // Initialize render window tracking for virtual scrolling:
426            // these indices define the cached visual line window. The window is
427            // expanded beyond the visible range to amortize redraws and keep scrolling smooth.
428            last_first_visible_line: 0,
429            cache_window_start_line: 0,
430            cache_window_end_line: 0,
431            buffer_revision: 0,
432            visual_lines_cache: RefCell::new(None),
433        };
434
435        // Perform initial character dimension calculation
436        editor.recalculate_char_dimensions(false);
437
438        editor
439    }
440
441    /// Sets the font used by the editor
442    ///
443    /// # Arguments
444    ///
445    /// * `font` - The iced font to set for the editor
446    pub fn set_font(&mut self, font: iced::Font) {
447        self.font = font;
448        self.recalculate_char_dimensions(false);
449    }
450
451    /// Sets the font size and recalculates character dimensions.
452    ///
453    /// If `auto_adjust_line_height` is true, `line_height` will also be scaled to maintain
454    /// the default proportion (Line Height ~ 1.43x).
455    ///
456    /// # Arguments
457    ///
458    /// * `size` - The font size in pixels
459    /// * `auto_adjust_line_height` - Whether to automatically adjust the line height
460    pub fn set_font_size(&mut self, size: f32, auto_adjust_line_height: bool) {
461        self.font_size = size;
462        self.recalculate_char_dimensions(auto_adjust_line_height);
463    }
464
465    /// Recalculates character dimensions based on current font and size.
466    fn recalculate_char_dimensions(&mut self, auto_adjust_line_height: bool) {
467        self.char_width = self.measure_single_char_width("a");
468        // Use '汉' as a standard reference for CJK (Chinese, Japanese, Korean) wide characters
469        self.full_char_width = self.measure_single_char_width("汉");
470
471        // Fallback for infinite width measurements
472        if self.char_width.is_infinite() {
473            self.char_width = self.font_size / 2.0; // Rough estimate for monospace
474        }
475
476        if self.full_char_width.is_infinite() {
477            self.full_char_width = self.font_size;
478        }
479
480        if auto_adjust_line_height {
481            let line_height_ratio = LINE_HEIGHT / FONT_SIZE;
482            self.line_height = self.font_size * line_height_ratio;
483        }
484
485        self.content_cache.clear();
486        self.overlay_cache.clear();
487    }
488
489    /// Measures the width of a single character string using the current font settings.
490    fn measure_single_char_width(&self, content: &str) -> f32 {
491        let text = Text {
492            content,
493            font: self.font,
494            size: iced::Pixels(self.font_size),
495            line_height: iced::advanced::text::LineHeight::default(),
496            bounds: iced::Size::new(f32::INFINITY, f32::INFINITY),
497            align_x: Alignment::Left,
498            align_y: iced::alignment::Vertical::Top,
499            shaping: iced::advanced::text::Shaping::Advanced,
500            wrapping: iced::advanced::text::Wrapping::default(),
501        };
502        let p = <iced::Renderer as TextRenderer>::Paragraph::with_text(text);
503        p.min_width()
504    }
505
506    /// Returns the current font size.
507    ///
508    /// # Returns
509    ///
510    /// The font size in pixels
511    pub fn font_size(&self) -> f32 {
512        self.font_size
513    }
514
515    /// Returns the width of a standard narrow character in pixels.
516    ///
517    /// # Returns
518    ///
519    /// The character width in pixels
520    pub fn char_width(&self) -> f32 {
521        self.char_width
522    }
523
524    /// Returns the width of a wide character (e.g. CJK) in pixels.
525    ///
526    /// # Returns
527    ///
528    /// The full character width in pixels
529    pub fn full_char_width(&self) -> f32 {
530        self.full_char_width
531    }
532
533    /// Sets the line height used by the editor
534    ///
535    /// # Arguments
536    ///
537    /// * `height` - The line height in pixels
538    pub fn set_line_height(&mut self, height: f32) {
539        self.line_height = height;
540        self.content_cache.clear();
541        self.overlay_cache.clear();
542    }
543
544    /// Returns the current line height.
545    ///
546    /// # Returns
547    ///
548    /// The line height in pixels
549    pub fn line_height(&self) -> f32 {
550        self.line_height
551    }
552
553    /// Returns the current text content as a string.
554    ///
555    /// # Returns
556    ///
557    /// The complete text content of the editor
558    pub fn content(&self) -> String {
559        self.buffer.to_string()
560    }
561
562    /// Sets the viewport height for the editor.
563    ///
564    /// This determines the minimum height of the canvas, ensuring proper
565    /// background rendering even when content is smaller than the viewport.
566    ///
567    /// # Arguments
568    ///
569    /// * `height` - The viewport height in pixels
570    ///
571    /// # Returns
572    ///
573    /// Self for method chaining
574    ///
575    /// # Example
576    ///
577    /// ```
578    /// use iced_code_editor::CodeEditor;
579    ///
580    /// let editor = CodeEditor::new("fn main() {}", "rs")
581    ///     .with_viewport_height(500.0);
582    /// ```
583    #[must_use]
584    pub fn with_viewport_height(mut self, height: f32) -> Self {
585        self.viewport_height = height;
586        self
587    }
588
589    /// Sets the theme style for the editor.
590    ///
591    /// # Arguments
592    ///
593    /// * `style` - The style to apply to the editor
594    ///
595    /// # Example
596    ///
597    /// ```
598    /// use iced_code_editor::{CodeEditor, theme};
599    ///
600    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
601    /// editor.set_theme(theme::from_iced_theme(&iced::Theme::TokyoNightStorm));
602    /// ```
603    pub fn set_theme(&mut self, style: Style) {
604        self.style = style;
605        self.content_cache.clear();
606        self.overlay_cache.clear();
607    }
608
609    /// Sets the language for UI translations.
610    ///
611    /// This changes the language used for all UI text elements in the editor,
612    /// including search dialog tooltips, placeholders, and labels.
613    ///
614    /// # Arguments
615    ///
616    /// * `language` - The language to use for UI text
617    ///
618    /// # Example
619    ///
620    /// ```
621    /// use iced_code_editor::{CodeEditor, Language};
622    ///
623    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
624    /// editor.set_language(Language::French);
625    /// ```
626    pub fn set_language(&mut self, language: crate::i18n::Language) {
627        self.translations.set_language(language);
628        self.overlay_cache.clear();
629    }
630
631    /// Returns the current UI language.
632    ///
633    /// # Returns
634    ///
635    /// The currently active language for UI text
636    ///
637    /// # Example
638    ///
639    /// ```
640    /// use iced_code_editor::{CodeEditor, Language};
641    ///
642    /// let editor = CodeEditor::new("fn main() {}", "rs");
643    /// let current_lang = editor.language();
644    /// ```
645    pub fn language(&self) -> crate::i18n::Language {
646        self.translations.language()
647    }
648
649    /// Requests focus for this editor.
650    ///
651    /// This method programmatically sets the focus to this editor instance,
652    /// allowing it to receive keyboard events. Other editors will automatically
653    /// lose focus.
654    ///
655    /// # Example
656    ///
657    /// ```
658    /// use iced_code_editor::CodeEditor;
659    ///
660    /// let mut editor1 = CodeEditor::new("fn main() {}", "rs");
661    /// let mut editor2 = CodeEditor::new("fn test() {}", "rs");
662    ///
663    /// // Give focus to editor2
664    /// editor2.request_focus();
665    /// ```
666    pub fn request_focus(&self) {
667        FOCUSED_EDITOR_ID.store(self.editor_id, Ordering::Relaxed);
668    }
669
670    /// Checks if this editor currently has focus.
671    ///
672    /// Returns `true` if this editor will receive keyboard events,
673    /// `false` otherwise.
674    ///
675    /// # Returns
676    ///
677    /// `true` if focused, `false` otherwise
678    ///
679    /// # Example
680    ///
681    /// ```
682    /// use iced_code_editor::CodeEditor;
683    ///
684    /// let editor = CodeEditor::new("fn main() {}", "rs");
685    /// if editor.is_focused() {
686    ///     println!("Editor has focus");
687    /// }
688    /// ```
689    pub fn is_focused(&self) -> bool {
690        FOCUSED_EDITOR_ID.load(Ordering::Relaxed) == self.editor_id
691    }
692
693    /// Resets the editor with new content.
694    ///
695    /// This method replaces the buffer content and resets all editor state
696    /// (cursor position, selection, scroll, history) to initial values.
697    /// Use this instead of creating a new `CodeEditor` instance to ensure
698    /// proper widget tree updates in iced.
699    ///
700    /// Returns a `Task` that scrolls the editor to the top, which also
701    /// forces a redraw of the canvas.
702    ///
703    /// # Arguments
704    ///
705    /// * `content` - The new text content
706    ///
707    /// # Returns
708    ///
709    /// A `Task<Message>` that should be returned from your update function
710    ///
711    /// # Example
712    ///
713    /// ```ignore
714    /// use iced_code_editor::CodeEditor;
715    ///
716    /// let mut editor = CodeEditor::new("initial content", "lua");
717    /// // Later, reset with new content and get the task
718    /// let task = editor.reset("new content");
719    /// // Return task.map(YourMessage::Editor) from your update function
720    /// ```
721    pub fn reset(&mut self, content: &str) -> iced::Task<Message> {
722        self.buffer = TextBuffer::new(content);
723        self.cursor = (0, 0);
724        self.scroll_offset = 0.0;
725        self.selection_start = None;
726        self.selection_end = None;
727        self.is_dragging = false;
728        self.viewport_scroll = 0.0;
729        self.history = CommandHistory::new(100);
730        self.is_grouping = false;
731        self.last_blink = Instant::now();
732        self.cursor_visible = true;
733        self.content_cache = canvas::Cache::default();
734        self.overlay_cache = canvas::Cache::default();
735        self.buffer_revision = self.buffer_revision.wrapping_add(1);
736        *self.visual_lines_cache.borrow_mut() = None;
737
738        // Scroll to top to force a redraw
739        snap_to(self.scrollable_id.clone(), RelativeOffset::START)
740    }
741
742    /// Resets the cursor blink animation.
743    pub(crate) fn reset_cursor_blink(&mut self) {
744        self.last_blink = Instant::now();
745        self.cursor_visible = true;
746    }
747
748    /// Refreshes search matches after buffer modification.
749    ///
750    /// Should be called after any operation that modifies the buffer.
751    /// If search is active, recalculates matches and selects the one
752    /// closest to the current cursor position.
753    pub(crate) fn refresh_search_matches_if_needed(&mut self) {
754        if self.search_state.is_open && !self.search_state.query.is_empty() {
755            // Recalculate matches with current query
756            self.search_state.update_matches(&self.buffer);
757
758            // Select match closest to cursor to maintain context
759            self.search_state.select_match_near_cursor(self.cursor);
760        }
761    }
762
763    /// Returns whether the editor has unsaved changes.
764    ///
765    /// # Returns
766    ///
767    /// `true` if there are unsaved modifications, `false` otherwise
768    pub fn is_modified(&self) -> bool {
769        self.history.is_modified()
770    }
771
772    /// Marks the current state as saved.
773    ///
774    /// Call this after successfully saving the file to reset the modified state.
775    pub fn mark_saved(&mut self) {
776        self.history.mark_saved();
777    }
778
779    /// Returns whether undo is available.
780    pub fn can_undo(&self) -> bool {
781        self.history.can_undo()
782    }
783
784    /// Returns whether redo is available.
785    pub fn can_redo(&self) -> bool {
786        self.history.can_redo()
787    }
788
789    /// Sets whether line wrapping is enabled.
790    ///
791    /// When enabled, long lines will wrap at the viewport width or at a
792    /// configured column width.
793    ///
794    /// # Arguments
795    ///
796    /// * `enabled` - Whether to enable line wrapping
797    ///
798    /// # Example
799    ///
800    /// ```
801    /// use iced_code_editor::CodeEditor;
802    ///
803    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
804    /// editor.set_wrap_enabled(false); // Disable wrapping
805    /// ```
806    pub fn set_wrap_enabled(&mut self, enabled: bool) {
807        if self.wrap_enabled != enabled {
808            self.wrap_enabled = enabled;
809            self.content_cache.clear();
810            self.overlay_cache.clear();
811        }
812    }
813
814    /// Returns whether line wrapping is enabled.
815    ///
816    /// # Returns
817    ///
818    /// `true` if line wrapping is enabled, `false` otherwise
819    pub fn wrap_enabled(&self) -> bool {
820        self.wrap_enabled
821    }
822
823    /// Enables or disables the search/replace functionality.
824    ///
825    /// When disabled, search/replace keyboard shortcuts (Ctrl+F, Ctrl+H, F3)
826    /// will be ignored. If the search dialog is currently open, it will be closed.
827    ///
828    /// # Arguments
829    ///
830    /// * `enabled` - Whether to enable search/replace functionality
831    ///
832    /// # Example
833    ///
834    /// ```
835    /// use iced_code_editor::CodeEditor;
836    ///
837    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
838    /// editor.set_search_replace_enabled(false); // Disable search/replace
839    /// ```
840    pub fn set_search_replace_enabled(&mut self, enabled: bool) {
841        self.search_replace_enabled = enabled;
842        if !enabled && self.search_state.is_open {
843            self.search_state.close();
844        }
845    }
846
847    /// Returns whether search/replace functionality is enabled.
848    ///
849    /// # Returns
850    ///
851    /// `true` if search/replace is enabled, `false` otherwise
852    pub fn search_replace_enabled(&self) -> bool {
853        self.search_replace_enabled
854    }
855
856    /// Sets the line wrapping with builder pattern.
857    ///
858    /// # Arguments
859    ///
860    /// * `enabled` - Whether to enable line wrapping
861    ///
862    /// # Returns
863    ///
864    /// Self for method chaining
865    ///
866    /// # Example
867    ///
868    /// ```
869    /// use iced_code_editor::CodeEditor;
870    ///
871    /// let editor = CodeEditor::new("fn main() {}", "rs")
872    ///     .with_wrap_enabled(false);
873    /// ```
874    #[must_use]
875    pub fn with_wrap_enabled(mut self, enabled: bool) -> Self {
876        self.wrap_enabled = enabled;
877        self
878    }
879
880    /// Sets the wrap column (fixed width wrapping).
881    ///
882    /// When set to `Some(n)`, lines will wrap at column `n`.
883    /// When set to `None`, lines will wrap at the viewport width.
884    ///
885    /// # Arguments
886    ///
887    /// * `column` - The column to wrap at, or None for viewport-based wrapping
888    ///
889    /// # Example
890    ///
891    /// ```
892    /// use iced_code_editor::CodeEditor;
893    ///
894    /// let editor = CodeEditor::new("fn main() {}", "rs")
895    ///     .with_wrap_column(Some(80)); // Wrap at 80 characters
896    /// ```
897    #[must_use]
898    pub fn with_wrap_column(mut self, column: Option<usize>) -> Self {
899        self.wrap_column = column;
900        self
901    }
902
903    /// Sets whether line numbers are displayed.
904    ///
905    /// When disabled, the gutter is completely removed (0px width),
906    /// providing more space for code display.
907    ///
908    /// # Arguments
909    ///
910    /// * `enabled` - Whether to display line numbers
911    ///
912    /// # Example
913    ///
914    /// ```
915    /// use iced_code_editor::CodeEditor;
916    ///
917    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
918    /// editor.set_line_numbers_enabled(false); // Hide line numbers
919    /// ```
920    pub fn set_line_numbers_enabled(&mut self, enabled: bool) {
921        if self.line_numbers_enabled != enabled {
922            self.line_numbers_enabled = enabled;
923            self.content_cache.clear();
924            self.overlay_cache.clear();
925        }
926    }
927
928    /// Returns whether line numbers are displayed.
929    ///
930    /// # Returns
931    ///
932    /// `true` if line numbers are displayed, `false` otherwise
933    pub fn line_numbers_enabled(&self) -> bool {
934        self.line_numbers_enabled
935    }
936
937    /// Sets the line numbers display with builder pattern.
938    ///
939    /// # Arguments
940    ///
941    /// * `enabled` - Whether to display line numbers
942    ///
943    /// # Returns
944    ///
945    /// Self for method chaining
946    ///
947    /// # Example
948    ///
949    /// ```
950    /// use iced_code_editor::CodeEditor;
951    ///
952    /// let editor = CodeEditor::new("fn main() {}", "rs")
953    ///     .with_line_numbers_enabled(false);
954    /// ```
955    #[must_use]
956    pub fn with_line_numbers_enabled(mut self, enabled: bool) -> Self {
957        self.line_numbers_enabled = enabled;
958        self
959    }
960
961    /// Returns the current gutter width based on whether line numbers are enabled.
962    ///
963    /// # Returns
964    ///
965    /// `GUTTER_WIDTH` if line numbers are enabled, `0.0` otherwise
966    pub(crate) fn gutter_width(&self) -> f32 {
967        if self.line_numbers_enabled { GUTTER_WIDTH } else { 0.0 }
968    }
969
970    /// Removes canvas focus from this editor.
971    ///
972    /// This method programmatically removes focus from the canvas, preventing
973    /// it from receiving keyboard events. The cursor will be hidden, but the
974    /// selection will remain visible.
975    ///
976    /// Call this when focus should move to another widget (e.g., text input).
977    ///
978    /// # Example
979    ///
980    /// ```
981    /// use iced_code_editor::CodeEditor;
982    ///
983    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
984    /// editor.lose_focus();
985    /// ```
986    pub fn lose_focus(&mut self) {
987        self.has_canvas_focus = false;
988        self.show_cursor = false;
989        self.ime_preedit = None;
990    }
991
992    /// Resets the focus lock state.
993    ///
994    /// This method can be called to manually unlock focus processing
995    /// after a focus transition has completed. This is useful when
996    /// you want to allow the editor to process input again after
997    /// programmatic focus changes.
998    ///
999    /// # Example
1000    ///
1001    /// ```
1002    /// use iced_code_editor::CodeEditor;
1003    ///
1004    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
1005    /// editor.reset_focus_lock();
1006    /// ```
1007    pub fn reset_focus_lock(&mut self) {
1008        self.focus_locked = false;
1009    }
1010
1011    /// Returns wrapped "visual lines" for the current buffer and layout, with memoization.
1012    ///
1013    /// The editor frequently needs the wrapped view of the buffer:
1014    /// - hit-testing (mouse selection, cursor placement)
1015    /// - mapping logical ↔ visual positions
1016    /// - rendering (text, line numbers, highlights)
1017    ///
1018    /// Computing visual lines is relatively expensive for large files, so we
1019    /// cache the result keyed by:
1020    /// - `buffer_revision` (buffer content changes)
1021    /// - viewport width / gutter width (layout changes)
1022    /// - wrapping settings (wrap enabled / wrap column)
1023    /// - measured character widths (font / size changes)
1024    ///
1025    /// The returned `Rc<Vec<VisualLine>>` is cheap to clone and allows multiple
1026    /// rendering passes (content + overlay layers) to share the same computed
1027    /// layout without extra allocation.
1028    pub(crate) fn visual_lines_cached(
1029        &self,
1030        viewport_width: f32,
1031    ) -> Rc<Vec<wrapping::VisualLine>> {
1032        let key = VisualLinesKey {
1033            buffer_revision: self.buffer_revision,
1034            viewport_width_bits: viewport_width.to_bits(),
1035            gutter_width_bits: self.gutter_width().to_bits(),
1036            wrap_enabled: self.wrap_enabled,
1037            wrap_column: self.wrap_column,
1038            full_char_width_bits: self.full_char_width.to_bits(),
1039            char_width_bits: self.char_width.to_bits(),
1040        };
1041
1042        let mut cache = self.visual_lines_cache.borrow_mut();
1043        if let Some(existing) = cache.as_ref()
1044            && existing.key == key
1045        {
1046            return existing.visual_lines.clone();
1047        }
1048
1049        let wrapping_calc = wrapping::WrappingCalculator::new(
1050            self.wrap_enabled,
1051            self.wrap_column,
1052            self.full_char_width,
1053            self.char_width,
1054        );
1055        let visual_lines = wrapping_calc.calculate_visual_lines(
1056            &self.buffer,
1057            viewport_width,
1058            self.gutter_width(),
1059        );
1060        let visual_lines = Rc::new(visual_lines);
1061
1062        *cache =
1063            Some(VisualLinesCache { key, visual_lines: visual_lines.clone() });
1064        visual_lines
1065    }
1066}
1067
1068#[cfg(test)]
1069mod tests {
1070    use super::*;
1071
1072    #[test]
1073    fn test_compare_floats() {
1074        // Equal cases
1075        assert_eq!(
1076            compare_floats(1.0, 1.0),
1077            CmpOrdering::Equal,
1078            "Exact equality"
1079        );
1080        assert_eq!(
1081            compare_floats(1.0, 1.0 + 0.0001),
1082            CmpOrdering::Equal,
1083            "Within epsilon (positive)"
1084        );
1085        assert_eq!(
1086            compare_floats(1.0, 1.0 - 0.0001),
1087            CmpOrdering::Equal,
1088            "Within epsilon (negative)"
1089        );
1090
1091        // Greater cases
1092        assert_eq!(
1093            compare_floats(1.0 + 0.002, 1.0),
1094            CmpOrdering::Greater,
1095            "Definitely greater"
1096        );
1097        assert_eq!(
1098            compare_floats(1.0011, 1.0),
1099            CmpOrdering::Greater,
1100            "Just above epsilon"
1101        );
1102
1103        // Less cases
1104        assert_eq!(
1105            compare_floats(1.0, 1.0 + 0.002),
1106            CmpOrdering::Less,
1107            "Definitely less"
1108        );
1109        assert_eq!(
1110            compare_floats(1.0, 1.0011),
1111            CmpOrdering::Less,
1112            "Just below negative epsilon"
1113        );
1114    }
1115
1116    #[test]
1117    fn test_measure_text_width_ascii() {
1118        // "abc" (3 chars) -> 3 * CHAR_WIDTH
1119        let text = "abc";
1120        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
1121        let expected = CHAR_WIDTH * 3.0;
1122        assert_eq!(
1123            compare_floats(width, expected),
1124            CmpOrdering::Equal,
1125            "Width mismatch for ASCII"
1126        );
1127    }
1128
1129    #[test]
1130    fn test_measure_text_width_cjk() {
1131        // "你好" (2 chars) -> 2 * FONT_SIZE
1132        // Chinese characters are typically full-width.
1133        // width = 2 * FONT_SIZE
1134        let text = "你好";
1135        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
1136        let expected = FONT_SIZE * 2.0;
1137        assert_eq!(
1138            compare_floats(width, expected),
1139            CmpOrdering::Equal,
1140            "Width mismatch for CJK"
1141        );
1142    }
1143
1144    #[test]
1145    fn test_measure_text_width_mixed() {
1146        // "Hi" (2 chars) -> 2 * CHAR_WIDTH
1147        // "你好" (2 chars) -> 2 * FONT_SIZE
1148        let text = "Hi你好";
1149        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
1150        let expected = CHAR_WIDTH * 2.0 + FONT_SIZE * 2.0;
1151        assert_eq!(
1152            compare_floats(width, expected),
1153            CmpOrdering::Equal,
1154            "Width mismatch for mixed content"
1155        );
1156    }
1157
1158    #[test]
1159    fn test_measure_text_width_control_chars() {
1160        // "\t\n" (2 chars)
1161        // width = 0.0 (control chars have 0 width in this implementation)
1162        let text = "\t\n";
1163        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
1164        let expected = 0.0;
1165        assert_eq!(
1166            compare_floats(width, expected),
1167            CmpOrdering::Equal,
1168            "Width mismatch for control chars"
1169        );
1170    }
1171
1172    #[test]
1173    fn test_measure_text_width_empty() {
1174        let text = "";
1175        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
1176        assert!(
1177            (width - 0.0).abs() < f32::EPSILON,
1178            "Width should be 0 for empty string"
1179        );
1180    }
1181
1182    #[test]
1183    fn test_measure_text_width_emoji() {
1184        // "👋" (1 char, width > 1) -> FONT_SIZE
1185        let text = "👋";
1186        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
1187        let expected = FONT_SIZE;
1188        assert_eq!(
1189            compare_floats(width, expected),
1190            CmpOrdering::Equal,
1191            "Width mismatch for emoji"
1192        );
1193    }
1194
1195    #[test]
1196    fn test_measure_text_width_korean() {
1197        // "안녕하세요" (5 chars)
1198        // Korean characters are typically full-width.
1199        // width = 5 * FONT_SIZE
1200        let text = "안녕하세요";
1201        let width = measure_text_width(text, FONT_SIZE, CHAR_WIDTH);
1202        let expected = FONT_SIZE * 5.0;
1203        assert_eq!(
1204            compare_floats(width, expected),
1205            CmpOrdering::Equal,
1206            "Width mismatch for Korean"
1207        );
1208    }
1209
1210    #[test]
1211    fn test_measure_text_width_japanese() {
1212        // "こんにちは" (Hiragana, 5 chars) -> 5 * FONT_SIZE
1213        // "カタカナ" (Katakana, 4 chars) -> 4 * FONT_SIZE
1214        // "漢字" (Kanji, 2 chars) -> 2 * FONT_SIZE
1215
1216        let text_hiragana = "こんにちは";
1217        let width_hiragana =
1218            measure_text_width(text_hiragana, FONT_SIZE, CHAR_WIDTH);
1219        let expected_hiragana = FONT_SIZE * 5.0;
1220        assert_eq!(
1221            compare_floats(width_hiragana, expected_hiragana),
1222            CmpOrdering::Equal,
1223            "Width mismatch for Hiragana"
1224        );
1225
1226        let text_katakana = "カタカナ";
1227        let width_katakana =
1228            measure_text_width(text_katakana, FONT_SIZE, CHAR_WIDTH);
1229        let expected_katakana = FONT_SIZE * 4.0;
1230        assert_eq!(
1231            compare_floats(width_katakana, expected_katakana),
1232            CmpOrdering::Equal,
1233            "Width mismatch for Katakana"
1234        );
1235
1236        let text_kanji = "漢字";
1237        let width_kanji = measure_text_width(text_kanji, FONT_SIZE, CHAR_WIDTH);
1238        let expected_kanji = FONT_SIZE * 2.0;
1239        assert_eq!(
1240            compare_floats(width_kanji, expected_kanji),
1241            CmpOrdering::Equal,
1242            "Width mismatch for Kanji"
1243        );
1244    }
1245
1246    #[test]
1247    fn test_set_font_size() {
1248        let mut editor = CodeEditor::new("", "rs");
1249
1250        // Initial state (defaults)
1251        assert!((editor.font_size() - 14.0).abs() < f32::EPSILON);
1252        assert!((editor.line_height() - 20.0).abs() < f32::EPSILON);
1253
1254        // Test auto adjust = true
1255        editor.set_font_size(28.0, true);
1256        assert!((editor.font_size() - 28.0).abs() < f32::EPSILON);
1257        // Line height should double: 20.0 * (28.0/14.0) = 40.0
1258        assert_eq!(
1259            compare_floats(editor.line_height(), 40.0),
1260            CmpOrdering::Equal
1261        );
1262
1263        // Test auto adjust = false
1264        // First set line height to something custom
1265        editor.set_line_height(50.0);
1266        // Change font size but keep line height
1267        editor.set_font_size(14.0, false);
1268        assert!((editor.font_size() - 14.0).abs() < f32::EPSILON);
1269        // Line height should stay 50.0
1270        assert_eq!(
1271            compare_floats(editor.line_height(), 50.0),
1272            CmpOrdering::Equal
1273        );
1274        // Char width should have scaled back to roughly default (but depends on measurement)
1275        // We check if it is close to the expected value, but since measurement can vary,
1276        // we just ensure it is positive and close to what we expect (around 8.4)
1277        assert!(editor.char_width > 0.0);
1278        assert!((editor.char_width - CHAR_WIDTH).abs() < 0.5);
1279    }
1280
1281    #[test]
1282    fn test_measure_single_char_width() {
1283        let editor = CodeEditor::new("", "rs");
1284
1285        // Measure 'a'
1286        let width_a = editor.measure_single_char_width("a");
1287        assert!(width_a > 0.0, "Width of 'a' should be positive");
1288
1289        // Measure Chinese char
1290        let width_cjk = editor.measure_single_char_width("汉");
1291        assert!(width_cjk > 0.0, "Width of '汉' should be positive");
1292
1293        assert!(
1294            width_cjk > width_a,
1295            "Width of '汉' should be greater than 'a'"
1296        );
1297
1298        // Check that width_cjk is roughly double of width_a (common in terminal fonts)
1299        // but we just check it is significantly larger
1300        assert!(width_cjk >= width_a * 1.5);
1301    }
1302
1303    #[test]
1304    fn test_set_line_height() {
1305        let mut editor = CodeEditor::new("", "rs");
1306
1307        // Initial state
1308        assert!((editor.line_height() - LINE_HEIGHT).abs() < f32::EPSILON);
1309
1310        // Set custom line height
1311        editor.set_line_height(35.0);
1312        assert!((editor.line_height() - 35.0).abs() < f32::EPSILON);
1313
1314        // Font size should remain unchanged
1315        assert!((editor.font_size() - FONT_SIZE).abs() < f32::EPSILON);
1316    }
1317
1318    #[test]
1319    fn test_visual_lines_cached_reuses_cache_for_same_key() {
1320        let editor = CodeEditor::new("a\nb\nc", "rs");
1321
1322        let first = editor.visual_lines_cached(800.0);
1323        let second = editor.visual_lines_cached(800.0);
1324
1325        assert!(
1326            Rc::ptr_eq(&first, &second),
1327            "visual_lines_cached should reuse the cached Rc for identical keys"
1328        );
1329    }
1330
1331    #[test]
1332    fn test_visual_lines_cached_changes_on_viewport_width_change() {
1333        let editor = CodeEditor::new("a\nb\nc", "rs");
1334
1335        let first = editor.visual_lines_cached(800.0);
1336        let second = editor.visual_lines_cached(801.0);
1337
1338        assert!(
1339            !Rc::ptr_eq(&first, &second),
1340            "visual_lines_cached should recompute when viewport width changes"
1341        );
1342    }
1343
1344    #[test]
1345    fn test_visual_lines_cached_changes_on_buffer_revision_change() {
1346        let mut editor = CodeEditor::new("a\nb\nc", "rs");
1347
1348        let first = editor.visual_lines_cached(800.0);
1349        editor.buffer_revision = editor.buffer_revision.wrapping_add(1);
1350        let second = editor.visual_lines_cached(800.0);
1351
1352        assert!(
1353            !Rc::ptr_eq(&first, &second),
1354            "visual_lines_cached should recompute when buffer_revision changes"
1355        );
1356    }
1357}