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