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