Skip to main content

iced_code_editor/canvas_editor/
view.rs

1//! Iced UI view and rendering logic.
2
3use iced::Size;
4use iced::advanced::input_method;
5use iced::widget::canvas::Canvas;
6use iced::widget::{Row, Scrollable, Space, container, scrollable};
7use iced::{Background, Border, Color, Element, Length, Rectangle, Shadow};
8
9use super::ime_requester::ImeRequester;
10use super::search_dialog;
11use super::wrapping::{self, WrappingCalculator};
12use super::{CodeEditor, GUTTER_WIDTH, Message};
13use std::rc::Rc;
14
15impl CodeEditor {
16    /// Calculates visual lines and canvas height for the editor.
17    ///
18    /// Returns a tuple of (visual_lines, canvas_height) where:
19    /// - visual_lines: The visual line mapping with wrapping applied
20    /// - canvas_height: The total height needed for the canvas
21    fn calculate_canvas_height(&self) -> (Rc<Vec<wrapping::VisualLine>>, f32) {
22        // Reuse memoized visual lines so view layout (canvas height + IME cursor rect)
23        // does not trigger repeated wrapping computation.
24        let visual_lines = self.visual_lines_cached(self.viewport_width);
25        let total_visual_lines = visual_lines.len();
26        let content_height = total_visual_lines as f32 * self.line_height;
27
28        // Use max of content height and viewport height to ensure the canvas
29        // always covers the visible area (prevents visual artifacts when
30        // content is shorter than viewport after reset/file change)
31        let canvas_height = content_height.max(self.viewport_height);
32
33        (visual_lines, canvas_height)
34    }
35
36    /// Creates the scrollable style function with custom colors.
37    ///
38    /// Returns a style function that configures the scrollbar appearance.
39    fn create_scrollable_style(
40        &self,
41    ) -> impl Fn(&iced::Theme, scrollable::Status) -> scrollable::Style {
42        let scrollbar_bg = self.style.scrollbar_background;
43        let scroller_color = self.style.scroller_color;
44
45        move |_theme, _status| scrollable::Style {
46            container: container::Style {
47                background: Some(Background::Color(Color::TRANSPARENT)),
48                ..container::Style::default()
49            },
50            vertical_rail: scrollable::Rail {
51                background: Some(scrollbar_bg.into()),
52                border: Border {
53                    radius: 4.0.into(),
54                    width: 0.0,
55                    color: Color::TRANSPARENT,
56                },
57                scroller: scrollable::Scroller {
58                    background: scroller_color.into(),
59                    border: Border {
60                        radius: 4.0.into(),
61                        width: 0.0,
62                        color: Color::TRANSPARENT,
63                    },
64                },
65            },
66            horizontal_rail: scrollable::Rail {
67                background: Some(scrollbar_bg.into()),
68                border: Border {
69                    radius: 4.0.into(),
70                    width: 0.0,
71                    color: Color::TRANSPARENT,
72                },
73                scroller: scrollable::Scroller {
74                    background: scroller_color.into(),
75                    border: Border {
76                        radius: 4.0.into(),
77                        width: 0.0,
78                        color: Color::TRANSPARENT,
79                    },
80                },
81            },
82            gap: None,
83            auto_scroll: scrollable::AutoScroll {
84                background: Color::TRANSPARENT.into(),
85                border: Border::default(),
86                shadow: Shadow::default(),
87                icon: Color::TRANSPARENT,
88            },
89        }
90    }
91
92    /// Creates the canvas widget wrapped in a scrollable container.
93    ///
94    /// # Arguments
95    ///
96    /// * `canvas_height` - The total height of the canvas
97    ///
98    /// # Returns
99    ///
100    /// A configured scrollable widget containing the canvas
101    fn create_canvas_with_scrollable(
102        &self,
103        canvas_height: f32,
104    ) -> Scrollable<'_, Message> {
105        let canvas = Canvas::new(self)
106            .width(Length::Fill)
107            .height(Length::Fixed(canvas_height));
108
109        Scrollable::new(canvas)
110            .id(self.scrollable_id.clone())
111            .width(Length::Fill)
112            .height(Length::Fill)
113            .on_scroll(Message::Scrolled)
114            .style(self.create_scrollable_style())
115    }
116
117    /// Creates the gutter background container if line numbers are enabled.
118    ///
119    /// # Returns
120    ///
121    /// Some(container) if line numbers are enabled, None otherwise
122    fn create_gutter_container(
123        &self,
124    ) -> Option<container::Container<'_, Message>> {
125        if self.line_numbers_enabled {
126            let gutter_background = self.style.gutter_background;
127            Some(
128                container(
129                    Space::new().width(Length::Fill).height(Length::Fill),
130                )
131                .width(Length::Fixed(GUTTER_WIDTH))
132                .height(Length::Fill)
133                .style(move |_| container::Style {
134                    background: Some(Background::Color(gutter_background)),
135                    ..container::Style::default()
136                }),
137            )
138        } else {
139            None
140        }
141    }
142
143    /// Creates the code area background container.
144    ///
145    /// # Returns
146    ///
147    /// The code background container widget
148    fn create_code_background_container(
149        &self,
150    ) -> container::Container<'_, Message> {
151        let background_color = self.style.background;
152        container(Space::new().width(Length::Fill).height(Length::Fill))
153            .width(Length::Fill)
154            .height(Length::Fill)
155            .style(move |_| container::Style {
156                background: Some(Background::Color(background_color)),
157                ..container::Style::default()
158            })
159    }
160
161    /// Creates the background layer combining gutter and code backgrounds.
162    ///
163    /// # Returns
164    ///
165    /// A row containing the background elements
166    fn create_background_layer(&self) -> Row<'_, Message> {
167        let gutter_container = self.create_gutter_container();
168        let code_background_container = self.create_code_background_container();
169
170        if let Some(gutter) = gutter_container {
171            Row::new().push(gutter).push(code_background_container)
172        } else {
173            Row::new().push(code_background_container)
174        }
175    }
176
177    /// Calculates the IME cursor rectangle for the current cursor position.
178    ///
179    /// # Arguments
180    ///
181    /// * `visual_lines` - The visual line mapping
182    ///
183    /// # Returns
184    ///
185    /// A rectangle representing the cursor position for IME
186    fn calculate_ime_cursor_rect(
187        &self,
188        visual_lines: &[wrapping::VisualLine],
189    ) -> Rectangle {
190        let ime_enabled = self.is_focused() && self.has_canvas_focus;
191
192        if !ime_enabled {
193            return Rectangle::new(
194                iced::Point::new(0.0, 0.0),
195                Size::new(0.0, 0.0),
196            );
197        }
198
199        if let Some(cursor_visual) = WrappingCalculator::logical_to_visual(
200            visual_lines,
201            self.cursor.0,
202            self.cursor.1,
203        ) {
204            let vl = &visual_lines[cursor_visual];
205            let line_content = self.buffer.line(vl.logical_line);
206            let prefix_len = self.cursor.1.saturating_sub(vl.start_col);
207            let prefix_text: String = line_content
208                .chars()
209                .skip(vl.start_col)
210                .take(prefix_len)
211                .collect();
212            let cursor_x = self.gutter_width()
213                + 5.0
214                + super::measure_text_width(
215                    &prefix_text,
216                    self.full_char_width,
217                    self.char_width,
218                );
219
220            // Calculate visual Y position relative to the viewport
221            // We subtract viewport_scroll because the content is scrolled up/down
222            // but the cursor position sent to IME must be relative to the visible area
223            let cursor_y = (cursor_visual as f32 * self.line_height)
224                - self.viewport_scroll;
225
226            Rectangle::new(
227                iced::Point::new(cursor_x, cursor_y + 2.0),
228                Size::new(2.0, self.line_height - 4.0),
229            )
230        } else {
231            Rectangle::new(iced::Point::new(0.0, 0.0), Size::new(0.0, 0.0))
232        }
233    }
234
235    /// Creates the IME (Input Method Editor) layer widget.
236    ///
237    /// # Arguments
238    ///
239    /// * `cursor_rect` - The rectangle representing the cursor position
240    ///
241    /// # Returns
242    ///
243    /// An element containing the IME requester widget
244    fn create_ime_layer(&self, cursor_rect: Rectangle) -> Element<'_, Message> {
245        let ime_enabled = self.is_focused() && self.has_canvas_focus;
246
247        let preedit =
248            self.ime_preedit.as_ref().map(|p| input_method::Preedit {
249                content: p.content.clone(),
250                selection: p.selection.clone(),
251                text_size: None,
252            });
253
254        let ime_layer = ImeRequester::new(ime_enabled, cursor_rect, preedit);
255        iced::Element::new(ime_layer)
256    }
257
258    /// Creates the view element with scrollable wrapper.
259    ///
260    /// The backgrounds (editor and gutter) are handled by container styles
261    /// to ensure proper clipping when the pane is resized.
262    pub fn view(&self) -> Element<'_, Message> {
263        // Calculate canvas height and visual lines
264        let (visual_lines, canvas_height) = self.calculate_canvas_height();
265
266        // Create scrollable containing the canvas
267        let scrollable = self.create_canvas_with_scrollable(canvas_height);
268
269        // Create background layer with gutter and code backgrounds
270        let background_row = self.create_background_layer();
271
272        // Build editor stack: backgrounds + scrollable
273        let mut editor_stack =
274            iced::widget::Stack::new().push(background_row).push(scrollable);
275
276        // Add IME layer for input method support.
277        // The IME requester needs the cursor rect in viewport coordinates, which
278        // depends on the current logical↔visual mapping.
279        let cursor_rect = self.calculate_ime_cursor_rect(visual_lines.as_ref());
280        let ime_layer = self.create_ime_layer(cursor_rect);
281        editor_stack = editor_stack.push(ime_layer);
282
283        // Add search dialog overlay if open
284        if self.search_state.is_open {
285            let search_dialog =
286                search_dialog::view(&self.search_state, &self.translations);
287
288            // Position the dialog in top-right corner with 20px margin
289            let positioned_dialog = container(
290                Row::new()
291                    .push(Space::new().width(Length::Fill))
292                    .push(search_dialog),
293            )
294            .padding(20)
295            .width(Length::Fill)
296            .height(Length::Shrink);
297
298            editor_stack = editor_stack.push(positioned_dialog);
299        }
300
301        // Wrap in a container with clip to ensure proper bounds
302        container(editor_stack)
303            .width(Length::Fill)
304            .height(Length::Fill)
305            .clip(true)
306            .into()
307    }
308}