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