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