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