iced_code_editor/canvas_editor/
view.rs1use iced::Size;
4use iced::advanced::input_method;
5use iced::widget::canvas::Canvas;
6use iced::widget::{
7 Column, Row, Scrollable, Space, container, scrollable, text,
8};
9use iced::{Background, Border, Color, Element, Length, Rectangle, Shadow};
10use iced_aw::ContextMenu;
11
12use super::context_menu;
13use super::goto_line_dialog;
14use super::ime_requester::ImeRequester;
15use super::search_dialog;
16use super::wrapping::{self, WrappingCalculator};
17use super::{CodeEditor, GUTTER_WIDTH, Message};
18use std::rc::Rc;
19
20impl CodeEditor {
21 fn calculate_canvas_height(&self) -> (Rc<Vec<wrapping::VisualLine>>, f32) {
27 let visual_lines = self.visual_lines_cached(self.viewport_width);
30 let total_visual_lines = visual_lines.len();
31 let content_height = total_visual_lines as f32 * self.line_height;
32
33 let canvas_height = content_height.max(self.viewport_height);
37
38 (visual_lines, canvas_height)
39 }
40
41 fn create_scrollable_style(
45 &self,
46 ) -> impl Fn(&iced::Theme, scrollable::Status) -> scrollable::Style {
47 let scrollbar_bg = self.style.scrollbar_background;
48 let scroller_color = self.style.scroller_color;
49
50 move |_theme, _status| scrollable::Style {
51 container: container::Style {
52 background: Some(Background::Color(Color::TRANSPARENT)),
53 ..container::Style::default()
54 },
55 vertical_rail: scrollable::Rail {
56 background: Some(scrollbar_bg.into()),
57 border: Border {
58 radius: 4.0.into(),
59 width: 0.0,
60 color: Color::TRANSPARENT,
61 },
62 scroller: scrollable::Scroller {
63 background: scroller_color.into(),
64 border: Border {
65 radius: 4.0.into(),
66 width: 0.0,
67 color: Color::TRANSPARENT,
68 },
69 },
70 },
71 horizontal_rail: scrollable::Rail {
72 background: Some(scrollbar_bg.into()),
73 border: Border {
74 radius: 4.0.into(),
75 width: 0.0,
76 color: Color::TRANSPARENT,
77 },
78 scroller: scrollable::Scroller {
79 background: scroller_color.into(),
80 border: Border {
81 radius: 4.0.into(),
82 width: 0.0,
83 color: Color::TRANSPARENT,
84 },
85 },
86 },
87 gap: None,
88 auto_scroll: scrollable::AutoScroll {
89 background: Color::TRANSPARENT.into(),
90 border: Border::default(),
91 shadow: Shadow::default(),
92 icon: Color::TRANSPARENT,
93 },
94 }
95 }
96
97 fn create_canvas_with_scrollable(
107 &self,
108 canvas_height: f32,
109 ) -> Scrollable<'_, Message> {
110 let canvas = Canvas::new(self)
111 .width(Length::Fill)
112 .height(Length::Fixed(canvas_height));
113
114 Scrollable::new(canvas)
115 .id(self.scrollable_id.clone())
116 .width(Length::Fill)
117 .height(Length::Fill)
118 .on_scroll(Message::Scrolled)
119 .style(self.create_scrollable_style())
120 }
121
122 fn create_horizontal_scrollbar(
132 &self,
133 max_content_width: f32,
134 ) -> Option<Element<'_, Message>> {
135 if self.wrap_enabled || max_content_width <= self.viewport_width {
136 return None;
137 }
138
139 let scrollbar_bg = self.style.scrollbar_background;
140 let scroller_color = self.style.scroller_color;
141
142 let h_scrollable = Scrollable::new(
143 Space::new().width(Length::Fixed(max_content_width)).height(0.0),
144 )
145 .id(self.horizontal_scrollable_id.clone())
146 .width(Length::Fill)
147 .height(Length::Fixed(12.0))
148 .direction(scrollable::Direction::Horizontal(
149 scrollable::Scrollbar::new(),
150 ))
151 .on_scroll(Message::HorizontalScrolled)
152 .style(move |_theme, _status| scrollable::Style {
153 container: container::Style {
154 background: Some(Background::Color(Color::TRANSPARENT)),
155 ..container::Style::default()
156 },
157 vertical_rail: scrollable::Rail {
158 background: Some(scrollbar_bg.into()),
159 border: Border {
160 radius: 4.0.into(),
161 width: 0.0,
162 color: Color::TRANSPARENT,
163 },
164 scroller: scrollable::Scroller {
165 background: scroller_color.into(),
166 border: Border {
167 radius: 4.0.into(),
168 width: 0.0,
169 color: Color::TRANSPARENT,
170 },
171 },
172 },
173 horizontal_rail: scrollable::Rail {
174 background: Some(scrollbar_bg.into()),
175 border: Border {
176 radius: 4.0.into(),
177 width: 0.0,
178 color: Color::TRANSPARENT,
179 },
180 scroller: scrollable::Scroller {
181 background: scroller_color.into(),
182 border: Border {
183 radius: 4.0.into(),
184 width: 0.0,
185 color: Color::TRANSPARENT,
186 },
187 },
188 },
189 gap: None,
190 auto_scroll: scrollable::AutoScroll {
191 background: Color::TRANSPARENT.into(),
192 border: Border::default(),
193 shadow: Shadow::default(),
194 icon: Color::TRANSPARENT,
195 },
196 });
197
198 Some(h_scrollable.into())
199 }
200
201 fn create_gutter_container(
207 &self,
208 ) -> Option<container::Container<'_, Message>> {
209 if self.line_numbers_enabled {
210 let gutter_background = self.style.gutter_background;
211 Some(
212 container(
213 Space::new().width(Length::Fill).height(Length::Fill),
214 )
215 .width(Length::Fixed(GUTTER_WIDTH))
216 .height(Length::Fill)
217 .style(move |_| container::Style {
218 background: Some(Background::Color(gutter_background)),
219 ..container::Style::default()
220 }),
221 )
222 } else {
223 None
224 }
225 }
226
227 fn create_code_background_container(
233 &self,
234 ) -> container::Container<'_, Message> {
235 let background_color = self.style.background;
236 container(Space::new().width(Length::Fill).height(Length::Fill))
237 .width(Length::Fill)
238 .height(Length::Fill)
239 .style(move |_| container::Style {
240 background: Some(Background::Color(background_color)),
241 ..container::Style::default()
242 })
243 }
244
245 fn create_vim_status_bar(&self) -> Element<'_, Message> {
247 let (left_text, right_text) = self.vim_state.status_line_text();
248 let background = self.style.gutter_background;
249 let text_color = self.style.text_color;
250
251 container(
252 Row::new()
253 .push(
254 text(left_text).size(self.font_size).style(move |_| {
255 text::Style { color: Some(text_color) }
256 }),
257 )
258 .push(Space::new().width(Length::Fill))
259 .push(
260 text(right_text).size(self.font_size).style(move |_| {
261 text::Style { color: Some(text_color) }
262 }),
263 ),
264 )
265 .padding([2, 8])
266 .width(Length::Fill)
267 .height(Length::Fixed(self.line_height.max(20.0)))
268 .style(move |_| container::Style {
269 background: Some(Background::Color(background)),
270 ..container::Style::default()
271 })
272 .into()
273 }
274
275 fn create_background_layer(&self) -> Row<'_, Message> {
281 let gutter_container = self.create_gutter_container();
282 let code_background_container = self.create_code_background_container();
283
284 if let Some(gutter) = gutter_container {
285 Row::new().push(gutter).push(code_background_container)
286 } else {
287 Row::new().push(code_background_container)
288 }
289 }
290
291 fn calculate_ime_cursor_rect(
301 &self,
302 visual_lines: &[wrapping::VisualLine],
303 ) -> Rectangle {
304 let ime_enabled = self.is_focused() && self.has_canvas_focus;
305
306 if !ime_enabled {
307 return Rectangle::new(
308 iced::Point::new(0.0, 0.0),
309 Size::new(0.0, 0.0),
310 );
311 }
312
313 if let Some(cursor_visual) = WrappingCalculator::logical_to_visual(
314 visual_lines,
315 self.cursors.primary_position().0,
316 self.cursors.primary_position().1,
317 ) {
318 let vl = &visual_lines[cursor_visual];
319 let line_content = self.buffer.line(vl.logical_line);
320 let prefix_len =
321 self.cursors.primary_position().1.saturating_sub(vl.start_col);
322 let prefix_text: String = line_content
323 .chars()
324 .skip(vl.start_col)
325 .take(prefix_len)
326 .collect();
327 let cursor_x = self.gutter_width()
328 + 5.0
329 + super::measure_text_width(
330 &prefix_text,
331 self.full_char_width,
332 self.char_width,
333 )
334 - self.horizontal_scroll_offset;
335
336 let cursor_y = (cursor_visual as f32 * self.line_height)
340 - self.viewport_scroll;
341
342 Rectangle::new(
343 iced::Point::new(cursor_x, cursor_y + 2.0),
344 Size::new(2.0, self.line_height - 4.0),
345 )
346 } else {
347 Rectangle::new(iced::Point::new(0.0, 0.0), Size::new(0.0, 0.0))
348 }
349 }
350
351 fn create_ime_layer(&self, cursor_rect: Rectangle) -> Element<'_, Message> {
361 let ime_enabled = self.is_focused() && self.has_canvas_focus;
362
363 let preedit =
364 self.ime_preedit.as_ref().map(|p| input_method::Preedit {
365 content: p.content.clone(),
366 selection: p.selection.clone(),
367 text_size: None,
368 });
369
370 let ime_layer = ImeRequester::new(ime_enabled, cursor_rect, preedit);
371 iced::Element::new(ime_layer)
372 }
373
374 pub fn view(&self) -> Element<'_, Message> {
379 let (visual_lines, canvas_height) = self.calculate_canvas_height();
381
382 let scrollable = self.create_canvas_with_scrollable(canvas_height);
384
385 let background_row = self.create_background_layer();
387
388 let mut editor_stack =
390 iced::widget::Stack::new().push(background_row).push(scrollable);
391
392 let cursor_rect = self.calculate_ime_cursor_rect(visual_lines.as_ref());
396 let ime_layer = self.create_ime_layer(cursor_rect);
397 editor_stack = editor_stack.push(ime_layer);
398
399 if self.search_state.is_open {
401 let search_dialog =
402 search_dialog::view(&self.search_state, &self.translations);
403
404 let positioned_dialog = container(
406 Row::new()
407 .push(Space::new().width(Length::Fill))
408 .push(search_dialog),
409 )
410 .padding(20)
411 .width(Length::Fill)
412 .height(Length::Shrink);
413
414 editor_stack = editor_stack.push(positioned_dialog);
415 }
416
417 if self.goto_line_state.is_open {
419 let goto_line_dialog = goto_line_dialog::view(
420 &self.goto_line_state,
421 self.buffer.line_count(),
422 );
423 let positioned_dialog = container(
424 Row::new()
425 .push(Space::new().width(Length::Fill))
426 .push(goto_line_dialog)
427 .push(Space::new().width(Length::Fill)),
428 )
429 .padding(20)
430 .width(Length::Fill)
431 .height(Length::Shrink);
432
433 editor_stack = editor_stack.push(positioned_dialog);
434 }
435
436 let editor_container = container(editor_stack)
438 .width(Length::Fill)
439 .height(Length::Fill)
440 .clip(true);
441
442 let can_undo = self.history.can_undo();
446 let can_redo = self.history.can_redo();
447 let has_selection =
448 self.cursors.iter().any(|cursor| cursor.has_selection());
449 let has_content =
450 self.buffer.line_count() > 1 || self.buffer.line_len(0) > 0;
451 let custom_context_menu_entries =
452 self.custom_context_menu_entries().to_vec();
453 let default_context_menu_enabled = self.default_context_menu_enabled();
454 let reveal_in_file_manager_enabled =
455 self.reveal_in_file_manager_enabled();
456 let translations = self.translations;
457 let editor_container = ContextMenu::new(editor_container, move || {
458 context_menu::view(
459 &custom_context_menu_entries,
460 default_context_menu_enabled,
461 context_menu::MenuState {
462 can_undo,
463 can_redo,
464 has_selection,
465 has_content,
466 reveal_in_file_manager_enabled,
467 },
468 translations,
469 )
470 });
471
472 let editor_body: Element<'_, Message> = if self.wrap_enabled {
474 editor_container.into()
475 } else {
476 let max_content_width = self.max_content_width();
481 if let Some(h_scrollbar) =
482 self.create_horizontal_scrollbar(max_content_width)
483 {
484 Column::new().push(editor_container).push(h_scrollbar).into()
485 } else {
486 editor_container.into()
487 }
488 };
489
490 if self.vim_enabled {
491 Column::new()
492 .push(editor_body)
493 .push(self.create_vim_status_bar())
494 .width(Length::Fill)
495 .height(Length::Fill)
496 .into()
497 } else {
498 editor_body
499 }
500 }
501}