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::WrappingCalculator;
12use super::{CodeEditor, GUTTER_WIDTH, Message};
13
14impl CodeEditor {
15    /// Creates the view element with scrollable wrapper.
16    ///
17    /// The backgrounds (editor and gutter) are handled by container styles
18    /// to ensure proper clipping when the pane is resized.
19    pub fn view(&self) -> Element<'_, Message> {
20        // Calculate total content height based on actual lines
21        // When wrapping is enabled, use visual line count
22        let wrapping_calc = WrappingCalculator::new(
23            self.wrap_enabled,
24            self.wrap_column,
25            self.full_char_width,
26            self.char_width,
27        );
28
29        // Use viewport width for calculating visual lines
30        let visual_lines = wrapping_calc.calculate_visual_lines(
31            &self.buffer,
32            self.viewport_width,
33            self.gutter_width(),
34        );
35
36        let total_visual_lines = visual_lines.len();
37        let content_height = total_visual_lines as f32 * self.line_height;
38
39        // Use max of content height and viewport height to ensure the canvas
40        // always covers the visible area (prevents visual artifacts when
41        // content is shorter than viewport after reset/file change)
42        let canvas_height = content_height.max(self.viewport_height);
43
44        // Create canvas with height that covers at least the viewport
45        let canvas = Canvas::new(self)
46            .width(Length::Fill)
47            .height(Length::Fixed(canvas_height));
48
49        // Capture style colors for closures
50        let scrollbar_bg = self.style.scrollbar_background;
51        let scroller_color = self.style.scroller_color;
52        let background_color = self.style.background;
53        let gutter_background = self.style.gutter_background;
54
55        // Wrap in scrollable for automatic scrollbar display with custom style
56        // Use Length::Fill to respect parent container constraints and enable proper clipping
57        // Background is TRANSPARENT here because it's handled by the Stack layer below
58        let scrollable = Scrollable::new(canvas)
59            .id(self.scrollable_id.clone())
60            .width(Length::Fill)
61            .height(Length::Fill)
62            .on_scroll(Message::Scrolled)
63            .style(move |_theme, _status| scrollable::Style {
64                container: container::Style {
65                    background: Some(Background::Color(Color::TRANSPARENT)),
66                    ..container::Style::default()
67                },
68                vertical_rail: scrollable::Rail {
69                    background: Some(scrollbar_bg.into()),
70                    border: Border {
71                        radius: 4.0.into(),
72                        width: 0.0,
73                        color: Color::TRANSPARENT,
74                    },
75                    scroller: scrollable::Scroller {
76                        background: scroller_color.into(),
77                        border: Border {
78                            radius: 4.0.into(),
79                            width: 0.0,
80                            color: Color::TRANSPARENT,
81                        },
82                    },
83                },
84                horizontal_rail: scrollable::Rail {
85                    background: Some(scrollbar_bg.into()),
86                    border: Border {
87                        radius: 4.0.into(),
88                        width: 0.0,
89                        color: Color::TRANSPARENT,
90                    },
91                    scroller: scrollable::Scroller {
92                        background: scroller_color.into(),
93                        border: Border {
94                            radius: 4.0.into(),
95                            width: 0.0,
96                            color: Color::TRANSPARENT,
97                        },
98                    },
99                },
100                gap: None,
101                auto_scroll: scrollable::AutoScroll {
102                    background: Color::TRANSPARENT.into(),
103                    border: Border::default(),
104                    shadow: Shadow::default(),
105                    icon: Color::TRANSPARENT,
106                },
107            });
108
109        // Gutter background container (fixed width, clipped by parent)
110        // Only create if line numbers are enabled
111        let gutter_container = if self.line_numbers_enabled {
112            Some(
113                container(
114                    Space::new().width(Length::Fill).height(Length::Fill),
115                )
116                .width(Length::Fixed(GUTTER_WIDTH))
117                .height(Length::Fill)
118                .style(move |_| container::Style {
119                    background: Some(Background::Color(gutter_background)),
120                    ..container::Style::default()
121                }),
122            )
123        } else {
124            None
125        };
126
127        // Code background container (fills remaining width)
128        let code_background_container =
129            container(Space::new().width(Length::Fill).height(Length::Fill))
130                .width(Length::Fill)
131                .height(Length::Fill)
132                .style(move |_| container::Style {
133                    background: Some(Background::Color(background_color)),
134                    ..container::Style::default()
135                });
136
137        // Main layout: use a Stack to layer the backgrounds behind the scrollable
138        // The scrollable has a transparent background so the colors show through
139        let background_row = if let Some(gutter) = gutter_container {
140            Row::new().push(gutter).push(code_background_container)
141        } else {
142            Row::new().push(code_background_container)
143        };
144
145        let mut editor_stack = iced::widget::Stack::new()
146            .push(
147                // Background layer (bottom): gutter + code backgrounds
148                background_row,
149            )
150            .push(
151                // Scrollable layer (top) - transparent, overlays the backgrounds
152                scrollable,
153            );
154
155        let ime_enabled = self.is_focused() && self.has_canvas_focus;
156        let cursor_rect = if ime_enabled {
157            if let Some(cursor_visual) = WrappingCalculator::logical_to_visual(
158                &visual_lines,
159                self.cursor.0,
160                self.cursor.1,
161            ) {
162                let vl = &visual_lines[cursor_visual];
163                let line_content = self.buffer.line(vl.logical_line);
164                let prefix_len = self.cursor.1.saturating_sub(vl.start_col);
165                let prefix_text: String = line_content
166                    .chars()
167                    .skip(vl.start_col)
168                    .take(prefix_len)
169                    .collect();
170                let cursor_x = self.gutter_width()
171                    + 5.0
172                    + super::measure_text_width(
173                        &prefix_text,
174                        self.full_char_width,
175                        self.char_width,
176                    );
177
178                // Calculate visual Y position relative to the viewport
179                // We subtract viewport_scroll because the content is scrolled up/down
180                // but the cursor position sent to IME must be relative to the visible area
181                let cursor_y = (cursor_visual as f32 * self.line_height)
182                    - self.viewport_scroll;
183
184                Rectangle::new(
185                    iced::Point::new(cursor_x, cursor_y + 2.0),
186                    Size::new(2.0, self.line_height - 4.0),
187                )
188            } else {
189                Rectangle::new(iced::Point::new(0.0, 0.0), Size::new(0.0, 0.0))
190            }
191        } else {
192            Rectangle::new(iced::Point::new(0.0, 0.0), Size::new(0.0, 0.0))
193        };
194
195        let preedit =
196            self.ime_preedit.as_ref().map(|p| input_method::Preedit {
197                content: p.content.clone(),
198                selection: p.selection.clone(),
199                text_size: None,
200            });
201
202        // Invisible IME request layer: sends IME state and caret on each redraw
203        // Note: Canvas Program cannot access Shell directly, so this widget bridges it
204        let ime_layer = ImeRequester::new(ime_enabled, cursor_rect, preedit);
205        editor_stack = editor_stack.push(iced::Element::new(ime_layer));
206
207        // Add search dialog overlay if open
208        if self.search_state.is_open {
209            let search_dialog =
210                search_dialog::view(&self.search_state, &self.translations);
211
212            // Position the dialog in top-right corner with 20px margin
213            // Use a Row with Fill space to push the dialog to the right
214            let positioned_dialog = container(
215                Row::new()
216                    .push(Space::new().width(Length::Fill)) // Push to right
217                    .push(search_dialog),
218            )
219            .padding(20) // 20px margin from edges
220            .width(Length::Fill)
221            .height(Length::Shrink);
222
223            editor_stack = editor_stack.push(positioned_dialog);
224        }
225
226        // Wrap in a container with clip to ensure proper bounds
227        container(editor_stack)
228            .width(Length::Fill)
229            .height(Length::Fill)
230            .clip(true)
231            .into()
232    }
233}