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}