Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:28:49.218Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

example_editor.rs

550 lines · 17.6 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.