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