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                ..
220            }) => {
221                // Handle Ctrl+C / Ctrl+Insert (copy)
222                if (modifiers.control()
223                    && matches!(key, keyboard::Key::Character(c) if c.as_str() == "c"))
224                    || (modifiers.control()
225                        && matches!(
226                            key,
227                            keyboard::Key::Named(keyboard::key::Named::Insert)
228                        ))
229                {
230                    return Some(Action::publish(Message::Copy).and_capture());
231                }
232
233                // Handle Ctrl+Z (undo)
234                if modifiers.control()
235                    && matches!(key, keyboard::Key::Character(z) if z.as_str() == "z")
236                {
237                    return Some(Action::publish(Message::Undo).and_capture());
238                }
239
240                // Handle Ctrl+Y (redo)
241                if modifiers.control()
242                    && matches!(key, keyboard::Key::Character(y) if y.as_str() == "y")
243                {
244                    return Some(Action::publish(Message::Redo).and_capture());
245                }
246
247                // Handle Ctrl+V / Shift+Insert (paste) - read clipboard and send paste message
248                if (modifiers.control()
249                    && matches!(key, keyboard::Key::Character(v) if v.as_str() == "v"))
250                    || (modifiers.shift()
251                        && matches!(
252                            key,
253                            keyboard::Key::Named(keyboard::key::Named::Insert)
254                        ))
255                {
256                    // Return an action that requests clipboard read
257                    return Some(Action::publish(
258                        Message::Paste(String::new()),
259                    ));
260                }
261
262                // Handle Ctrl+Home (go to start of document)
263                if modifiers.control()
264                    && matches!(
265                        key,
266                        keyboard::Key::Named(keyboard::key::Named::Home)
267                    )
268                {
269                    return Some(
270                        Action::publish(Message::CtrlHome).and_capture(),
271                    );
272                }
273
274                // Handle Ctrl+End (go to end of document)
275                if modifiers.control()
276                    && matches!(
277                        key,
278                        keyboard::Key::Named(keyboard::key::Named::End)
279                    )
280                {
281                    return Some(
282                        Action::publish(Message::CtrlEnd).and_capture(),
283                    );
284                }
285
286                // Handle Shift+Delete (delete selection)
287                if modifiers.shift()
288                    && matches!(
289                        key,
290                        keyboard::Key::Named(keyboard::key::Named::Delete)
291                    )
292                {
293                    return Some(
294                        Action::publish(Message::DeleteSelection).and_capture(),
295                    );
296                }
297
298                let message = match key {
299                    keyboard::Key::Character(c) if !modifiers.control() => {
300                        c.chars().next().map(Message::CharacterInput)
301                    }
302                    keyboard::Key::Named(keyboard::key::Named::Backspace) => {
303                        Some(Message::Backspace)
304                    }
305                    keyboard::Key::Named(keyboard::key::Named::Delete) => {
306                        Some(Message::Delete)
307                    }
308                    keyboard::Key::Named(keyboard::key::Named::Enter) => {
309                        Some(Message::Enter)
310                    }
311                    keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
312                        Some(Message::ArrowKey(
313                            ArrowDirection::Up,
314                            modifiers.shift(),
315                        ))
316                    }
317                    keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
318                        Some(Message::ArrowKey(
319                            ArrowDirection::Down,
320                            modifiers.shift(),
321                        ))
322                    }
323                    keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
324                        Some(Message::ArrowKey(
325                            ArrowDirection::Left,
326                            modifiers.shift(),
327                        ))
328                    }
329                    keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
330                        Some(Message::ArrowKey(
331                            ArrowDirection::Right,
332                            modifiers.shift(),
333                        ))
334                    }
335                    keyboard::Key::Named(keyboard::key::Named::PageUp) => {
336                        Some(Message::PageUp)
337                    }
338                    keyboard::Key::Named(keyboard::key::Named::PageDown) => {
339                        Some(Message::PageDown)
340                    }
341                    keyboard::Key::Named(keyboard::key::Named::Home) => {
342                        Some(Message::Home(modifiers.shift()))
343                    }
344                    keyboard::Key::Named(keyboard::key::Named::End) => {
345                        Some(Message::End(modifiers.shift()))
346                    }
347                    _ => None,
348                };
349
350                message.map(|msg| Action::publish(msg).and_capture())
351            }
352            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
353                cursor.position_in(bounds).map(|position| {
354                    // Don't capture the event so it can bubble up for focus management
355                    Action::publish(Message::MouseClick(position))
356                })
357            }
358            Event::Mouse(mouse::Event::CursorMoved { .. }) => {
359                // Handle mouse drag for selection
360                cursor.position_in(bounds).map(|position| {
361                    Action::publish(Message::MouseDrag(position)).and_capture()
362                })
363            }
364            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
365                Some(Action::publish(Message::MouseRelease).and_capture())
366            }
367            _ => None,
368        }
369    }
370}