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}