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}