example_editor.rs

  1//! The `ExampleEditor` entity — owns the truth about text content, cursor position,
  2//! blink state, and keyboard handling.
  3//!
  4//! Also contains `ExampleEditorText`, the low-level custom `Element` that shapes text
  5//! and paints the cursor.
  6use std::ops::Range;
  7use std::time::Duration;
  8
  9use gpui::{
 10    App, Bounds, Context, ElementInputHandler, Entity, EntityInputHandler, FocusHandle, Focusable,
 11    LayoutId, PaintQuad, Pixels, ShapedLine, SharedString, Task, TextRun, UTF16Selection, Window,
 12    fill, hsla, point, prelude::*, px, relative, size,
 13};
 14use unicode_segmentation::*;
 15
 16use crate::{Backspace, Delete, End, Home, Left, Right};
 17
 18pub struct ExampleEditor {
 19    pub focus_handle: FocusHandle,
 20    pub content: String,
 21    pub cursor: usize,
 22    pub cursor_visible: bool,
 23    _blink_task: Task<()>,
 24}
 25
 26impl ExampleEditor {
 27    pub fn new(cx: &mut Context<Self>) -> Self {
 28        let blink_task = Self::spawn_blink_task(cx);
 29
 30        Self {
 31            focus_handle: cx.focus_handle(),
 32            content: String::new(),
 33            cursor: 0,
 34            cursor_visible: true,
 35            _blink_task: blink_task,
 36        }
 37    }
 38
 39    fn spawn_blink_task(cx: &mut Context<Self>) -> Task<()> {
 40        cx.spawn(async move |this, cx| {
 41            loop {
 42                cx.background_executor()
 43                    .timer(Duration::from_millis(500))
 44                    .await;
 45                let result = this.update(cx, |editor, cx| {
 46                    editor.cursor_visible = !editor.cursor_visible;
 47                    cx.notify();
 48                });
 49                if result.is_err() {
 50                    break;
 51                }
 52            }
 53        })
 54    }
 55
 56    pub fn reset_blink(&mut self, cx: &mut Context<Self>) {
 57        self.cursor_visible = true;
 58        self._blink_task = Self::spawn_blink_task(cx);
 59    }
 60
 61    pub fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
 62        if self.cursor > 0 {
 63            self.cursor = self.previous_boundary(self.cursor);
 64        }
 65        self.reset_blink(cx);
 66        cx.notify();
 67    }
 68
 69    pub fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
 70        if self.cursor < self.content.len() {
 71            self.cursor = self.next_boundary(self.cursor);
 72        }
 73        self.reset_blink(cx);
 74        cx.notify();
 75    }
 76
 77    pub fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
 78        self.cursor = 0;
 79        self.reset_blink(cx);
 80        cx.notify();
 81    }
 82
 83    pub fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
 84        self.cursor = self.content.len();
 85        self.reset_blink(cx);
 86        cx.notify();
 87    }
 88
 89    pub fn backspace(&mut self, _: &Backspace, _: &mut Window, cx: &mut Context<Self>) {
 90        if self.cursor > 0 {
 91            let prev = self.previous_boundary(self.cursor);
 92            self.content.drain(prev..self.cursor);
 93            self.cursor = prev;
 94        }
 95        self.reset_blink(cx);
 96        cx.notify();
 97    }
 98
 99    pub fn delete(&mut self, _: &Delete, _: &mut Window, cx: &mut Context<Self>) {
100        if self.cursor < self.content.len() {
101            let next = self.next_boundary(self.cursor);
102            self.content.drain(self.cursor..next);
103        }
104        self.reset_blink(cx);
105        cx.notify();
106    }
107
108    pub fn insert_newline(&mut self, cx: &mut Context<Self>) {
109        self.content.insert(self.cursor, '\n');
110        self.cursor += 1;
111        self.reset_blink(cx);
112        cx.notify();
113    }
114
115    fn previous_boundary(&self, offset: usize) -> usize {
116        self.content
117            .grapheme_indices(true)
118            .rev()
119            .find_map(|(idx, _)| (idx < offset).then_some(idx))
120            .unwrap_or(0)
121    }
122
123    fn next_boundary(&self, offset: usize) -> usize {
124        self.content
125            .grapheme_indices(true)
126            .find_map(|(idx, _)| (idx > offset).then_some(idx))
127            .unwrap_or(self.content.len())
128    }
129
130    fn offset_from_utf16(&self, offset: usize) -> usize {
131        let mut utf8_offset = 0;
132        let mut utf16_count = 0;
133        for ch in self.content.chars() {
134            if utf16_count >= offset {
135                break;
136            }
137            utf16_count += ch.len_utf16();
138            utf8_offset += ch.len_utf8();
139        }
140        utf8_offset
141    }
142
143    fn offset_to_utf16(&self, offset: usize) -> usize {
144        let mut utf16_offset = 0;
145        let mut utf8_count = 0;
146        for ch in self.content.chars() {
147            if utf8_count >= offset {
148                break;
149            }
150            utf8_count += ch.len_utf8();
151            utf16_offset += ch.len_utf16();
152        }
153        utf16_offset
154    }
155
156    fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
157        self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
158    }
159
160    fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
161        self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
162    }
163}
164
165impl Focusable for ExampleEditor {
166    fn focus_handle(&self, _cx: &App) -> FocusHandle {
167        self.focus_handle.clone()
168    }
169}
170
171impl EntityInputHandler for ExampleEditor {
172    fn text_for_range(
173        &mut self,
174        range_utf16: Range<usize>,
175        actual_range: &mut Option<Range<usize>>,
176        _window: &mut Window,
177        _cx: &mut Context<Self>,
178    ) -> Option<String> {
179        let range = self.range_from_utf16(&range_utf16);
180        actual_range.replace(self.range_to_utf16(&range));
181        Some(self.content[range].to_string())
182    }
183
184    fn selected_text_range(
185        &mut self,
186        _ignore_disabled_input: bool,
187        _window: &mut Window,
188        _cx: &mut Context<Self>,
189    ) -> Option<UTF16Selection> {
190        let utf16_cursor = self.offset_to_utf16(self.cursor);
191        Some(UTF16Selection {
192            range: utf16_cursor..utf16_cursor,
193            reversed: false,
194        })
195    }
196
197    fn marked_text_range(
198        &self,
199        _window: &mut Window,
200        _cx: &mut Context<Self>,
201    ) -> Option<Range<usize>> {
202        None
203    }
204
205    fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
206
207    fn replace_text_in_range(
208        &mut self,
209        range_utf16: Option<Range<usize>>,
210        new_text: &str,
211        _window: &mut Window,
212        cx: &mut Context<Self>,
213    ) {
214        let range = range_utf16
215            .as_ref()
216            .map(|r| self.range_from_utf16(r))
217            .unwrap_or(self.cursor..self.cursor);
218
219        self.content =
220            self.content[..range.start].to_owned() + new_text + &self.content[range.end..];
221        self.cursor = range.start + new_text.len();
222        self.reset_blink(cx);
223        cx.notify();
224    }
225
226    fn replace_and_mark_text_in_range(
227        &mut self,
228        range_utf16: Option<Range<usize>>,
229        new_text: &str,
230        _new_selected_range_utf16: Option<Range<usize>>,
231        window: &mut Window,
232        cx: &mut Context<Self>,
233    ) {
234        self.replace_text_in_range(range_utf16, new_text, window, cx);
235    }
236
237    fn bounds_for_range(
238        &mut self,
239        _range_utf16: Range<usize>,
240        _bounds: Bounds<Pixels>,
241        _window: &mut Window,
242        _cx: &mut Context<Self>,
243    ) -> Option<Bounds<Pixels>> {
244        None
245    }
246
247    fn character_index_for_point(
248        &mut self,
249        _point: gpui::Point<Pixels>,
250        _window: &mut Window,
251        _cx: &mut Context<Self>,
252    ) -> Option<usize> {
253        None
254    }
255}
256
257// ---------------------------------------------------------------------------
258// ExampleEditorText — custom Element that shapes text & paints the cursor
259// ---------------------------------------------------------------------------
260
261struct ExampleEditorText {
262    editor: Entity<ExampleEditor>,
263}
264
265struct ExampleEditorTextPrepaintState {
266    lines: Vec<ShapedLine>,
267    cursor: Option<PaintQuad>,
268}
269
270impl ExampleEditorText {
271    pub fn new(editor: Entity<ExampleEditor>) -> Self {
272        Self { editor }
273    }
274}
275
276impl IntoElement for ExampleEditorText {
277    type Element = Self;
278
279    fn into_element(self) -> Self::Element {
280        self
281    }
282}
283
284impl Element for ExampleEditorText {
285    type RequestLayoutState = ();
286    type PrepaintState = ExampleEditorTextPrepaintState;
287
288    fn id(&self) -> Option<gpui::ElementId> {
289        None
290    }
291
292    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
293        None
294    }
295
296    fn request_layout(
297        &mut self,
298        _id: Option<&gpui::GlobalElementId>,
299        _inspector_id: Option<&gpui::InspectorElementId>,
300        window: &mut Window,
301        cx: &mut App,
302    ) -> (LayoutId, Self::RequestLayoutState) {
303        let line_count = self.editor.read(cx).content.split('\n').count().max(1);
304        let line_height = window.line_height();
305        let mut style = gpui::Style::default();
306        style.size.width = relative(1.).into();
307        style.size.height = (line_height * line_count as f32).into();
308        (window.request_layout(style, [], cx), ())
309    }
310
311    fn prepaint(
312        &mut self,
313        _id: Option<&gpui::GlobalElementId>,
314        _inspector_id: Option<&gpui::InspectorElementId>,
315        bounds: Bounds<Pixels>,
316        _request_layout: &mut Self::RequestLayoutState,
317        window: &mut Window,
318        cx: &mut App,
319    ) -> Self::PrepaintState {
320        let editor = self.editor.read(cx);
321        let content = &editor.content;
322        let cursor_offset = editor.cursor;
323        let cursor_visible = editor.cursor_visible;
324        let is_focused = editor.focus_handle.is_focused(window);
325
326        let style = window.text_style();
327        let text_color = style.color;
328        let font_size = style.font_size.to_pixels(window.rem_size());
329        let line_height = window.line_height();
330
331        let is_placeholder = content.is_empty();
332
333        let shaped_lines: Vec<ShapedLine> = if is_placeholder {
334            let placeholder: SharedString = "Type here...".into();
335            let run = TextRun {
336                len: placeholder.len(),
337                font: style.font(),
338                color: hsla(0., 0., 0.5, 0.5),
339                background_color: None,
340                underline: None,
341                strikethrough: None,
342            };
343            vec![
344                window
345                    .text_system()
346                    .shape_line(placeholder, font_size, &[run], None),
347            ]
348        } else {
349            content
350                .split('\n')
351                .map(|line_str| {
352                    let text: SharedString = SharedString::from(line_str.to_string());
353                    let run = TextRun {
354                        len: text.len(),
355                        font: style.font(),
356                        color: text_color,
357                        background_color: None,
358                        underline: None,
359                        strikethrough: None,
360                    };
361                    window
362                        .text_system()
363                        .shape_line(text, font_size, &[run], None)
364                })
365                .collect()
366        };
367
368        let cursor = if is_focused && cursor_visible && !is_placeholder {
369            let (cursor_line, offset_in_line) = cursor_line_and_offset(content, cursor_offset);
370            let cursor_line = cursor_line.min(shaped_lines.len().saturating_sub(1));
371            let cursor_x = shaped_lines[cursor_line].x_for_index(offset_in_line);
372
373            Some(fill(
374                Bounds::new(
375                    point(
376                        bounds.left() + cursor_x,
377                        bounds.top() + line_height * cursor_line as f32,
378                    ),
379                    size(px(1.5), line_height),
380                ),
381                text_color,
382            ))
383        } else if is_focused && cursor_visible && is_placeholder {
384            Some(fill(
385                Bounds::new(
386                    point(bounds.left(), bounds.top()),
387                    size(px(1.5), line_height),
388                ),
389                text_color,
390            ))
391        } else {
392            None
393        };
394
395        ExampleEditorTextPrepaintState {
396            lines: shaped_lines,
397            cursor,
398        }
399    }
400
401    fn paint(
402        &mut self,
403        _id: Option<&gpui::GlobalElementId>,
404        _inspector_id: Option<&gpui::InspectorElementId>,
405        bounds: Bounds<Pixels>,
406        _request_layout: &mut Self::RequestLayoutState,
407        prepaint: &mut Self::PrepaintState,
408        window: &mut Window,
409        cx: &mut App,
410    ) {
411        let focus_handle = self.editor.read(cx).focus_handle.clone();
412
413        window.handle_input(
414            &focus_handle,
415            ElementInputHandler::new(bounds, self.editor.clone()),
416            cx,
417        );
418
419        let line_height = window.line_height();
420        for (i, line) in prepaint.lines.iter().enumerate() {
421            let origin = point(bounds.left(), bounds.top() + line_height * i as f32);
422            line.paint(origin, line_height, gpui::TextAlign::Left, None, window, cx)
423                .unwrap();
424        }
425
426        if let Some(cursor) = prepaint.cursor.take() {
427            window.paint_quad(cursor);
428        }
429    }
430}
431
432fn cursor_line_and_offset(content: &str, cursor: usize) -> (usize, usize) {
433    let mut line_index = 0;
434    let mut line_start = 0;
435    for (i, ch) in content.char_indices() {
436        if i >= cursor {
437            break;
438        }
439        if ch == '\n' {
440            line_index += 1;
441            line_start = i + 1;
442        }
443    }
444    (line_index, cursor - line_start)
445}
446
447impl gpui::EntityView for ExampleEditor {
448    fn render(
449        &mut self,
450        _window: &mut Window,
451        cx: &mut Context<ExampleEditor>,
452    ) -> impl IntoElement {
453        ExampleEditorText::new(cx.entity().clone())
454    }
455}