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