gpui_editor/
element.rs

1//! GPUI Element implementation for rendering an Editor
2
3use crate::buffer::TextBuffer;
4use crate::editor::Editor;
5use gpui::*;
6
7/// A GPUI Element that renders an Editor
8pub struct EditorElement {
9    editor: Editor,
10}
11
12impl EditorElement {
13    /// Create a new EditorElement from an Editor
14    pub fn new(editor: Editor) -> Self {
15        Self { editor }
16    }
17
18    /// Get a reference to the underlying Editor
19    pub fn editor(&self) -> &Editor {
20        &self.editor
21    }
22
23    /// Get a mutable reference to the underlying Editor
24    pub fn editor_mut(&mut self) -> &mut Editor {
25        &mut self.editor
26    }
27
28    fn line_bounds(&self, row: usize, bounds: Bounds<Pixels>) -> Bounds<Pixels> {
29        let config = self.editor.config();
30        Bounds {
31            origin: point(
32                bounds.origin.x + config.gutter_width,
33                bounds.origin.y + config.line_height * row as f32,
34            ),
35            size: size(bounds.size.width - config.gutter_width, config.line_height),
36        }
37    }
38
39    fn cursor_position_px(&self, bounds: Bounds<Pixels>, window: &mut Window) -> Point<Pixels> {
40        let config = self.editor.config();
41        let cursor_pos = self.editor.get_cursor_position();
42        let line = self
43            .editor
44            .get_buffer()
45            .get_line(cursor_pos.row)
46            .unwrap_or_else(|| String::new());
47
48        let text_before_cursor = &line[..cursor_pos.col.min(line.len())];
49        let text_x = bounds.origin.x + config.gutter_width + config.gutter_padding;
50
51        let offset_x = if !text_before_cursor.is_empty() {
52            let shaped = window.text_system().shape_line(
53                SharedString::from(text_before_cursor.to_string()),
54                config.font_size,
55                &[TextRun {
56                    len: text_before_cursor.len(),
57                    font: Font {
58                        family: config.font_family.clone(),
59                        features: Default::default(),
60                        weight: FontWeight::NORMAL,
61                        style: FontStyle::Normal,
62                        fallbacks: Default::default(),
63                    },
64                    color: config.text_color.into(),
65                    background_color: None,
66                    underline: None,
67                    strikethrough: None,
68                }],
69                None,
70            );
71            shaped.width
72        } else {
73            px(0.0)
74        };
75
76        point(
77            text_x + offset_x,
78            bounds.origin.y + config.line_height * cursor_pos.row as f32,
79        )
80    }
81
82    fn paint_editor_background(&self, window: &mut Window, bounds: Bounds<Pixels>) {
83        let config = self.editor.config();
84        let bg_color: Hsla = config.editor_bg_color.into();
85
86        if bg_color.is_opaque() {
87            let editor_bounds = Bounds {
88                origin: point(bounds.origin.x + config.gutter_width, bounds.origin.y),
89                size: size(bounds.size.width - config.gutter_width, bounds.size.height),
90            };
91            window.paint_quad(PaintQuad {
92                bounds: editor_bounds,
93                corner_radii: (0.0).into(),
94                background: config.editor_bg_color.into(),
95                border_color: transparent_black(),
96                border_widths: (0.0).into(),
97                border_style: BorderStyle::Solid,
98            });
99        }
100    }
101
102    fn paint_gutter_background(&self, window: &mut Window, bounds: Bounds<Pixels>) {
103        let config = self.editor.config();
104        let bg_color: Hsla = config.gutter_bg_color.into();
105
106        if bg_color.is_opaque() {
107            let gutter_bounds = Bounds {
108                origin: bounds.origin,
109                size: size(config.gutter_width, bounds.size.height),
110            };
111            window.paint_quad(PaintQuad {
112                bounds: gutter_bounds,
113                corner_radii: (0.0).into(),
114                background: config.gutter_bg_color.into(),
115                border_color: transparent_black(),
116                border_widths: (0.0).into(),
117                border_style: BorderStyle::Solid,
118            });
119        }
120    }
121
122    fn paint_active_line_background(&self, window: &mut Window, bounds: Bounds<Pixels>) {
123        let config = self.editor.config();
124        let cursor_pos = self.editor.get_cursor_position();
125        let bg_color: Hsla = config.active_line_bg_color.into();
126
127        if bg_color.is_opaque() {
128            let active_line_bounds = self.line_bounds(cursor_pos.row, bounds);
129            window.paint_quad(PaintQuad {
130                bounds: active_line_bounds,
131                corner_radii: (0.0).into(),
132                background: config.active_line_bg_color.into(),
133                border_color: transparent_black(),
134                border_widths: (0.0).into(),
135                border_style: BorderStyle::Solid,
136            });
137        }
138    }
139
140    fn paint_selection(&self, window: &mut Window, bounds: Bounds<Pixels>) {
141        let config = self.editor.config();
142
143        if let Some((start, end)) = self.editor.get_selection_range() {
144            let selection_color = rgba(0x264f78ff);
145
146            for row in start.row..=end.row {
147                if let Some(line) = self.editor.get_buffer().get_line(row) {
148                    let line_bounds = self.line_bounds(row, bounds);
149
150                    let start_col = if row == start.row { start.col } else { 0 };
151                    let end_col = if row == end.row { end.col } else { line.len() };
152
153                    let text_x_start = line_bounds.origin.x + config.gutter_padding;
154
155                    let start_x = if start_col > 0 {
156                        let text_before =
157                            SharedString::from(line[..start_col.min(line.len())].to_string());
158                        let shaped = window.text_system().shape_line(
159                            text_before.clone(),
160                            config.font_size,
161                            &[TextRun {
162                                len: text_before.len(),
163                                font: Font {
164                                    family: config.font_family.clone(),
165                                    features: Default::default(),
166                                    weight: FontWeight::NORMAL,
167                                    style: FontStyle::Normal,
168                                    fallbacks: Default::default(),
169                                },
170                                color: config.text_color.into(),
171                                background_color: None,
172                                underline: None,
173                                strikethrough: None,
174                            }],
175                            None,
176                        );
177                        shaped.width
178                    } else {
179                        px(0.0)
180                    };
181
182                    let end_x = if end_col > 0 {
183                        let text_to_end =
184                            SharedString::from(line[..end_col.min(line.len())].to_string());
185                        let shaped = window.text_system().shape_line(
186                            text_to_end.clone(),
187                            config.font_size,
188                            &[TextRun {
189                                len: text_to_end.len(),
190                                font: Font {
191                                    family: config.font_family.clone(),
192                                    features: Default::default(),
193                                    weight: FontWeight::NORMAL,
194                                    style: FontStyle::Normal,
195                                    fallbacks: Default::default(),
196                                },
197                                color: config.text_color.into(),
198                                background_color: None,
199                                underline: None,
200                                strikethrough: None,
201                            }],
202                            None,
203                        );
204                        shaped.width
205                    } else {
206                        px(0.0)
207                    };
208
209                    let selection_bounds = Bounds {
210                        origin: point(text_x_start + start_x, line_bounds.origin.y),
211                        size: size(end_x - start_x, config.line_height),
212                    };
213
214                    window.paint_quad(PaintQuad {
215                        bounds: selection_bounds,
216                        corner_radii: (0.0).into(),
217                        background: selection_color.into(),
218                        border_color: transparent_black(),
219                        border_widths: (0.0).into(),
220                        border_style: BorderStyle::Solid,
221                    });
222                }
223            }
224        }
225    }
226
227    fn paint_lines(&mut self, cx: &mut App, window: &mut Window, bounds: Bounds<Pixels>) {
228        let _config = self.editor.config();
229        let lines = self.editor.get_buffer().all_lines();
230
231        for (i, line) in lines.iter().enumerate() {
232            let line_bounds = self.line_bounds(i, bounds);
233            self.paint_line_number(cx, window, i + 1, line_bounds, bounds);
234            self.paint_line(cx, window, line, i, line_bounds);
235        }
236    }
237
238    fn paint_line_number(
239        &self,
240        cx: &mut App,
241        window: &mut Window,
242        line_number: usize,
243        line_bounds: Bounds<Pixels>,
244        editor_bounds: Bounds<Pixels>,
245    ) {
246        let config = self.editor.config();
247        let line_number_str = SharedString::new(line_number.to_string());
248        let line_number_len = line_number_str.len();
249        let gutter_padding = px(10.0);
250        let line_number_x =
251            editor_bounds.origin.x + config.gutter_width - gutter_padding - px(20.0);
252
253        let shaped_line_number = window.text_system().shape_line(
254            line_number_str,
255            config.font_size,
256            &[TextRun {
257                len: line_number_len,
258                font: Font {
259                    family: config.font_family.clone(),
260                    features: Default::default(),
261                    weight: FontWeight::NORMAL,
262                    style: FontStyle::Normal,
263                    fallbacks: Default::default(),
264                },
265                color: config.line_number_color.into(),
266                background_color: None,
267                underline: None,
268                strikethrough: None,
269            }],
270            None,
271        );
272
273        let _ = shaped_line_number.paint(
274            point(line_number_x, line_bounds.origin.y),
275            config.line_height,
276            window,
277            cx,
278        );
279    }
280
281    fn paint_line(
282        &mut self,
283        cx: &mut App,
284        window: &mut Window,
285        line: impl Into<SharedString>,
286        line_index: usize,
287        line_bounds: Bounds<Pixels>,
288    ) {
289        let gutter_padding = px(10.0);
290        let text_x = line_bounds.origin.x + gutter_padding;
291        let line = line.into();
292
293        // Get syntax highlighted text runs
294        let config = self.editor.config();
295        let font_family = config.font_family.clone();
296        let font_size = config.font_size;
297        let line_height = config.line_height;
298        let font_size_f32: f32 = font_size.into();
299        let text_runs = self
300            .editor
301            .highlight_line(&line, line_index, font_family, font_size_f32);
302
303        let shaped_line =
304            window
305                .text_system()
306                .shape_line(line.clone(), font_size, &text_runs, None);
307
308        let _ = shaped_line.paint(point(text_x, line_bounds.origin.y), line_height, window, cx);
309    }
310
311    fn paint_cursor(&self, window: &mut Window, bounds: Bounds<Pixels>) {
312        let config = self.editor.config();
313        let cursor_pos = self.cursor_position_px(bounds, window);
314        let cursor_bounds = Bounds {
315            origin: cursor_pos,
316            size: size(px(2.0), config.line_height),
317        };
318
319        window.paint_quad(PaintQuad {
320            bounds: cursor_bounds,
321            corner_radii: (0.0).into(),
322            background: rgb(0xffffff).into(),
323            border_color: transparent_black(),
324            border_widths: (0.0).into(),
325            border_style: BorderStyle::Solid,
326        });
327    }
328}
329
330impl IntoElement for EditorElement {
331    type Element = Self;
332
333    fn into_element(self) -> Self::Element {
334        self
335    }
336}
337
338impl Element for EditorElement {
339    type RequestLayoutState = ();
340    type PrepaintState = ();
341
342    fn id(&self) -> Option<ElementId> {
343        Some(self.editor.id().clone())
344    }
345
346    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
347        None
348    }
349
350    fn request_layout(
351        &mut self,
352        _: Option<&GlobalElementId>,
353        _: Option<&gpui::InspectorElementId>,
354        window: &mut Window,
355        cx: &mut App,
356    ) -> (LayoutId, Self::RequestLayoutState) {
357        let mut style = Style::default();
358        style.flex_grow = 1.0;
359        style.size.width = relative(1.0).into();
360        style.size.height = relative(1.0).into();
361        let layout_id = window.request_layout(style, None, cx);
362        (layout_id, ())
363    }
364
365    fn prepaint(
366        &mut self,
367        _: Option<&GlobalElementId>,
368        _: Option<&gpui::InspectorElementId>,
369        _: Bounds<Pixels>,
370        _: &mut Self::RequestLayoutState,
371        _window: &mut Window,
372        _cx: &mut App,
373    ) -> Self::PrepaintState {
374        ()
375    }
376
377    fn paint(
378        &mut self,
379        _: Option<&GlobalElementId>,
380        _: Option<&gpui::InspectorElementId>,
381        bounds: Bounds<Pixels>,
382        _: &mut Self::RequestLayoutState,
383        _: &mut Self::PrepaintState,
384        window: &mut Window,
385        cx: &mut App,
386    ) {
387        self.paint_gutter_background(window, bounds);
388        self.paint_editor_background(window, bounds);
389        self.paint_active_line_background(window, bounds);
390        self.paint_selection(window, bounds);
391        self.paint_lines(cx, window, bounds);
392        self.paint_cursor(window, bounds);
393    }
394}