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.cursors.primary_position().0,
281 self.cursors.primary_position().1,
282 ) {
283 let vl = &visual_lines[cursor_visual];
284 let line_content = self.buffer.line(vl.logical_line);
285 let prefix_len =
286 self.cursors.primary_position().1.saturating_sub(vl.start_col);
287 let prefix_text: String = line_content
288 .chars()
289 .skip(vl.start_col)
290 .take(prefix_len)
291 .collect();
292 let cursor_x = self.gutter_width()
293 + 5.0
294 + super::measure_text_width(
295 &prefix_text,
296 self.full_char_width,
297 self.char_width,
298 )
299 - self.horizontal_scroll_offset;
300
301 let cursor_y = (cursor_visual as f32 * self.line_height)
305 - self.viewport_scroll;
306
307 Rectangle::new(
308 iced::Point::new(cursor_x, cursor_y + 2.0),
309 Size::new(2.0, self.line_height - 4.0),
310 )
311 } else {
312 Rectangle::new(iced::Point::new(0.0, 0.0), Size::new(0.0, 0.0))
313 }
314 }
315
316 fn create_ime_layer(&self, cursor_rect: Rectangle) -> Element<'_, Message> {
326 let ime_enabled = self.is_focused() && self.has_canvas_focus;
327
328 let preedit =
329 self.ime_preedit.as_ref().map(|p| input_method::Preedit {
330 content: p.content.clone(),
331 selection: p.selection.clone(),
332 text_size: None,
333 });
334
335 let ime_layer = ImeRequester::new(ime_enabled, cursor_rect, preedit);
336 iced::Element::new(ime_layer)
337 }
338
339 pub fn view(&self) -> Element<'_, Message> {
344 let (visual_lines, canvas_height) = self.calculate_canvas_height();
346
347 let scrollable = self.create_canvas_with_scrollable(canvas_height);
349
350 let background_row = self.create_background_layer();
352
353 let mut editor_stack =
355 iced::widget::Stack::new().push(background_row).push(scrollable);
356
357 let cursor_rect = self.calculate_ime_cursor_rect(visual_lines.as_ref());
361 let ime_layer = self.create_ime_layer(cursor_rect);
362 editor_stack = editor_stack.push(ime_layer);
363
364 if self.search_state.is_open {
366 let search_dialog =
367 search_dialog::view(&self.search_state, &self.translations);
368
369 let positioned_dialog = container(
371 Row::new()
372 .push(Space::new().width(Length::Fill))
373 .push(search_dialog),
374 )
375 .padding(20)
376 .width(Length::Fill)
377 .height(Length::Shrink);
378
379 editor_stack = editor_stack.push(positioned_dialog);
380 }
381
382 let editor_container = container(editor_stack)
384 .width(Length::Fill)
385 .height(Length::Fill)
386 .clip(true);
387
388 let max_content_width = self.max_content_width();
390 if let Some(h_scrollbar) =
391 self.create_horizontal_scrollbar(max_content_width)
392 {
393 Column::new().push(editor_container).push(h_scrollbar).into()
394 } else {
395 editor_container.into()
396 }
397 }
398}