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