iced_code_editor/canvas_editor/
view.rs1use iced::Size;
4use iced::advanced::input_method;
5use iced::widget::canvas::Canvas;
6use iced::widget::{Column, 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 fn calculate_canvas_height(&self) -> (Rc<Vec<wrapping::VisualLine>>, f32) {
22 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 let canvas_height = content_height.max(self.viewport_height);
32
33 (visual_lines, canvas_height)
34 }
35
36 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 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 fn create_horizontal_scrollbar(
127 &self,
128 max_content_width: f32,
129 ) -> Option<Element<'_, Message>> {
130 if self.wrap_enabled || max_content_width <= self.viewport_width {
131 return None;
132 }
133
134 let scrollbar_bg = self.style.scrollbar_background;
135 let scroller_color = self.style.scroller_color;
136
137 let h_scrollable = Scrollable::new(
138 Space::new().width(Length::Fixed(max_content_width)).height(0.0),
139 )
140 .id(self.horizontal_scrollable_id.clone())
141 .width(Length::Fill)
142 .height(Length::Fixed(12.0))
143 .direction(scrollable::Direction::Horizontal(
144 scrollable::Scrollbar::new(),
145 ))
146 .on_scroll(Message::HorizontalScrolled)
147 .style(move |_theme, _status| scrollable::Style {
148 container: container::Style {
149 background: Some(Background::Color(Color::TRANSPARENT)),
150 ..container::Style::default()
151 },
152 vertical_rail: scrollable::Rail {
153 background: Some(scrollbar_bg.into()),
154 border: Border {
155 radius: 4.0.into(),
156 width: 0.0,
157 color: Color::TRANSPARENT,
158 },
159 scroller: scrollable::Scroller {
160 background: scroller_color.into(),
161 border: Border {
162 radius: 4.0.into(),
163 width: 0.0,
164 color: Color::TRANSPARENT,
165 },
166 },
167 },
168 horizontal_rail: scrollable::Rail {
169 background: Some(scrollbar_bg.into()),
170 border: Border {
171 radius: 4.0.into(),
172 width: 0.0,
173 color: Color::TRANSPARENT,
174 },
175 scroller: scrollable::Scroller {
176 background: scroller_color.into(),
177 border: Border {
178 radius: 4.0.into(),
179 width: 0.0,
180 color: Color::TRANSPARENT,
181 },
182 },
183 },
184 gap: None,
185 auto_scroll: scrollable::AutoScroll {
186 background: Color::TRANSPARENT.into(),
187 border: Border::default(),
188 shadow: Shadow::default(),
189 icon: Color::TRANSPARENT,
190 },
191 });
192
193 Some(h_scrollable.into())
194 }
195
196 fn create_gutter_container(
202 &self,
203 ) -> Option<container::Container<'_, Message>> {
204 if self.line_numbers_enabled {
205 let gutter_background = self.style.gutter_background;
206 Some(
207 container(
208 Space::new().width(Length::Fill).height(Length::Fill),
209 )
210 .width(Length::Fixed(GUTTER_WIDTH))
211 .height(Length::Fill)
212 .style(move |_| container::Style {
213 background: Some(Background::Color(gutter_background)),
214 ..container::Style::default()
215 }),
216 )
217 } else {
218 None
219 }
220 }
221
222 fn create_code_background_container(
228 &self,
229 ) -> container::Container<'_, Message> {
230 let background_color = self.style.background;
231 container(Space::new().width(Length::Fill).height(Length::Fill))
232 .width(Length::Fill)
233 .height(Length::Fill)
234 .style(move |_| container::Style {
235 background: Some(Background::Color(background_color)),
236 ..container::Style::default()
237 })
238 }
239
240 fn create_background_layer(&self) -> Row<'_, Message> {
246 let gutter_container = self.create_gutter_container();
247 let code_background_container = self.create_code_background_container();
248
249 if let Some(gutter) = gutter_container {
250 Row::new().push(gutter).push(code_background_container)
251 } else {
252 Row::new().push(code_background_container)
253 }
254 }
255
256 fn calculate_ime_cursor_rect(
266 &self,
267 visual_lines: &[wrapping::VisualLine],
268 ) -> Rectangle {
269 let ime_enabled = self.is_focused() && self.has_canvas_focus;
270
271 if !ime_enabled {
272 return Rectangle::new(
273 iced::Point::new(0.0, 0.0),
274 Size::new(0.0, 0.0),
275 );
276 }
277
278 if let Some(cursor_visual) = WrappingCalculator::logical_to_visual(
279 visual_lines,
280 self.cursor.0,
281 self.cursor.1,
282 ) {
283 let vl = &visual_lines[cursor_visual];
284 let line_content = self.buffer.line(vl.logical_line);
285 let prefix_len = self.cursor.1.saturating_sub(vl.start_col);
286 let prefix_text: String = line_content
287 .chars()
288 .skip(vl.start_col)
289 .take(prefix_len)
290 .collect();
291 let cursor_x = self.gutter_width()
292 + 5.0
293 + super::measure_text_width(
294 &prefix_text,
295 self.full_char_width,
296 self.char_width,
297 )
298 - self.horizontal_scroll_offset;
299
300 let cursor_y = (cursor_visual as f32 * self.line_height)
304 - self.viewport_scroll;
305
306 Rectangle::new(
307 iced::Point::new(cursor_x, cursor_y + 2.0),
308 Size::new(2.0, self.line_height - 4.0),
309 )
310 } else {
311 Rectangle::new(iced::Point::new(0.0, 0.0), Size::new(0.0, 0.0))
312 }
313 }
314
315 fn create_ime_layer(&self, cursor_rect: Rectangle) -> Element<'_, Message> {
325 let ime_enabled = self.is_focused() && self.has_canvas_focus;
326
327 let preedit =
328 self.ime_preedit.as_ref().map(|p| input_method::Preedit {
329 content: p.content.clone(),
330 selection: p.selection.clone(),
331 text_size: None,
332 });
333
334 let ime_layer = ImeRequester::new(ime_enabled, cursor_rect, preedit);
335 iced::Element::new(ime_layer)
336 }
337
338 pub fn view(&self) -> Element<'_, Message> {
343 let (visual_lines, canvas_height) = self.calculate_canvas_height();
345
346 let scrollable = self.create_canvas_with_scrollable(canvas_height);
348
349 let background_row = self.create_background_layer();
351
352 let mut editor_stack =
354 iced::widget::Stack::new().push(background_row).push(scrollable);
355
356 let cursor_rect = self.calculate_ime_cursor_rect(visual_lines.as_ref());
360 let ime_layer = self.create_ime_layer(cursor_rect);
361 editor_stack = editor_stack.push(ime_layer);
362
363 if self.search_state.is_open {
365 let search_dialog =
366 search_dialog::view(&self.search_state, &self.translations);
367
368 let positioned_dialog = container(
370 Row::new()
371 .push(Space::new().width(Length::Fill))
372 .push(search_dialog),
373 )
374 .padding(20)
375 .width(Length::Fill)
376 .height(Length::Shrink);
377
378 editor_stack = editor_stack.push(positioned_dialog);
379 }
380
381 let editor_container = container(editor_stack)
383 .width(Length::Fill)
384 .height(Length::Fill)
385 .clip(true);
386
387 let max_content_width = self.max_content_width();
389 if let Some(h_scrollbar) =
390 self.create_horizontal_scrollbar(max_content_width)
391 {
392 Column::new().push(editor_container).push(h_scrollbar).into()
393 } else {
394 editor_container.into()
395 }
396 }
397}