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