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}