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