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