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}