input.rs

  1#![cfg_attr(target_family = "wasm", no_main)]
  2
  3use std::ops::Range;
  4
  5use gpui::{
  6    App, Bounds, ClipboardItem, Context, CursorStyle, ElementId, ElementInputHandler, Entity,
  7    EntityInputHandler, FocusHandle, Focusable, GlobalElementId, KeyBinding, Keystroke, LayoutId,
  8    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
  9    ShapedLine, SharedString, Style, TextRun, UTF16Selection, UnderlineStyle, Window, WindowBounds,
 10    WindowOptions, actions, black, div, fill, hsla, opaque_grey, point, prelude::*, px, relative,
 11    rgb, rgba, size, white, yellow,
 12};
 13use gpui_platform::application;
 14use unicode_segmentation::*;
 15
 16actions!(
 17    text_input,
 18    [
 19        Backspace,
 20        Delete,
 21        Left,
 22        Right,
 23        SelectLeft,
 24        SelectRight,
 25        SelectAll,
 26        Home,
 27        End,
 28        ShowCharacterPalette,
 29        Paste,
 30        Cut,
 31        Copy,
 32        Quit,
 33    ]
 34);
 35
 36struct TextInput {
 37    focus_handle: FocusHandle,
 38    content: SharedString,
 39    placeholder: SharedString,
 40    selected_range: Range<usize>,
 41    selection_reversed: bool,
 42    marked_range: Option<Range<usize>>,
 43    last_layout: Option<ShapedLine>,
 44    last_bounds: Option<Bounds<Pixels>>,
 45    is_selecting: bool,
 46}
 47
 48impl TextInput {
 49    fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
 50        if self.selected_range.is_empty() {
 51            self.move_to(self.previous_boundary(self.cursor_offset()), cx);
 52        } else {
 53            self.move_to(self.selected_range.start, cx)
 54        }
 55    }
 56
 57    fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
 58        if self.selected_range.is_empty() {
 59            self.move_to(self.next_boundary(self.selected_range.end), cx);
 60        } else {
 61            self.move_to(self.selected_range.end, cx)
 62        }
 63    }
 64
 65    fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
 66        self.select_to(self.previous_boundary(self.cursor_offset()), cx);
 67    }
 68
 69    fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
 70        self.select_to(self.next_boundary(self.cursor_offset()), cx);
 71    }
 72
 73    fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
 74        self.move_to(0, cx);
 75        self.select_to(self.content.len(), cx)
 76    }
 77
 78    fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
 79        self.move_to(0, cx);
 80    }
 81
 82    fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
 83        self.move_to(self.content.len(), cx);
 84    }
 85
 86    fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
 87        if self.selected_range.is_empty() {
 88            self.select_to(self.previous_boundary(self.cursor_offset()), cx)
 89        }
 90        self.replace_text_in_range(None, "", window, cx)
 91    }
 92
 93    fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
 94        if self.selected_range.is_empty() {
 95            self.select_to(self.next_boundary(self.cursor_offset()), cx)
 96        }
 97        self.replace_text_in_range(None, "", window, cx)
 98    }
 99
