iced_code_editor/canvas_editor/
canvas_impl.rs

1//! Canvas rendering implementation using Iced's `canvas::Program`.
2
3use iced::mouse;
4use iced::widget::canvas::{self, Geometry};
5use iced::{Color, Event, Point, Rectangle, Size, Theme, keyboard};
6use syntect::easy::HighlightLines;
7use syntect::highlighting::{Style, ThemeSet};
8use syntect::parsing::SyntaxSet;
9
10use super::{
11    ArrowDirection, CHAR_WIDTH, CodeEditor, FONT_SIZE, GUTTER_WIDTH,
12    LINE_HEIGHT, Message,
13};
14use iced::widget::canvas::Action;
15
16impl canvas::Program<Message> for CodeEditor {
17    type State = ();
18
19    fn draw(
20        &self,
21        _state: &Self::State,
22        renderer: &iced::Renderer,
23        _theme: &Theme,
24        bounds: Rectangle,
25        _cursor: mouse::Cursor,
26    ) -> Vec<Geometry> {
27        let geometry = self.cache.draw(renderer, bounds.size(), |frame| {
28            // Note: Background colors are handled by parent containers in view.rs
29            // to ensure proper clipping when the editor is resized.
30            // The Canvas only draws content (text, line numbers, cursor, selection).
31
32            let total_lines = self.buffer.line_count();
33
34            // Calculate visible line range based on viewport for optimized rendering
35            // This ensures we only draw lines that are visible, preventing overflow
36            // and improving performance for large files.
37            // Use bounds.height as fallback when viewport_height is not yet initialized
38            let effective_viewport_height = if self.viewport_height > 0.0 {
39                self.viewport_height
40            } else {
41                bounds.height
42            };
43            let first_visible_line =
44                (self.viewport_scroll / LINE_HEIGHT).floor() as usize;
45            let visible_lines_count =
46                (effective_viewport_height / LINE_HEIGHT).ceil() as usize + 2;
47            let last_visible_line =
48                (first_visible_line + visible_lines_count).min(total_lines);
49
50            // Load syntax highlighting
51            let syntax_set = SyntaxSet::load_defaults_newlines();
52            let theme_set = ThemeSet::load_defaults();
53            let syntax_theme = &theme_set.themes["base16-ocean.dark"];
54
55            let syntax_ref = match self.syntax.as_str() {
56                "py" | "python" => syntax_set.find_syntax_by_extension("py"),
57                "lua" => syntax_set.find_syntax_by_extension("lua"),
58                "rs" | "rust" => syntax_set.find_syntax_by_extension("rs"),
59                "js" | "javascript" => {
60                    syntax_set.find_syntax_by_extension("js")
61                }
62                "html" | "htm" => syntax_set.find_syntax_by_extension("html"),
63                "xml" | "svg" => syntax_set.find_syntax_by_extension("xml"),
64                "css" => syntax_set.find_syntax_by_extension("css"),
65                "json" => syntax_set.find_syntax_by_extension("json"),
66                "md" | "markdown" => syntax_set.find_syntax_by_extension("md"),
67                _ => Some(syntax_set.find_syntax_plain_text()),
68            };
69
70            // Draw only visible lines (virtual scrolling optimization)
71            for line_idx in first_visible_line..last_visible_line {
72                let y = line_idx as f32 * LINE_HEIGHT;
73
74                // Note: Gutter background is handled by a container in view.rs
75                // to ensure proper clipping when the pane is resized.
76
77                // Draw line number
78                let line_num_text = format!("{:>4}", line_idx + 1);
79                frame.fill_text(canvas::Text {
80                    content: line_num_text,
81                    position: Point::new(5.0, y + 2.0),
82                    color: self.style.line_number_color,
83                    size: FONT_SIZE.into(),
84                    font: iced::Font::MONOSPACE,
85                    ..canvas::Text::default()
86                });
87
88                // Highlight current line
89                if line_idx == self.cursor.0 {
90                    frame.fill_rectangle(
91                        Point::new(GUTTER_WIDTH, y),
92                        Size::new(bounds.width - GUTTER_WIDTH, LINE_HEIGHT),
93                        self.style.current_line_highlight,
94                    );
95                }
96
97                // Draw text content with syntax highlighting
98                let line_content = self.buffer.line(line_idx);
99
100                if let Some(syntax) = syntax_ref {
101                    let mut highlighter =
102                        HighlightLines::new(syntax, syntax_theme);
103                    let ranges = highlighter
104                        .highlight_line(line_content, &syntax_set)
105                        .unwrap_or_else(|_| {
106                            vec![(Style::default(), line_content)]
107                        });
108
109                    let mut x_offset = GUTTER_WIDTH + 5.0;
110                    for (style, text) in ranges {
111                        let color = Color::from_rgb(
112                            f32::from(style.foreground.r) / 255.0,
113                            f32::from(style.foreground.g) / 255.0,
114                            f32::from(style.foreground.b) / 255.0,
115                        );
116
117                        frame.fill_text(canvas::Text {
118                            content: text.to_string(),
119                            position: Point::new(x_offset, y + 2.0),
120                            color,
121                            size: FONT_SIZE.into(),
122                            font: iced::Font::MONOSPACE,
123                            ..canvas::Text::default()
124                        });
125
126                        x_offset += text.len() as f32 * CHAR_WIDTH;
127                    }
128                } else {
129                    // Fallback to plain text
130                    frame.fill_text(canvas::Text {
131                        content: line_content.to_string(),
132                        position: Point::new(GUTTER_WIDTH + 5.0, y + 2.0),
133                        color: self.style.text_color,
134                        size: FONT_SIZE.into(),
135                        font: iced::Font::MONOSPACE,
136                        ..canvas::Text::default()
137                    });
138                }
139            }
140
141            // Draw selection highlight
142            if let Some((start, end)) = self.get_selection_range()
143                && start != end
144            {
145                let selection_color = Color { r: 0.3, g: 0.5, b: 0.8, a: 0.3 };
146
147                if start.0 == end.0 {
148                    // Single line selection
149                    let y = start.0 as f32 * LINE_HEIGHT;
150                    let x_start =
151                        GUTTER_WIDTH + 5.0 + start.1 as f32 * CHAR_WIDTH;
152                    let x_end = GUTTER_WIDTH + 5.0 + end.1 as f32 * CHAR_WIDTH;
153
154                    frame.fill_rectangle(
155                        Point::new(x_start, y + 2.0),
156                        Size::new(x_end - x_start, LINE_HEIGHT - 4.0),
157                        selection_color,
158                    );
159                } else {
160                    // Multi-line selection
161                    // First line - from start column to end of line
162                    let y_start = start.0 as f32 * LINE_HEIGHT;
163                    let x_start =
164                        GUTTER_WIDTH + 5.0 + start.1 as f32 * CHAR_WIDTH;
165                    let first_line_len = self.buffer.line_len(start.0);
166                    let x_end_first =
167                        GUTTER_WIDTH + 5.0 + first_line_len as f32 * CHAR_WIDTH;
168
169                    frame.fill_rectangle(
170                        Point::new(x_start, y_start + 2.0),
171                        Size::new(x_end_first - x_start, LINE_HEIGHT - 4.0),
172                        selection_color,
173                    );
174
175                    // Middle lines - full width
176                    for line_idx in (start.0 + 1)..end.0 {
177                        let y = line_idx as f32 * LINE_HEIGHT;
178                        let line_len = self.buffer.line_len(line_idx);
179                        let width = line_len as f32 * CHAR_WIDTH;
180
181                        frame.fill_rectangle(
182                            Point::new(GUTTER_WIDTH + 5.0, y + 2.0),
183                            Size::new(width, LINE_HEIGHT - 4.0),
184                            selection_color,
185                        );
186                    }
187
188                    // Last line - from start of line to end column
189                    let y_end = end.0 as f32 * LINE_HEIGHT;
190                    let x_end = GUTTER_WIDTH + 5.0 + end.1 as f32 * CHAR_WIDTH;
191
192                    frame.fill_rectangle(
193                        Point::new(GUTTER_WIDTH + 5.0, y_end + 2.0),
194                        Size::new(
195                            x_end - (GUTTER_WIDTH + 5.0),
196                            LINE_HEIGHT - 4.0,
197                        ),
198                        selection_color,
199                    );
200                }
201            }
202
203            // Draw cursor
204            if self.cursor_visible {
205                let cursor_x =
206                    GUTTER_WIDTH + 5.0 + self.cursor.1 as f32 * CHAR_WIDTH;
207                let cursor_y = self.cursor.0 as f32 * LINE_HEIGHT;
208
209                frame.fill_rectangle(
210                    Point::new(cursor_x, cursor_y + 2.0),
211                    Size::new(2.0, LINE_HEIGHT - 4.0),
212                    self.style.text_color,
213                );
214            }
215        });
216
217        vec![geometry]
218    }
219
220    fn update(
221        &self,
222        _state: &mut Self::State,
223        event: &Event,
224        bounds: Rectangle,
225        cursor: mouse::Cursor,
226    ) -> Option<Action<Message>> {
227        match event {
228            Event::Keyboard(keyboard::Event::KeyPressed {
229                key,
230                modifiers,
231                text,
232                ..
233            }) => {
234                // Handle Ctrl+C / Ctrl+Insert (copy)
235                if (modifiers.control()
236                    && matches!(key, keyboard::Key::Character(c) if c.as_str() == "c"))
237                    || (modifiers.control()
238                        && matches!(
239                            key,
240                            keyboard::Key::Named(keyboard::key::Named::Insert)
241                        ))
242                {
243                    return Some(Action::publish(Message::Copy).and_capture());
244                }
245
246                // Handle Ctrl+Z (undo)
247                if modifiers.control()
248                    && matches!(key, keyboard::Key::Character(z) if z.as_str() == "z")
249                {
250                    return Some(Action::publish(Message::Undo).and_capture());
251                }
252
253                // Handle Ctrl+Y (redo)
254                if modifiers.control()
255                    && matches!(key, keyboard::Key::Character(y) if y.as_str() == "y")
256                {
257                    return Some(Action::publish(Message::Redo).and_capture());
258                }
259
260                // Handle Ctrl+V / Shift+Insert (paste) - read clipboard and send paste message
261                if (modifiers.control()
262                    && matches!(key, keyboard::Key::Character(v) if v.as_str() == "v"))
263                    || (modifiers.shift()
264                        && matches!(
265                            key,
266                            keyboard::Key::Named(keyboard::key::Named::Insert)
267                        ))
268                {
269                    // Return an action that requests clipboard read
270                    return Some(Action::publish(
271                        Message::Paste(String::new()),
272                    ));
273                }
274
275                // Handle Ctrl+Home (go to start of document)
276                if modifiers.control()
277                    && matches!(
278                        key,
279                        keyboard::Key::Named(keyboard::key::Named::Home)
280                    )
281                {
282                    return Some(
283                        Action::publish(Message::CtrlHome).and_capture(),
284                    );
285                }
286
287                // Handle Ctrl+End (go to end of document)
288                if modifiers.control()
289                    && matches!(
290                        key,
291                        keyboard::Key::Named(keyboard::key::Named::End)
292                    )
293                {
294                    return Some(
295                        Action::publish(Message::CtrlEnd).and_capture(),
296                    );
297                }
298
299                // Handle Shift+Delete (delete selection)
300                if modifiers.shift()
301                    && matches!(
302                        key,
303                        keyboard::Key::Named(keyboard::key::Named::Delete)
304                    )
305                {
306                    return Some(
307                        Action::publish(Message::DeleteSelection).and_capture(),
308                    );
309                }
310
311                // PRIORITY 1: Check if 'text' field has valid printable character
312                // This handles:
313                // - Numpad keys with NumLock ON (key=Named(ArrowDown), text=Some("2"))
314                // - Regular typing with shift, accents, international layouts
315                if let Some(text_content) = text
316                    && !text_content.is_empty()
317                    && !modifiers.control()
318                    && !modifiers.alt()
319                {
320                    // Check if it's a printable character (not a control character)
321                    // This filters out Enter (\n), Tab (\t), Delete (U+007F), etc.
322                    if let Some(first_char) = text_content.chars().next()
323                        && !first_char.is_control()
324                    {
325                        return Some(
326                            Action::publish(Message::CharacterInput(
327                                first_char,
328                            ))
329                            .and_capture(),
330                        );
331                    }
332                }
333
334                // PRIORITY 2: Handle special named keys (navigation, editing)
335                // These are only processed if text didn't contain a printable character
336                let message = match key {
337                    keyboard::Key::Named(keyboard::key::Named::Backspace) => {
338                        Some(Message::Backspace)
339                    }
340                    keyboard::Key::Named(keyboard::key::Named::Delete) => {
341                        Some(Message::Delete)
342                    }
343                    keyboard::Key::Named(keyboard::key::Named::Enter) => {
344                        Some(Message::Enter)
345                    }
346                    keyboard::Key::Named(keyboard::key::Named::Tab) => {
347                        // Insert 4 spaces for Tab
348                        Some(Message::Tab)
349                    }
350                    keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
351                        Some(Message::ArrowKey(
352                            ArrowDirection::Up,
353                            modifiers.shift(),
354                        ))
355                    }
356                    keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
357                        Some(Message::ArrowKey(
358                            ArrowDirection::Down,
359                            modifiers.shift(),
360                        ))
361                    }
362                    keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
363                        Some(Message::ArrowKey(
364                            ArrowDirection::Left,
365                            modifiers.shift(),
366                        ))
367                    }
368                    keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
369                        Some(Message::ArrowKey(
370                            ArrowDirection::Right,
371                            modifiers.shift(),
372                        ))
373                    }
374                    keyboard::Key::Named(keyboard::key::Named::PageUp) => {
375                        Some(Message::PageUp)
376                    }
377                    keyboard::Key::Named(keyboard::key::Named::PageDown) => {
378                        Some(Message::PageDown)
379                    }
380                    keyboard::Key::Named(keyboard::key::Named::Home) => {
381                        Some(Message::Home(modifiers.shift()))
382                    }
383                    keyboard::Key::Named(keyboard::key::Named::End) => {
384                        Some(Message::End(modifiers.shift()))
385                    }
386                    // PRIORITY 3: Fallback to extracting from 'key' if text was empty/control char
387                    // This handles edge cases where text field is not populated
388                    _ => {
389                        if !modifiers.control()
390                            && !modifiers.alt()
391                            && let keyboard::Key::Character(c) = key
392                            && !c.is_empty()
393                        {
394                            return c
395                                .chars()
396                                .next()
397                                .map(Message::CharacterInput)
398                                .map(|msg| Action::publish(msg).and_capture());
399                        }
400                        None
401                    }
402                };
403
404                message.map(|msg| Action::publish(msg).and_capture())
405            }
406            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
407                cursor.position_in(bounds).map(|position| {
408                    // Don't capture the event so it can bubble up for focus management
409                    Action::publish(Message::MouseClick(position))
410                })
411            }
412            Event::Mouse(mouse::Event::CursorMoved { .. }) => {
413                // Handle mouse drag for selection
414                cursor.position_in(bounds).map(|position| {
415                    Action::publish(Message::MouseDrag(position)).and_capture()
416                })
417            }
418            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
419                Some(Action::publish(Message::MouseRelease).and_capture())
420            }
421            _ => None,
422        }
423    }
424}