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}