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