Skip to repository content550 lines · 17.6 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:28:49.218Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
example_editor.rs
1//! `Editor` — the workhorse entity. It owns the cursor, blink, focus, keyboard
2//! handling, and the specialized text-shaping renderer. The *text itself* lives
3//! in a shared `Entity<String>` it's handed at construction, so the value is
4//! readable/writable from outside while the editing machinery stays in here.
5//!
6//! This is the piece that proves the point: a text input is genuinely
7//! complicated, and `View` lets all of that complexity live in one entity that
8//! anything can embed.
9
10use std::ops::Range;
11use std::time::Duration;
12
13use gpui::{
14 App, Bounds, Context, ElementInputHandler, Entity, EntityInputHandler, FocusHandle, Focusable,
15 InteractiveElement, LayoutId, PaintQuad, Pixels, ShapedLine, SharedString, Subscription, Task,
16 TextRun, UTF16Selection, Window, fill, hsla, point, prelude::*, px, relative, size,
17};
18use unicode_segmentation::*;
19
20use crate::{Backspace, Delete, End, Home, Left, Right};
21
22pub struct Editor {
23 pub value: Entity<String>,
24 pub focus_handle: FocusHandle,
25 pub cursor: usize,
26 pub cursor_visible: bool,
27 _blink_task: Task<()>,
28 _subscriptions: Vec<Subscription>,
29}
30
31impl Editor {
32 /// An editor that owns its own string internally, seeded with `text`.
33 /// Nothing to allocate or wire up at the call site.
34 pub fn new(text: impl Into<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
35 let value = cx.new(|_| text.into());
36 Self::over(value, window, cx)
37 }
38
39 /// An editor over a string *you* own, so the value is shared in and out.
40 pub fn over(value: Entity<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
41 let focus_handle = cx.focus_handle();
42
43 let focus_sub = cx.on_focus(&focus_handle, window, |this, _window, cx| {
44 this.start_blink(cx);
45 });
46 let blur_sub = cx.on_blur(&focus_handle, window, |this, _window, cx| {
47 this.stop_blink(cx);
48 });
49
50 // The value is shared: anything can write it while we hold a cursor into
51 // it. Observe it so external writes (a) clamp the cursor back onto a char
52 // boundary before the next IME round-trip can slice out of bounds, and
53 // (b) notify us, so an `editor.cached(..)` subtree re-renders — the cache
54 // is keyed on *our* notify, not the value's.
55 let value_sub = cx.observe(&value, |this, value, cx| {
56 let content = value.read(cx);
57 let mut cursor = this.cursor.min(content.len());
58 while cursor > 0 && !content.is_char_boundary(cursor) {
59 cursor -= 1;
60 }
61 this.cursor = cursor;
62 cx.notify();
63 });
64
65 Self {
66 value,
67 focus_handle,
68 cursor: 0,
69 cursor_visible: false,
70 _blink_task: Task::ready(()),
71 _subscriptions: vec![focus_sub, blur_sub, value_sub],
72 }
73 }
74
75 /// The current text. Read this from anywhere to get the value out.
76 pub fn text(&self, cx: &App) -> String {
77 self.value.read(cx).clone()
78 }
79
80 fn start_blink(&mut self, cx: &mut Context<Self>) {
81 self.cursor_visible = true;
82 self._blink_task = Self::spawn_blink_task(cx);
83 }
84
85 fn stop_blink(&mut self, cx: &mut Context<Self>) {
86 self.cursor_visible = false;
87 self._blink_task = Task::ready(());
88 cx.notify();
89 }
90
91 fn spawn_blink_task(cx: &mut Context<Self>) -> Task<()> {
92 cx.spawn(async move |this, cx| {
93 loop {
94 cx.background_executor()
95 .timer(Duration::from_millis(500))
96 .await;
97 let result = this.update(cx, |editor, cx| {
98 editor.cursor_visible = !editor.cursor_visible;
99 cx.notify();
100 });
101 if result.is_err() {
102 break;
103 }
104 }
105 })
106 }
107
108 fn reset_blink(&mut self, cx: &mut Context<Self>) {
109 self.cursor_visible = true;
110 self._blink_task = Self::spawn_blink_task(cx);
111 }
112
113 pub fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
114 let content = self.text(cx);
115 if self.cursor > 0 {
116 self.cursor = previous_boundary(&content, self.cursor);
117 }
118 self.reset_blink(cx);
119 cx.notify();
120 }
121
122 pub fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
123 let content = self.text(cx);
124 if self.cursor < content.len() {
125 self.cursor = next_boundary(&content, self.cursor);
126 }
127 self.reset_blink(cx);
128 cx.notify();
129 }
130
131 pub fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
132 self.cursor = 0;
133 self.reset_blink(cx);
134 cx.notify();
135 }
136
137 pub fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
138 self.cursor = self.text(cx).len();
139 self.reset_blink(cx);
140 cx.notify();
141 }
142
143 pub fn backspace(&mut self, _: &Backspace, _: &mut Window, cx: &mut Context<Self>) {
144 let content = self.text(cx);
145 if self.cursor > 0 {
146 let prev = previous_boundary(&content, self.cursor);
147 let cursor = self.cursor;
148 self.value.update(cx, |s, cx| {
149 s.drain(prev..cursor);
150 cx.notify();
151 });
152 self.cursor = prev;
153 }
154 self.reset_blink(cx);
155 cx.notify();
156 }
157
158 pub fn delete(&mut self, _: &Delete, _: &mut Window, cx: &mut Context<Self>) {
159 let content = self.text(cx);
160 if self.cursor < content.len() {
161 let next = next_boundary(&content, self.cursor);
162 let cursor = self.cursor;
163 self.value.update(cx, |s, cx| {
164 s.drain(cursor..next);
165 cx.notify();
166 });
167 }
168 self.reset_blink(cx);
169 cx.notify();
170 }
171
172 pub fn insert_newline(&mut self, cx: &mut Context<Self>) {
173 let cursor = self.cursor;
174 self.value.update(cx, |s, cx| {
175 s.insert(cursor, '\n');
176 cx.notify();
177 });
178 self.cursor += 1;
179 self.reset_blink(cx);
180 cx.notify();
181 }
182}
183
184fn previous_boundary(content: &str, offset: usize) -> usize {
185 content
186 .grapheme_indices(true)
187 .rev()
188 .find_map(|(idx, _)| (idx < offset).then_some(idx))
189 .unwrap_or(0)
190}
191
192fn next_boundary(content: &str, offset: usize) -> usize {
193 content
194 .grapheme_indices(true)
195 .find_map(|(idx, _)| (idx > offset).then_some(idx))
196 .unwrap_or(content.len())
197}
198
199fn offset_from_utf16(content: &str, offset: usize) -> usize {
200 let mut utf8_offset = 0;
201 let mut utf16_count = 0;
202 for ch in content.chars() {
203 if utf16_count >= offset {
204 break;
205 }
206 utf16_count += ch.len_utf16();
207 utf8_offset += ch.len_utf8();
208 }
209 utf8_offset
210}
211
212fn offset_to_utf16(content: &str, offset: usize) -> usize {
213 let mut utf16_offset = 0;
214 let mut utf8_count = 0;
215 for ch in content.chars() {
216 if utf8_count >= offset {
217 break;
218 }
219 utf8_count += ch.len_utf8();
220 utf16_offset += ch.len_utf16();
221 }
222 utf16_offset
223}
224
225fn range_to_utf16(content: &str, range: &Range<usize>) -> Range<usize> {
226 offset_to_utf16(content, range.start)..offset_to_utf16(content, range.end)
227}
228
229fn range_from_utf16(content: &str, range_utf16: &Range<usize>) -> Range<usize> {
230 offset_from_utf16(content, range_utf16.start)..offset_from_utf16(content, range_utf16.end)
231}
232
233impl Focusable for Editor {
234 fn focus_handle(&self, _cx: &App) -> FocusHandle {
235 self.focus_handle.clone()
236 }
237}
238
239impl EntityInputHandler for Editor {
240 fn text_for_range(
241 &mut self,
242 range_utf16: Range<usize>,
243 actual_range: &mut Option<Range<usize>>,
244 _window: &mut Window,
245 cx: &mut Context<Self>,
246 ) -> Option<String> {
247 let content = self.text(cx);
248 let range = range_from_utf16(&content, &range_utf16);
249 actual_range.replace(range_to_utf16(&content, &range));
250 Some(content[range].to_string())
251 }
252
253 fn selected_text_range(
254 &mut self,
255 _ignore_disabled_input: bool,
256 _window: &mut Window,
257 cx: &mut Context<Self>,
258 ) -> Option<UTF16Selection> {
259 let content = self.text(cx);
260 let utf16_cursor = offset_to_utf16(&content, self.cursor);
261 Some(UTF16Selection {
262 range: utf16_cursor..utf16_cursor,
263 reversed: false,
264 })
265 }
266
267 fn marked_text_range(
268 &self,
269 _window: &mut Window,
270 _cx: &mut Context<Self>,
271 ) -> Option<Range<usize>> {
272 None
273 }
274
275 fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
276
277 fn replace_text_in_range(
278 &mut self,
279 range_utf16: Option<Range<usize>>,
280 new_text: &str,
281 _window: &mut Window,
282 cx: &mut Context<Self>,
283 ) {
284 let content = self.text(cx);
285 let range = range_utf16
286 .as_ref()
287 .map(|r| range_from_utf16(&content, r))
288 .unwrap_or(self.cursor..self.cursor);
289
290 let new_content = content[..range.start].to_owned() + new_text + &content[range.end..];
291 self.cursor = range.start + new_text.len();
292 self.value.update(cx, |s, cx| {
293 *s = new_content;
294 cx.notify();
295 });
296 self.reset_blink(cx);
297 cx.notify();
298 }
299
300 fn replace_and_mark_text_in_range(
301 &mut self,
302 range_utf16: Option<Range<usize>>,
303 new_text: &str,
304 _new_selected_range_utf16: Option<Range<usize>>,
305 window: &mut Window,
306 cx: &mut Context<Self>,
307 ) {
308 self.replace_text_in_range(range_utf16, new_text, window, cx);
309 }
310
311 fn bounds_for_range(
312 &mut self,
313 _range_utf16: Range<usize>,
314 _bounds: Bounds<Pixels>,
315 _window: &mut Window,
316 _cx: &mut Context<Self>,
317 ) -> Option<Bounds<Pixels>> {
318 None
319 }
320
321 fn character_index_for_point(
322 &mut self,
323 _point: gpui::Point<Pixels>,
324 _window: &mut Window,
325 _cx: &mut Context<Self>,
326 ) -> Option<usize> {
327 None
328 }
329}
330
331impl gpui::Render for Editor {
332 fn render(&mut self, _window: &mut Window, cx: &mut Context<Editor>) -> impl IntoElement {
333 EditorText {
334 editor: cx.entity(),
335 }
336 }
337}
338
339// ---------------------------------------------------------------------------
340// EditorText — the specialized renderer: shapes the text and paints the cursor.
341// ---------------------------------------------------------------------------
342
343struct EditorText {
344 editor: Entity<Editor>,
345}
346
347struct EditorTextPrepaint {
348 lines: Vec<ShapedLine>,
349 cursor: Option<PaintQuad>,
350}
351
352impl IntoElement for EditorText {
353 type Element = Self;
354
355 fn into_element(self) -> Self::Element {
356 self
357 }
358}
359
360impl Element for EditorText {
361 type RequestLayoutState = ();
362 type PrepaintState = EditorTextPrepaint;
363
364 fn id(&self) -> Option<gpui::ElementId> {
365 None
366 }
367
368 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
369 None
370 }
371
372 fn request_layout(
373 &mut self,
374 _id: Option<&gpui::GlobalElementId>,
375 _inspector_id: Option<&gpui::InspectorElementId>,
376 window: &mut Window,
377 cx: &mut App,
378 ) -> (LayoutId, Self::RequestLayoutState) {
379 let editor = self.editor.read(cx);
380 let content = editor.value.read(cx);
381 let line_count = content.split('\n').count().max(1);
382 let line_height = window.line_height();
383 let mut style = gpui::Style::default();
384 style.size.width = relative(1.).into();
385 style.size.height = (line_height * line_count as f32).into();
386 (window.request_layout(style, [], cx), ())
387 }
388
389 fn prepaint(
390 &mut self,
391 _id: Option<&gpui::GlobalElementId>,
392 _inspector_id: Option<&gpui::InspectorElementId>,
393 bounds: Bounds<Pixels>,
394 _request_layout: &mut Self::RequestLayoutState,
395 window: &mut Window,
396 cx: &mut App,
397 ) -> Self::PrepaintState {
398 let editor = self.editor.read(cx);
399 let content = editor.value.read(cx).clone();
400 let cursor_offset = editor.cursor;
401 let cursor_visible = editor.cursor_visible;
402 let is_focused = editor.focus_handle.is_focused(window);
403
404 let style = window.text_style();
405 let text_color = style.color;
406 let font_size = style.font_size.to_pixels(window.rem_size());
407 let line_height = window.line_height();
408
409 let is_placeholder = content.is_empty();
410
411 let lines: Vec<ShapedLine> = if is_placeholder {
412 let placeholder: SharedString = "Type here...".into();
413 let run = TextRun {
414 len: placeholder.len(),
415 font: style.font(),
416 color: hsla(0., 0., 0.5, 0.5),
417 background_color: None,
418 underline: None,
419 strikethrough: None,
420 };
421 vec![
422 window
423 .text_system()
424 .shape_line(placeholder, font_size, &[run], None),
425 ]
426 } else {
427 content
428 .split('\n')
429 .map(|line_str| {
430 let text: SharedString = SharedString::from(line_str.to_string());
431 let run = TextRun {
432 len: text.len(),
433 font: style.font(),
434 color: text_color,
435 background_color: None,
436 underline: None,
437 strikethrough: None,
438 };
439 window
440 .text_system()
441 .shape_line(text, font_size, &[run], None)
442 })
443 .collect()
444 };
445
446 let cursor = if is_focused && cursor_visible && !is_placeholder {
447 let (cursor_line, offset_in_line) = cursor_line_and_offset(&content, cursor_offset);
448 let cursor_line = cursor_line.min(lines.len().saturating_sub(1));
449 let cursor_x = lines[cursor_line].x_for_index(offset_in_line);
450 Some(fill(
451 Bounds::new(
452 point(
453 bounds.left() + cursor_x,
454 bounds.top() + line_height * cursor_line as f32,
455 ),
456 size(px(1.5), line_height),
457 ),
458 text_color,
459 ))
460 } else if is_focused && cursor_visible && is_placeholder {
461 Some(fill(
462 Bounds::new(
463 point(bounds.left(), bounds.top()),
464 size(px(1.5), line_height),
465 ),
466 text_color,
467 ))
468 } else {
469 None
470 };
471
472 EditorTextPrepaint { lines, cursor }
473 }
474
475 fn paint(
476 &mut self,
477 _id: Option<&gpui::GlobalElementId>,
478 _inspector_id: Option<&gpui::InspectorElementId>,
479 bounds: Bounds<Pixels>,
480 _request_layout: &mut Self::RequestLayoutState,
481 prepaint: &mut Self::PrepaintState,
482 window: &mut Window,
483 cx: &mut App,
484 ) {
485 let focus_handle = self.editor.read(cx).focus_handle.clone();
486 window.handle_input(
487 &focus_handle,
488 ElementInputHandler::new(bounds, self.editor.clone()),
489 cx,
490 );
491
492 let line_height = window.line_height();
493 for (i, line) in prepaint.lines.iter().enumerate() {
494 let origin = point(bounds.left(), bounds.top() + line_height * i as f32);
495 line.paint(origin, line_height, gpui::TextAlign::Left, None, window, cx)
496 .unwrap();
497 }
498
499 if let Some(cursor) = prepaint.cursor.take() {
500 window.paint_quad(cursor);
501 }
502 }
503}
504
505fn cursor_line_and_offset(content: &str, cursor: usize) -> (usize, usize) {
506 let mut line_index = 0;
507 let mut line_start = 0;
508 for (i, ch) in content.char_indices() {
509 if i >= cursor {
510 break;
511 }
512 if ch == '\n' {
513 line_index += 1;
514 line_start = i + 1;
515 }
516 }
517 (line_index, cursor - line_start)
518}
519
520pub fn standard_actions<E: InteractiveElement>(editor: Entity<Editor>) -> impl FnOnce(E) -> E {
521 move |element| {
522 element
523 .on_action({
524 let editor = editor.clone();
525 move |a: &Left, window, cx| editor.update(cx, |e, cx| e.left(a, window, cx))
526 })
527 .on_action({
528 let editor = editor.clone();
529 move |a: &Right, window, cx| editor.update(cx, |e, cx| e.right(a, window, cx))
530 })
531 .on_action({
532 let editor = editor.clone();
533 move |a: &Home, window, cx| editor.update(cx, |e, cx| e.home(a, window, cx))
534 })
535 .on_action({
536 let editor = editor.clone();
537 move |a: &End, window, cx| editor.update(cx, |e, cx| e.end(a, window, cx))
538 })
539 .on_action({
540 let editor = editor.clone();
541 move |a: &Backspace, window, cx| {
542 editor.update(cx, |e, cx| e.backspace(a, window, cx))
543 }
544 })
545 .on_action(move |a: &Delete, window, cx| {
546 editor.update(cx, |e, cx| e.delete(a, window, cx))
547 })
548 }
549}
550