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::widget::operation::{RelativeOffset, snap_to};
7use iced::widget::{Id, canvas};
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::time::Instant;
10
11use crate::i18n::Translations;
12use crate::text_buffer::TextBuffer;
13use crate::theme::Style;
14pub use history::CommandHistory;
15
16/// Global counter for generating unique editor IDs (starts at 1)
17static EDITOR_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
18
19/// ID of the currently focused editor (0 = no editor focused)
20static FOCUSED_EDITOR_ID: AtomicU64 = AtomicU64::new(0);
21
22// Re-export submodules
23mod canvas_impl;
24mod clipboard;
25pub mod command;
26mod cursor;
27pub mod history;
28mod search;
29mod search_dialog;
30mod selection;
31mod update;
32mod view;
33mod wrapping;
34
35/// Canvas-based text editor constants
36pub(crate) const FONT_SIZE: f32 = 14.0;
37pub(crate) const LINE_HEIGHT: f32 = 20.0;
38pub(crate) const CHAR_WIDTH: f32 = 8.4; // Monospace character width
39pub(crate) const GUTTER_WIDTH: f32 = 45.0;
40pub(crate) const CURSOR_BLINK_INTERVAL: std::time::Duration =
41    std::time::Duration::from_millis(530);
42
43/// Canvas-based high-performance text editor.
44pub struct CodeEditor {
45    /// Unique ID for this editor instance (for focus management)
46    pub(crate) editor_id: u64,
47    /// Text buffer
48    pub(crate) buffer: TextBuffer,
49    /// Cursor position (line, column)
50    pub(crate) cursor: (usize, usize),
51    /// Scroll offset in pixels
52    pub(crate) scroll_offset: f32,
53    /// Editor theme style
54    pub(crate) style: Style,
55    /// Syntax highlighting language
56    pub(crate) syntax: String,
57    /// Last cursor blink time
58    pub(crate) last_blink: Instant,
59    /// Cursor visible state
60    pub(crate) cursor_visible: bool,
61    /// Selection start (if any)
62    pub(crate) selection_start: Option<(usize, usize)>,
63    /// Selection end (if any) - cursor position during selection
64    pub(crate) selection_end: Option<(usize, usize)>,
65    /// Mouse is currently dragging for selection
66    pub(crate) is_dragging: bool,
67    /// Cache for canvas rendering
68    pub(crate) cache: canvas::Cache,
69    /// Scrollable ID for programmatic scrolling
70    pub(crate) scrollable_id: Id,
71    /// Current viewport scroll position (Y offset)
72    pub(crate) viewport_scroll: f32,
73    /// Viewport height (visible area)
74    pub(crate) viewport_height: f32,
75    /// Viewport width (visible area)
76    pub(crate) viewport_width: f32,
77    /// Command history for undo/redo
78    pub(crate) history: CommandHistory,
79    /// Whether we're currently grouping commands (for smart undo)
80    pub(crate) is_grouping: bool,
81    /// Line wrapping enabled
82    pub(crate) wrap_enabled: bool,
83    /// Wrap column (None = wrap at viewport width)
84    pub(crate) wrap_column: Option<usize>,
85    /// Search state
86    pub(crate) search_state: search::SearchState,
87    /// Translations for UI text
88    pub(crate) translations: Translations,
89    /// Whether search/replace functionality is enabled
90    pub(crate) search_replace_enabled: bool,
91    /// Whether line numbers are displayed
92    pub(crate) line_numbers_enabled: bool,
93    /// Whether the canvas has user input focus (for keyboard events)
94    pub(crate) has_canvas_focus: bool,
95    /// Whether to show the cursor (for rendering)
96    pub(crate) show_cursor: bool,
97}
98
99/// Messages emitted by the code editor
100#[derive(Debug, Clone)]
101pub enum Message {
102    /// Character typed
103    CharacterInput(char),
104    /// Backspace pressed
105    Backspace,
106    /// Delete pressed
107    Delete,
108    /// Enter pressed
109    Enter,
110    /// Tab pressed (inserts 4 spaces)
111    Tab,
112    /// Arrow key pressed (direction, shift_pressed)
113    ArrowKey(ArrowDirection, bool),
114    /// Mouse clicked at position
115    MouseClick(iced::Point),
116    /// Mouse drag for selection
117    MouseDrag(iced::Point),
118    /// Mouse released
119    MouseRelease,
120    /// Copy selected text (Ctrl+C)
121    Copy,
122    /// Paste text from clipboard (Ctrl+V)
123    Paste(String),
124    /// Delete selected text (Shift+Delete)
125    DeleteSelection,
126    /// Request redraw for cursor blink
127    Tick,
128    /// Page Up pressed
129    PageUp,
130    /// Page Down pressed
131    PageDown,
132    /// Home key pressed (move to start of line, shift_pressed)
133    Home(bool),
134    /// End key pressed (move to end of line, shift_pressed)
135    End(bool),
136    /// Ctrl+Home pressed (move to start of document)
137    CtrlHome,
138    /// Ctrl+End pressed (move to end of document)
139    CtrlEnd,
140    /// Viewport scrolled - track scroll position
141    Scrolled(iced::widget::scrollable::Viewport),
142    /// Undo last operation (Ctrl+Z)
143    Undo,
144    /// Redo last undone operation (Ctrl+Y)
145    Redo,
146    /// Open search dialog (Ctrl+F)
147    OpenSearch,
148    /// Open search and replace dialog (Ctrl+H)
149    OpenSearchReplace,
150    /// Close search dialog (Escape)
151    CloseSearch,
152    /// Search query text changed
153    SearchQueryChanged(String),
154    /// Replace text changed
155    ReplaceQueryChanged(String),
156    /// Toggle case sensitivity
157    ToggleCaseSensitive,
158    /// Find next match (F3)
159    FindNext,
160    /// Find previous match (Shift+F3)
161    FindPrevious,
162    /// Replace current match
163    ReplaceNext,
164    /// Replace all matches
165    ReplaceAll,
166    /// Tab pressed in search dialog (cycle forward)
167    SearchDialogTab,
168    /// Shift+Tab pressed in search dialog (cycle backward)
169    SearchDialogShiftTab,
170    /// Canvas gained focus (mouse click)
171    CanvasFocusGained,
172    /// Canvas lost focus (external widget interaction)
173    CanvasFocusLost,
174}
175
176/// Arrow key directions
177#[derive(Debug, Clone, Copy)]
178pub enum ArrowDirection {
179    Up,
180    Down,
181    Left,
182    Right,
183}
184
185impl CodeEditor {
186    /// Creates a new canvas-based text editor.
187    ///
188    /// # Arguments
189    ///
190    /// * `content` - Initial text content
191    /// * `syntax` - Syntax highlighting language (e.g., "py", "lua", "rs")
192    ///
193    /// # Returns
194    ///
195    /// A new `CodeEditor` instance
196    pub fn new(content: &str, syntax: &str) -> Self {
197        // Generate a unique ID for this editor instance
198        let editor_id = EDITOR_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
199
200        // Give focus to the first editor created (ID == 1)
201        if editor_id == 1 {
202            FOCUSED_EDITOR_ID.store(editor_id, Ordering::Relaxed);
203        }
204
205        Self {
206            editor_id,
207            buffer: TextBuffer::new(content),
208            cursor: (0, 0),
209            scroll_offset: 0.0,
210            style: crate::theme::from_iced_theme(&iced::Theme::TokyoNightStorm),
211            syntax: syntax.to_string(),
212            last_blink: Instant::now(),
213            cursor_visible: true,
214            selection_start: None,
215            selection_end: None,
216            is_dragging: false,
217            cache: canvas::Cache::default(),
218            scrollable_id: Id::unique(),
219            viewport_scroll: 0.0,
220            viewport_height: 600.0, // Default, will be updated
221            viewport_width: 800.0,  // Default, will be updated
222            history: CommandHistory::new(100),
223            is_grouping: false,
224            wrap_enabled: true,
225            wrap_column: None,
226            search_state: search::SearchState::new(),
227            translations: Translations::default(),
228            search_replace_enabled: true,
229            line_numbers_enabled: true,
230            has_canvas_focus: false,
231            show_cursor: false,
232        }
233    }
234
235    /// Returns the current text content as a string.
236    ///
237    /// # Returns
238    ///
239    /// The complete text content of the editor
240    pub fn content(&self) -> String {
241        self.buffer.to_string()
242    }
243
244    /// Sets the viewport height for the editor.
245    ///
246    /// This determines the minimum height of the canvas, ensuring proper
247    /// background rendering even when content is smaller than the viewport.
248    ///
249    /// # Arguments
250    ///
251    /// * `height` - The viewport height in pixels
252    ///
253    /// # Returns
254    ///
255    /// Self for method chaining
256    ///
257    /// # Example
258    ///
259    /// ```
260    /// use iced_code_editor::CodeEditor;
261    ///
262    /// let editor = CodeEditor::new("fn main() {}", "rs")
263    ///     .with_viewport_height(500.0);
264    /// ```
265    #[must_use]
266    pub fn with_viewport_height(mut self, height: f32) -> Self {
267        self.viewport_height = height;
268        self
269    }
270
271    /// Sets the theme style for the editor.
272    ///
273    /// # Arguments
274    ///
275    /// * `style` - The style to apply to the editor
276    ///
277    /// # Example
278    ///
279    /// ```
280    /// use iced_code_editor::{CodeEditor, theme};
281    ///
282    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
283    /// editor.set_theme(theme::from_iced_theme(&iced::Theme::TokyoNightStorm));
284    /// ```
285    pub fn set_theme(&mut self, style: Style) {
286        self.style = style;
287        self.cache.clear(); // Force redraw with new theme
288    }
289
290    /// Sets the language for UI translations.
291    ///
292    /// This changes the language used for all UI text elements in the editor,
293    /// including search dialog tooltips, placeholders, and labels.
294    ///
295    /// # Arguments
296    ///
297    /// * `language` - The language to use for UI text
298    ///
299    /// # Example
300    ///
301    /// ```
302    /// use iced_code_editor::{CodeEditor, Language};
303    ///
304    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
305    /// editor.set_language(Language::French);
306    /// ```
307    pub fn set_language(&mut self, language: crate::i18n::Language) {
308        self.translations.set_language(language);
309        self.cache.clear(); // Force UI redraw
310    }
311
312    /// Returns the current UI language.
313    ///
314    /// # Returns
315    ///
316    /// The currently active language for UI text
317    ///
318    /// # Example
319    ///
320    /// ```
321    /// use iced_code_editor::{CodeEditor, Language};
322    ///
323    /// let editor = CodeEditor::new("fn main() {}", "rs");
324    /// let current_lang = editor.language();
325    /// ```
326    pub fn language(&self) -> crate::i18n::Language {
327        self.translations.language()
328    }
329
330    /// Requests focus for this editor.
331    ///
332    /// This method programmatically sets the focus to this editor instance,
333    /// allowing it to receive keyboard events. Other editors will automatically
334    /// lose focus.
335    ///
336    /// # Example
337    ///
338    /// ```
339    /// use iced_code_editor::CodeEditor;
340    ///
341    /// let mut editor1 = CodeEditor::new("fn main() {}", "rs");
342    /// let mut editor2 = CodeEditor::new("fn test() {}", "rs");
343    ///
344    /// // Give focus to editor2
345    /// editor2.request_focus();
346    /// ```
347    pub fn request_focus(&self) {
348        FOCUSED_EDITOR_ID.store(self.editor_id, Ordering::Relaxed);
349    }
350
351    /// Checks if this editor currently has focus.
352    ///
353    /// Returns `true` if this editor will receive keyboard events,
354    /// `false` otherwise.
355    ///
356    /// # Returns
357    ///
358    /// `true` if focused, `false` otherwise
359    ///
360    /// # Example
361    ///
362    /// ```
363    /// use iced_code_editor::CodeEditor;
364    ///
365    /// let editor = CodeEditor::new("fn main() {}", "rs");
366    /// if editor.is_focused() {
367    ///     println!("Editor has focus");
368    /// }
369    /// ```
370    pub fn is_focused(&self) -> bool {
371        FOCUSED_EDITOR_ID.load(Ordering::Relaxed) == self.editor_id
372    }
373
374    /// Resets the editor with new content.
375    ///
376    /// This method replaces the buffer content and resets all editor state
377    /// (cursor position, selection, scroll, history) to initial values.
378    /// Use this instead of creating a new `CodeEditor` instance to ensure
379    /// proper widget tree updates in iced.
380    ///
381    /// Returns a `Task` that scrolls the editor to the top, which also
382    /// forces a redraw of the canvas.
383    ///
384    /// # Arguments
385    ///
386    /// * `content` - The new text content
387    ///
388    /// # Returns
389    ///
390    /// A `Task<Message>` that should be returned from your update function
391    ///
392    /// # Example
393    ///
394    /// ```ignore
395    /// use iced_code_editor::CodeEditor;
396    ///
397    /// let mut editor = CodeEditor::new("initial content", "lua");
398    /// // Later, reset with new content and get the task
399    /// let task = editor.reset("new content");
400    /// // Return task.map(YourMessage::Editor) from your update function
401    /// ```
402    pub fn reset(&mut self, content: &str) -> iced::Task<Message> {
403        self.buffer = TextBuffer::new(content);
404        self.cursor = (0, 0);
405        self.scroll_offset = 0.0;
406        self.selection_start = None;
407        self.selection_end = None;
408        self.is_dragging = false;
409        self.viewport_scroll = 0.0;
410        self.history = CommandHistory::new(100);
411        self.is_grouping = false;
412        self.last_blink = Instant::now();
413        self.cursor_visible = true;
414        // Create a new cache to ensure complete redraw (clear() is not sufficient
415        // when new content is smaller than previous content)
416        self.cache = canvas::Cache::default();
417
418        // Scroll to top to force a redraw
419        snap_to(self.scrollable_id.clone(), RelativeOffset::START)
420    }
421
422    /// Resets the cursor blink animation.
423    pub(crate) fn reset_cursor_blink(&mut self) {
424        self.last_blink = Instant::now();
425        self.cursor_visible = true;
426    }
427
428    /// Refreshes search matches after buffer modification.
429    ///
430    /// Should be called after any operation that modifies the buffer.
431    /// If search is active, recalculates matches and selects the one
432    /// closest to the current cursor position.
433    pub(crate) fn refresh_search_matches_if_needed(&mut self) {
434        if self.search_state.is_open && !self.search_state.query.is_empty() {
435            // Recalculate matches with current query
436            self.search_state.update_matches(&self.buffer);
437
438            // Select match closest to cursor to maintain context
439            self.search_state.select_match_near_cursor(self.cursor);
440        }
441    }
442
443    /// Returns whether the editor has unsaved changes.
444    ///
445    /// # Returns
446    ///
447    /// `true` if there are unsaved modifications, `false` otherwise
448    pub fn is_modified(&self) -> bool {
449        self.history.is_modified()
450    }
451
452    /// Marks the current state as saved.
453    ///
454    /// Call this after successfully saving the file to reset the modified state.
455    pub fn mark_saved(&mut self) {
456        self.history.mark_saved();
457    }
458
459    /// Returns whether undo is available.
460    pub fn can_undo(&self) -> bool {
461        self.history.can_undo()
462    }
463
464    /// Returns whether redo is available.
465    pub fn can_redo(&self) -> bool {
466        self.history.can_redo()
467    }
468
469    /// Sets whether line wrapping is enabled.
470    ///
471    /// When enabled, long lines will wrap at the viewport width or at a
472    /// configured column width.
473    ///
474    /// # Arguments
475    ///
476    /// * `enabled` - Whether to enable line wrapping
477    ///
478    /// # Example
479    ///
480    /// ```
481    /// use iced_code_editor::CodeEditor;
482    ///
483    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
484    /// editor.set_wrap_enabled(false); // Disable wrapping
485    /// ```
486    pub fn set_wrap_enabled(&mut self, enabled: bool) {
487        if self.wrap_enabled != enabled {
488            self.wrap_enabled = enabled;
489            self.cache.clear(); // Force redraw
490        }
491    }
492
493    /// Returns whether line wrapping is enabled.
494    ///
495    /// # Returns
496    ///
497    /// `true` if line wrapping is enabled, `false` otherwise
498    pub fn wrap_enabled(&self) -> bool {
499        self.wrap_enabled
500    }
501
502    /// Enables or disables the search/replace functionality.
503    ///
504    /// When disabled, search/replace keyboard shortcuts (Ctrl+F, Ctrl+H, F3)
505    /// will be ignored. If the search dialog is currently open, it will be closed.
506    ///
507    /// # Arguments
508    ///
509    /// * `enabled` - Whether to enable search/replace functionality
510    ///
511    /// # Example
512    ///
513    /// ```
514    /// use iced_code_editor::CodeEditor;
515    ///
516    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
517    /// editor.set_search_replace_enabled(false); // Disable search/replace
518    /// ```
519    pub fn set_search_replace_enabled(&mut self, enabled: bool) {
520        self.search_replace_enabled = enabled;
521        if !enabled && self.search_state.is_open {
522            self.search_state.close();
523        }
524    }
525
526    /// Returns whether search/replace functionality is enabled.
527    ///
528    /// # Returns
529    ///
530    /// `true` if search/replace is enabled, `false` otherwise
531    pub fn search_replace_enabled(&self) -> bool {
532        self.search_replace_enabled
533    }
534
535    /// Sets the line wrapping with builder pattern.
536    ///
537    /// # Arguments
538    ///
539    /// * `enabled` - Whether to enable line wrapping
540    ///
541    /// # Returns
542    ///
543    /// Self for method chaining
544    ///
545    /// # Example
546    ///
547    /// ```
548    /// use iced_code_editor::CodeEditor;
549    ///
550    /// let editor = CodeEditor::new("fn main() {}", "rs")
551    ///     .with_wrap_enabled(false);
552    /// ```
553    #[must_use]
554    pub fn with_wrap_enabled(mut self, enabled: bool) -> Self {
555        self.wrap_enabled = enabled;
556        self
557    }
558
559    /// Sets the wrap column (fixed width wrapping).
560    ///
561    /// When set to `Some(n)`, lines will wrap at column `n`.
562    /// When set to `None`, lines will wrap at the viewport width.
563    ///
564    /// # Arguments
565    ///
566    /// * `column` - The column to wrap at, or None for viewport-based wrapping
567    ///
568    /// # Example
569    ///
570    /// ```
571    /// use iced_code_editor::CodeEditor;
572    ///
573    /// let editor = CodeEditor::new("fn main() {}", "rs")
574    ///     .with_wrap_column(Some(80)); // Wrap at 80 characters
575    /// ```
576    #[must_use]
577    pub fn with_wrap_column(mut self, column: Option<usize>) -> Self {
578        self.wrap_column = column;
579        self
580    }
581
582    /// Sets whether line numbers are displayed.
583    ///
584    /// When disabled, the gutter is completely removed (0px width),
585    /// providing more space for code display.
586    ///
587    /// # Arguments
588    ///
589    /// * `enabled` - Whether to display line numbers
590    ///
591    /// # Example
592    ///
593    /// ```
594    /// use iced_code_editor::CodeEditor;
595    ///
596    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
597    /// editor.set_line_numbers_enabled(false); // Hide line numbers
598    /// ```
599    pub fn set_line_numbers_enabled(&mut self, enabled: bool) {
600        if self.line_numbers_enabled != enabled {
601            self.line_numbers_enabled = enabled;
602            self.cache.clear(); // Force redraw
603        }
604    }
605
606    /// Returns whether line numbers are displayed.
607    ///
608    /// # Returns
609    ///
610    /// `true` if line numbers are displayed, `false` otherwise
611    pub fn line_numbers_enabled(&self) -> bool {
612        self.line_numbers_enabled
613    }
614
615    /// Sets the line numbers display with builder pattern.
616    ///
617    /// # Arguments
618    ///
619    /// * `enabled` - Whether to display line numbers
620    ///
621    /// # Returns
622    ///
623    /// Self for method chaining
624    ///
625    /// # Example
626    ///
627    /// ```
628    /// use iced_code_editor::CodeEditor;
629    ///
630    /// let editor = CodeEditor::new("fn main() {}", "rs")
631    ///     .with_line_numbers_enabled(false);
632    /// ```
633    #[must_use]
634    pub fn with_line_numbers_enabled(mut self, enabled: bool) -> Self {
635        self.line_numbers_enabled = enabled;
636        self
637    }
638
639    /// Returns the current gutter width based on whether line numbers are enabled.
640    ///
641    /// # Returns
642    ///
643    /// `GUTTER_WIDTH` if line numbers are enabled, `0.0` otherwise
644    pub(crate) fn gutter_width(&self) -> f32 {
645        if self.line_numbers_enabled { GUTTER_WIDTH } else { 0.0 }
646    }
647
648    /// Removes canvas focus from this editor.
649    ///
650    /// This method programmatically removes focus from the canvas, preventing
651    /// it from receiving keyboard events. The cursor will be hidden, but the
652    /// selection will remain visible.
653    ///
654    /// Call this when focus should move to another widget (e.g., text input).
655    ///
656    /// # Example
657    ///
658    /// ```
659    /// use iced_code_editor::CodeEditor;
660    ///
661    /// let mut editor = CodeEditor::new("fn main() {}", "rs");
662    /// editor.lose_focus();
663    /// ```
664    pub fn lose_focus(&mut self) {
665        self.has_canvas_focus = false;
666        self.show_cursor = false;
667    }
668}