100    fn on_mouse_down(
101        &mut self,
102        event: &MouseDownEvent,
103        _window: &mut Window,
104        cx: &mut Context<Self>,
105    ) {
106        self.is_selecting = true;
107
108        if event.modifiers.shift {
109            self.select_to(self.index_for_mouse_position(event.position), cx);
110        } else {
111            self.move_to(self.index_for_mouse_position(event.position), cx)
112        }
113    }
114
115    fn on_mouse_up(&mut self, _: &MouseUpEvent, _window: &mut Window, _: &mut Context<Self>) {
116        self.is_selecting = false;
117    }
118
119    fn on_mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) {
120        if self.is_selecting {
121            self.select_to(self.index_for_mouse_position(event.position), cx);
122        }
123    }
124
125    fn show_character_palette(
126        &mut self,
127        _: &ShowCharacterPalette,
128        window: &mut Window,
129        _: &mut Context<Self>,
130    ) {
131        window.show_character_palette();
132    }
133
134    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
135        if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
136            self.replace_text_in_range(None, &text.replace("\n", " "), window, cx);
137        }
138    }
139
140    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
141        if !self.selected_range.is_empty() {
142            cx.write_to_clipboard(ClipboardItem::new_string(
143                self.content[self.selected_range.clone()].to_string(),
144            ));
145        }
146    }
147    fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
148        if !self.selected_range.is_empty() {
149            cx.write_to_clipboard(ClipboardItem::new_string(
150                self.content[self.selected_range.clone()].to_string(),
151            ));
152            self.replace_text_in_range(None, "", window, cx)
153        }
154    }
155
156    fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
157        self.selected_range = offset..offset;
158        cx.notify()
159    }
160
161    fn cursor_offset(&self) -> usize {
162        if self.selection_reversed {
163            self.selected_range.start
164        } else {
165            self.selected_range.end
166        }
167    }
168
169    fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
170        if self.content.is_empty() {
171            return 0;
172        }
173
174        let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
175        else {
176            return 0;
177        };
178        if position.y < bounds.top() {
179            return 0;
180        }
181        if position.y > bounds.bottom() {
182            return self.content.len();
183        }
184        line.closest_index_for_x(position.x - bounds.left())
185    }
186
187    fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
188        if self.selection_reversed {
189            self.selected_range.start = offset
190        } else {
191            self.selected_range.end = offset
192        };
193        if self.selected_range.end < self.selected_range.start {
194            self.selection_reversed = !self.selection_reversed;
195            self.selected_range = self.selected_range.end..self.selected_range.start;
196        }
197        cx.notify()
198    }
199
200    fn offset_from_utf16(&self, offset: usize) -> usize {
201        let mut utf8_offset = 0;
202        let mut utf16_count = 0;
203
204        for ch in self.content.chars() {
205            if utf16_count >= offset {
206                break;
207            }
208            utf16_count += ch.len_utf16();
209            utf8_offset += ch.len_utf8();
210        }
211
212        utf8_offset
213    }
214
215    fn offset_to_utf16(&self, offset: usize) -> usize {
216        let mut utf16_offset = 0;
217        let mut utf8_count = 0;
218
219        for ch in self.content.chars() {
220            if utf8_count >= offset {
221                break;
222            }
223            utf8_count += ch.len_utf8();
224            utf16_offset += ch.len_utf16();
225        }
226
227        utf16_offset
228    }
229
230    fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
231        self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
232    }
233
234    fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
235        self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
236    }
237
238    fn previous_boundary(&self, offset: usize) -> usize {
239        self.content
240            .grapheme_indices(true)
241            .rev()
242            .find_map(|(idx, _)| (idx < offset).then_some(idx))
243            .unwrap_or(0)
244    }
245
246    fn next_boundary(&self, offset: usize) -> usize {
247        self.content
248            .grapheme_indices(true)
249            .find_map(|(idx, _)| (idx > offset).then_some(idx))
250            .unwrap_or(self.content.len())
251    }
252
253    fn reset(&mut self) {
254        self.content = "".into();
255        self.selected_range = 0..0;
256        self.selection_reversed = false;
257        self.marked_range = None;
258        self.last_layout = None;
259        self.last_bounds = None;
260        self.is_selecting = false;
261    }
262}
263
264impl EntityInputHandler for TextInput {
265    fn text_for_range(
266        &mut self,
267        range_utf16: Range<usize>,
268        actual_range: &mut Option<Range<usize>>,
269        _window: &mut Window,
270        _cx: &mut Context<Self>,
271    ) -> Option<String> {
272        let range = self.range_from_utf16(&range_utf16);
273        actual_range.replace(self.range_to_utf16(&range));
274        Some(self.content[range].to_string())
275    }
276
277    fn selected_text_range(
278        &mut self,
279        _ignore_disabled_input: bool,
280        _window: &mut Window,
281        _cx: &mut Context<Self>,
282    ) -> Option<UTF16Selection> {
283        Some(UTF16Selection {
284            range: self.range_to_utf16(&self.selected_range),
285            reversed: self.selection_reversed,
286        })
287    }
288
289    fn marked_text_range(
290        &self,
291        _window: &mut Window,
292        _cx: &mut Context<Self>,
293    ) -> Option<Range<usize>> {
294        self.marked_range
295            .as_ref()
296            .map(|range| self.range_to_utf16(range))
297    }
298
299    fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
300        self.marked_range = None;
301    }
302
303    fn replace_text_in_range(
304        &mut self,
305        range_utf16: Option<Range<usize>>,
306        new_text: &str,
307        _: &mut Window,
308        cx: &mut Context<Self>,
309    ) {
310        let range = range_utf16
311            .as_ref()
312            .map(|range_utf16| self.range_from_utf16(range_utf16))
313            .or(self.marked_range.clone())
314            .unwrap_or(self.selected_range.clone());
315
316        self.content =
317            (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
318                .into();
319        self.selected_range = range.start + new_text.len()..range.start + new_text.len();
320        self.marked_range.take();
321        cx.notify();
322    }
323
324    fn replace_and_mark_text_in_range(
325        &mut self,
326        range_utf16: Option<Range<usize>>,
327        new_text: &str,
328        new_selected_range_utf16: Option<Range<usize>>,
329        _window: &mut Window,
330        cx: &mut Context<Self>,
331    ) {
332        let range = range_utf16
333            .as_ref()
334            .map(|range_utf16| self.range_from_utf16(range_utf16))
335            .or(self.marked_range.clone())
336            .unwrap_or(self.selected_range.clone());
337
338        self.content =
339            (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
340                .into();
341        if !new_text.is_empty() {
342            self.marked_range = Some(range.start..range.start + new_text.len());
343        } else {
344            self.marked_range = None;
345        }
346        self.selected_range = new_selected_range_utf16
347            .as_ref()
348            .map(|range_utf16| self.range_from_utf16(range_utf16))
349            .map(|new_range| new_range.start + range.start..new_range.end + range.end)
350            .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
351
352        cx.notify();
353    }
354
355    fn bounds_for_range(
356        &mut self,
357        range_utf16: Range<usize>,
358        bounds: Bounds<Pixels>,
359        _window: &mut Window,
360        _cx: &mut Context<Self>,
361    ) -> Option<Bounds<Pixels>> {
362        let last_layout = self.last_layout.as_ref()?;
363        let range = self.range_from_utf16(&range_utf16);
364        Some(Bounds::from_corners(
365            point(
366                bounds.left() + last_layout.x_for_index(range.start),
367                bounds.top(),
368            ),
369            point(
370                bounds.left() + last_layout.x_for_index(range.end),
371                bounds.bottom(),
372            ),
373        ))
374    }
375
376    fn character_index_for_point(
377        &mut self,
378        point: gpui::Point<Pixels>,
379        _window: &mut Window,
380        _cx: &mut Context<Self>,
381    ) -> Option<usize> {
382        let line_point = self.last_bounds?.localize(&point)?;
383        let last_layout = self.last_layout.as_ref()?;
384
385        assert_eq!(last_layout.text, self.content);
386        let utf8_index = last_layout.index_for_x(point.x - line_point.x)?;
387        Some(self.offset_to_utf16(utf8_index))
388    }
389}
390
391struct TextElement {
392    input: Entity<TextInput>,
393}
394
395struct PrepaintState {
396    line: Option<ShapedLine>,
397    cursor: Option<PaintQuad>,
398    selection: Option<PaintQuad>,
399}
400
401impl IntoElement for TextElement {
402    type Element = Self;
403
404    fn into_element(self) -> Self::Element {
405        self
406    }
407}
408
409impl Element for TextElement {
410    type RequestLayoutState = ();
411    type PrepaintState = PrepaintState;
412
413    fn id(&self) -> Option<ElementId> {
414        None
415    }
416
417    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
418        None
419    }
420
421    fn request_layout(
422        &mut self,
423        _id: Option<&GlobalElementId>,
424        _inspector_id: Option<&gpui::InspectorElementId>,
425        window: &mut Window,
426        cx: &mut App,
427    ) -> (LayoutId, Self::RequestLayoutState) {
428        let mut style = Style::default();
429        style.size.width = relative(1.).into();
430        style.size.height = window.line_height().into();
431        (window.request_layout(style, [], cx), ())
432    }
433
434    fn prepaint(
435        &mut self,
436        _id: Option<&GlobalElementId>,
437        _inspector_id: Option<&gpui::InspectorElementId>,
438        bounds: Bounds<Pixels>,
439        _request_layout: &mut Self::RequestLayoutState,
440        window: &mut Window,
441        cx: &mut App,
442    ) -> Self::PrepaintState {
443        let input = self.input.read(cx);
444        let content = input.content.clone();
445        let selected_range = input.selected_range.clone();
446        let cursor = input.cursor_offset();
447        let style = window.text_style();
448
449        let (display_text, text_color) = if content.is_empty() {
450            (input.placeholder.clone(), hsla(0., 0., 0., 0.2))
451        } else {
452            (content, style.color)
453        };
454
455        let run = TextRun {
456            len: display_text.len(),
457            font: style.font(),
458            color: text_color,
459            background_color: None,
460            underline: None,
461            strikethrough: None,
462        };
463        let runs = if let Some(marked_range) = input.marked_range.as_ref() {
464            vec![
465                TextRun {
466                    len: marked_range.start,
467                    ..run.clone()
468                },
469                TextRun {
470                    len: marked_range.end - marked_range.start,
471                    underline: Some(UnderlineStyle {
472                        color: Some(run.color),
473                        thickness: px(1.0),
474                        wavy: false,
475                    }),
476                    ..run.clone()
477                },
478                TextRun {
479                    len: display_text.len() - marked_range.end,
480                    ..run
481                },
482            ]
483            .into_iter()
484            .filter(|run| run.len > 0)
485            .collect()
486        } else {
487            vec![run]
488        };
489
490        let font_size = style.font_size.to_pixels(window.rem_size());
491        let line = window
492            .text_system()
493            .shape_line(display_text, font_size, &runs, None);
494
495        let cursor_pos = line.x_for_index(cursor);
496        let (selection, cursor) = if selected_range.is_empty() {
497            (
498                None,
499                Some(fill(
500                    Bounds::new(
501                        point(bounds.left() + cursor_pos, bounds.top()),
502                        size(px(2.), bounds.bottom() - bounds.top()),
503                    ),
504                    gpui::blue(),
505                )),
506            )
507        } else {
508            (
509                Some(fill(
510                    Bounds::from_corners(
511                        point(
512                            bounds.left() + line.x_for_index(selected_range.start),
513                            bounds.top(),
514                        ),
515                        point(
516                            bounds.left() + line.x_for_index(selected_range.end),
517                            bounds.bottom(),
518                        ),
519                    ),
520                    rgba(0x3311ff30),
521                )),
522                None,
523            )
524        };
525        PrepaintState {
526            line: Some(line),
527            cursor,
528            selection,
529        }
530    }
531
532    fn paint(
533        &mut self,
534        _id: Option<&GlobalElementId>,
535        _inspector_id: Option<&gpui::InspectorElementId>,
536        bounds: Bounds<Pixels>,
537        _request_layout: &mut Self::RequestLayoutState,
538        prepaint: &mut Self::PrepaintState,
539        window: &mut Window,
540        cx: &mut App,
541    ) {
542        let focus_handle = self.input.read(cx).focus_handle.clone();
543        window.handle_input(
544            &focus_handle,
545            ElementInputHandler::new(bounds, self.input.clone()),
546            cx,
547        );
548        if let Some(selection) = prepaint.selection.take() {
549            window.paint_quad(selection)
550        }
551        let line = prepaint.line.take().unwrap();
552        line.paint(
553            bounds.origin,
554            window.line_height(),
555            gpui::TextAlign::Left,
556            None,
557            window,
558            cx,
559        )
560        .unwrap();
561
562        if focus_handle.is_focused(window)
563            && let Some(cursor) = prepaint.cursor.take()
564        {
565            window.paint_quad(cursor);
566        }
567
568        self.input.update(cx, |input, _cx| {
569            input.last_layout = Some(line);
570            input.last_bounds = Some(bounds);
571        });
572    }
573}
574
575impl Render for TextInput {
576    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
577        div()
578            .flex()
579            .key_context("TextInput")
580            .track_focus(&self.focus_handle(cx))
581            .cursor(CursorStyle::IBeam)
582            .on_action(cx.listener(Self::backspace))
583            .on_action(cx.listener(Self::delete))
584            .on_action(cx.listener(Self::left))
585            .on_action(cx.listener(Self::right))
586            .on_action(cx.listener(Self::select_left))
587            .on_action(cx.listener(Self::select_right))
588            .on_action(cx.listener(Self::select_all))
589            .on_action(cx.listener(Self::home))
590            .on_action(cx.listener(Self::end))
591            .on_action(cx.listener(Self::show_character_palette))
592            .on_action(cx.listener(Self::paste))
593            .on_action(cx.listener(Self::cut))
594            .on_action(cx.listener(Self::copy))
595            .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
596            .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
597            .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
598            .on_mouse_move(cx.listener(Self::on_mouse_move))
599            .bg(rgb(0xeeeeee))
600            .line_height(px(30.))
601            .text_size(px(24.))
602            .child(
603                div()
604                    .h(px(30. + 4. * 2.))
605                    .w_full()
606                    .p(px(4.))
607                    .bg(white())
608                    .child(TextElement { input: cx.entity() }),
609            )
610    }
611}
612
613impl Focusable for TextInput {
614    fn focus_handle(&self, _: &App) -> FocusHandle {
615        self.focus_handle.clone()
616    }
617}
618
619struct InputExample {
620    text_input: Entity<TextInput>,
621    recent_keystrokes: Vec<Keystroke>,
622    focus_handle: FocusHandle,
623}
624
625impl Focusable for InputExample {
626    fn focus_handle(&self, _: &App) -> FocusHandle {
627        self.focus_handle.clone()
628    }
629}
630
631impl InputExample {
632    fn on_reset_click(&mut self, _: &MouseUpEvent, _window: &mut Window, cx: &mut Context<Self>) {
633        self.recent_keystrokes.clear();
634        self.text_input
635            .update(cx, |text_input, _cx| text_input.reset());
636        cx.notify();
637    }
638}
639
640impl Render for InputExample {
641    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
642        div()
643            .bg(rgb(0xaaaaaa))
644            .track_focus(&self.focus_handle(cx))
645            .flex()
646            .flex_col()
647            .size_full()
648            .child(
649                div()
650                    .bg(white())
651                    .border_b_1()
652                    .border_color(black())
653                    .flex()
654                    .flex_row()
655                    .justify_between()
656                    .child(format!("Keyboard {}", cx.keyboard_layout().name()))
657                    .child(
658                        div()
659                            .border_1()
660                            .border_color(black())
661                            .px_2()
662                            .bg(yellow())
663                            .child("Reset")
664                            .hover(|style| {
665                                style
666                                    .bg(yellow().blend(opaque_grey(0.5, 0.5)))
667                                    .cursor_pointer()
668                            })
669                            .on_mouse_up(MouseButton::Left, cx.listener(Self::on_reset_click)),
670                    ),
671            )
672            .child(self.text_input.clone())
673            .children(self.recent_keystrokes.iter().rev().map(|ks| {
674                format!(
675                    "{:} {}",
676                    ks.unparse(),
677                    if let Some(key_char) = ks.key_char.as_ref() {
678                        format!("-> {:?}", key_char)
679                    } else {
680                        "".to_owned()
681                    }
682                )
683            }))
684    }
685}
686
687fn run_example() {
688    application().run(|cx: &mut App| {
689        let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
690        cx.bind_keys([
691            KeyBinding::new("backspace", Backspace, None),
692            KeyBinding::new("delete", Delete, None),
693            KeyBinding::new("left", Left, None),
694            KeyBinding::new("right", Right, None),
695            KeyBinding::new("shift-left", SelectLeft, None),
696            KeyBinding::new("shift-right", SelectRight, None),
697            KeyBinding::new("cmd-a", SelectAll, None),
698            KeyBinding::new("cmd-v", Paste, None),
699            KeyBinding::new("cmd-c", Copy, None),
700            KeyBinding::new("cmd-x", Cut, None),
701            KeyBinding::new("home", Home, None),
702            KeyBinding::new("end", End, None),
703            KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
704        ]);
705
706        let window = cx
707            .open_window(
708                WindowOptions {
709                    window_bounds: Some(WindowBounds::Windowed(bounds)),
710                    ..Default::default()
711                },
712                |_, cx| {
713                    let text_input = cx.new(|cx| TextInput {
714                        focus_handle: cx.focus_handle(),
715                        content: "".into(),
716                        placeholder: "Type here...".into(),
717                        selected_range: 0..0,
718                        selection_reversed: false,
719                        marked_range: None,
720                        last_layout: None,
721                        last_bounds: None,
722                        is_selecting: false,
723                    });
724                    cx.new(|cx| InputExample {
725                        text_input,
726                        recent_keystrokes: vec![],
727                        focus_handle: cx.focus_handle(),
728                    })
729                },
730            )
731            .unwrap();
732        let view = window.update(cx, |_, _, cx| cx.entity()).unwrap();
733        cx.observe_keystrokes(move |ev, _, cx| {
734            view.update(cx, |view, cx| {
735                view.recent_keystrokes.push(ev.keystroke.clone());
736                cx.notify();
737            })
738        })
739        .detach();
740        cx.on_keyboard_layout_change({
741            move |cx| {
742                window.update(cx, |_, _, cx| cx.notify()).ok();
743            }
744        })
745        .detach();
746
747        window
748            .update(cx, |view, window, cx| {
749                window.focus(&view.text_input.focus_handle(cx), cx);
750                cx.activate(true);
751            })
752            .unwrap();
753        cx.on_action(|_: &Quit, cx| cx.quit());
754        cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
755    });
756}
757
758#[cfg(not(target_family = "wasm"))]
759fn main() {
760    run_example();
761}
762
763#[cfg(target_family = "wasm")]
764#[wasm_bindgen::prelude::wasm_bindgen(start)]
765pub fn start() {
766    gpui_platform::web_init();
767    run_example();
768}