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::Id;
7use iced::widget::canvas;
8use std::time::Instant;
9
10use crate::text_buffer::TextBuffer;
11use crate::theme::Style;
12pub use history::CommandHistory;
13
14// Re-export submodules
15mod canvas_impl;
16mod clipboard;
17pub mod command;
18mod cursor;
19pub mod history;
20mod selection;
21mod update;
22mod view;
23
24/// Canvas-based text editor constants
25pub(crate) const FONT_SIZE: f32 = 14.0;
26pub(crate) const LINE_HEIGHT: f32 = 20.0;
27pub(crate) const CHAR_WIDTH: f32 = 8.4; // Monospace character width
28pub(crate) const GUTTER_WIDTH: f32 = 60.0;
29pub(crate) const CURSOR_BLINK_INTERVAL: std::time::Duration =
30 std::time::Duration::from_millis(530);
31
32/// Canvas-based high-performance text editor.
33pub struct CodeEditor {
34 /// Text buffer
35 pub(crate) buffer: TextBuffer,
36 /// Cursor position (line, column)
37 pub(crate) cursor: (usize, usize),
38 /// Scroll offset in pixels
39 pub(crate) scroll_offset: f32,
40 /// Editor theme style
41 pub(crate) style: Style,
42 /// Syntax highlighting language
43 pub(crate) syntax: String,
44 /// Last cursor blink time
45 pub(crate) last_blink: Instant,
46 /// Cursor visible state
47 pub(crate) cursor_visible: bool,
48 /// Selection start (if any)
49 pub(crate) selection_start: Option<(usize, usize)>,
50 /// Selection end (if any) - cursor position during selection
51 pub(crate) selection_end: Option<(usize, usize)>,
52 /// Mouse is currently dragging for selection
53 pub(crate) is_dragging: bool,
54 /// Cache for canvas rendering
55 pub(crate) cache: canvas::Cache,
56 /// Scrollable ID for programmatic scrolling
57 pub(crate) scrollable_id: Id,
58 /// Current viewport scroll position (Y offset)
59 pub(crate) viewport_scroll: f32,
60 /// Viewport height (visible area)
61 pub(crate) viewport_height: f32,
62 /// Command history for undo/redo
63 pub(crate) history: CommandHistory,
64 /// Whether we're currently grouping commands (for smart undo)
65 pub(crate) is_grouping: bool,
66}
67
68/// Messages emitted by the code editor
69#[derive(Debug, Clone)]
70pub enum Message {
71 /// Character typed
72 CharacterInput(char),
73 /// Backspace pressed
74 Backspace,
75 /// Delete pressed
76 Delete,
77 /// Enter pressed
78 Enter,
79 /// Tab pressed (inserts 4 spaces)
80 Tab,
81 /// Arrow key pressed (direction, shift_pressed)
82 ArrowKey(ArrowDirection, bool),
83 /// Mouse clicked at position
84 MouseClick(iced::Point),
85 /// Mouse drag for selection
86 MouseDrag(iced::Point),
87 /// Mouse released
88 MouseRelease,
89 /// Copy selected text (Ctrl+C)
90 Copy,
91 /// Paste text from clipboard (Ctrl+V)
92 Paste(String),
93 /// Delete selected text (Shift+Delete)
94 DeleteSelection,
95 /// Request redraw for cursor blink
96 Tick,
97 /// Page Up pressed
98 PageUp,
99 /// Page Down pressed
100 PageDown,
101 /// Home key pressed (move to start of line, shift_pressed)
102 Home(bool),
103 /// End key pressed (move to end of line, shift_pressed)
104 End(bool),
105 /// Ctrl+Home pressed (move to start of document)
106 CtrlHome,
107 /// Ctrl+End pressed (move to end of document)
108 CtrlEnd,
109 /// Viewport scrolled - track scroll position
110 Scrolled(iced::widget::scrollable::Viewport),
111 /// Undo last operation (Ctrl+Z)
112 Undo,
113 /// Redo last undone operation (Ctrl+Y)
114 Redo,
115}
116
117/// Arrow key directions
118#[derive(Debug, Clone, Copy)]
119pub enum ArrowDirection {
120 Up,
121 Down,
122 Left,
123 Right,
124}
125
126impl CodeEditor {
127 /// Creates a new canvas-based text editor.
128 ///
129 /// # Arguments
130 ///
131 /// * `content` - Initial text content
132 /// * `syntax` - Syntax highlighting language (e.g., "py", "lua", "rs")
133 ///
134 /// # Returns
135 ///
136 /// A new `CodeEditor` instance
137 pub fn new(content: &str, syntax: &str) -> Self {
138 Self {
139 buffer: TextBuffer::new(content),
140 cursor: (0, 0),
141 scroll_offset: 0.0,
142 style: crate::theme::dark(&iced::Theme::Dark),
143 syntax: syntax.to_string(),
144 last_blink: Instant::now(),
145 cursor_visible: true,
146 selection_start: None,
147 selection_end: None,
148 is_dragging: false,
149 cache: canvas::Cache::default(),
150 scrollable_id: Id::unique(),
151 viewport_scroll: 0.0,
152 viewport_height: 600.0, // Default, will be updated
153 history: CommandHistory::new(100),
154 is_grouping: false,
155 }
156 }
157
158 /// Returns the current text content as a string.
159 ///
160 /// # Returns
161 ///
162 /// The complete text content of the editor
163 pub fn content(&self) -> String {
164 self.buffer.to_string()
165 }
166
167 /// Sets the theme style for the editor.
168 ///
169 /// # Arguments
170 ///
171 /// * `style` - The style to apply to the editor
172 ///
173 /// # Example
174 ///
175 /// ```
176 /// use iced_code_editor::{CodeEditor, theme};
177 ///
178 /// let mut editor = CodeEditor::new("fn main() {}", "rs");
179 /// editor.set_theme(theme::light(&iced::Theme::Light));
180 /// ```
181 pub fn set_theme(&mut self, style: Style) {
182 self.style = style;
183 self.cache.clear(); // Force redraw with new theme
184 }
185
186 /// Resets the cursor blink animation.
187 pub(crate) fn reset_cursor_blink(&mut self) {
188 self.last_blink = Instant::now();
189 self.cursor_visible = true;
190 }
191
192 /// Returns whether the editor has unsaved changes.
193 ///
194 /// # Returns
195 ///
196 /// `true` if there are unsaved modifications, `false` otherwise
197 pub fn is_modified(&self) -> bool {
198 self.history.is_modified()
199 }
200
201 /// Marks the current state as saved.
202 ///
203 /// Call this after successfully saving the file to reset the modified state.
204 pub fn mark_saved(&mut self) {
205 self.history.mark_saved();
206 }
207
208 /// Returns whether undo is available.
209 pub fn can_undo(&self) -> bool {
210 self.history.can_undo()
211 }
212
213 /// Returns whether redo is available.
214 pub fn can_redo(&self) -> bool {
215 self.history.can_redo()
216 }
217}