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}