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