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