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