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(&mut self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
229 Some(self.range_to_utf16(&self.selected_range))
230 }
231
232 fn marked_text_range(&self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
233 self.marked_range
234 .as_ref()
235 .map(|range| self.range_to_utf16(range))
236 }
237
238 fn unmark_text(&mut self, _cx: &mut ViewContext<Self>) {
239 self.marked_range = None;
240 }
241
242 fn replace_text_in_range(
243 &mut self,
244 range_utf16: Option<Range<usize>>,
245 new_text: &str,
246 cx: &mut ViewContext<Self>,
247 ) {
248 let range = range_utf16
249 .as_ref()
250 .map(|range_utf16| self.range_from_utf16(range_utf16))
251 .or(self.marked_range.clone())
252 .unwrap_or(self.selected_range.clone());
253
254 self.content =
255 (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
256 .into();
257 self.selected_range = range.start + new_text.len()..range.start + new_text.len();
258 self.marked_range.take();
259 cx.notify();
260 }
261
262 fn replace_and_mark_text_in_range(
263 &mut self,
264 range_utf16: Option<Range<usize>>,
265 new_text: &str,
266 new_selected_range_utf16: Option<Range<usize>>,
267 cx: &mut ViewContext<Self>,
268 ) {
269 let range = range_utf16
270 .as_ref()
271 .map(|range_utf16| self.range_from_utf16(range_utf16))
272 .or(self.marked_range.clone())
273 .unwrap_or(self.selected_range.clone());
274
275 self.content =
276 (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
277 .into();
278 self.marked_range = Some(range.start..range.start + new_text.len());
279 self.selected_range = new_selected_range_utf16
280 .as_ref()
281 .map(|range_utf16| self.range_from_utf16(range_utf16))
282 .map(|new_range| new_range.start + range.start..new_range.end + range.end)
283 .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
284
285 cx.notify();
286 }
287
288 fn bounds_for_range(
289 &mut self,
290 range_utf16: Range<usize>,
291 bounds: Bounds<Pixels>,
292 _cx: &mut ViewContext<Self>,
293 ) -> Option<Bounds<Pixels>> {
294 let Some(last_layout) = self.last_layout.as_ref() else {
295 return None;
296 };
297 let range = self.range_from_utf16(&range_utf16);
298 Some(Bounds::from_corners(
299 point(
300 bounds.left() + last_layout.x_for_index(range.start),
301 bounds.top(),
302 ),
303 point(
304 bounds.left() + last_layout.x_for_index(range.end),
305 bounds.bottom(),
306 ),
307 ))
308 }
309}
310
311struct TextElement {
312 input: View<TextInput>,
313}
314
315struct PrepaintState {
316 line: Option<ShapedLine>,
317 cursor: Option<PaintQuad>,
318 selection: Option<PaintQuad>,
319}
320
321impl IntoElement for TextElement {
322 type Element = Self;
323
324 fn into_element(self) -> Self::Element {
325 self
326 }
327}
328
329impl Element for TextElement {
330 type RequestLayoutState = ();
331
332 type PrepaintState = PrepaintState;
333
334 fn id(&self) -> Option<ElementId> {
335 None
336 }
337
338 fn request_layout(
339 &mut self,
340 _id: Option<&GlobalElementId>,
341 cx: &mut WindowContext,
342 ) -> (LayoutId, Self::RequestLayoutState) {
343 let mut style = Style::default();
344 style.size.width = relative(1.).into();
345 style.size.height = cx.line_height().into();
346 (cx.request_layout(style, []), ())
347 }
348
349 fn prepaint(
350 &mut self,
351 _id: Option<&GlobalElementId>,
352 bounds: Bounds<Pixels>,
353 _request_layout: &mut Self::RequestLayoutState,
354 cx: &mut WindowContext,
355 ) -> Self::PrepaintState {
356 let input = self.input.read(cx);
357 let content = input.content.clone();
358 let selected_range = input.selected_range.clone();
359 let cursor = input.cursor_offset();
360 let style = cx.text_style();
361
362 let (display_text, text_color) = if content.is_empty() {
363 (input.placeholder.clone(), hsla(0., 0., 0., 0.2))
364 } else {
365 (content.clone(), style.color)
366 };
367
368 let run = TextRun {
369 len: display_text.len(),
370 font: style.font(),
371 color: text_color,
372 background_color: None,
373 underline: None,
374 strikethrough: None,
375 };
376 let runs = if let Some(marked_range) = input.marked_range.as_ref() {
377 vec![
378 TextRun {
379 len: marked_range.start,
380 ..run.clone()
381 },
382 TextRun {
383 len: marked_range.end - marked_range.start,
384 underline: Some(UnderlineStyle {
385 color: Some(run.color),
386 thickness: px(1.0),
387 wavy: false,
388 }),
389 ..run.clone()
390 },
391 TextRun {
392 len: display_text.len() - marked_range.end,
393 ..run.clone()
394 },
395 ]
396 .into_iter()
397 .filter(|run| run.len > 0)
398 .collect()
399 } else {
400 vec![run]
401 };
402
403 let font_size = style.font_size.to_pixels(cx.rem_size());
404 let line = cx
405 .text_system()
406 .shape_line(display_text, font_size, &runs)
407 .unwrap();
408
409 let cursor_pos = line.x_for_index(cursor);
410 let (selection, cursor) = if selected_range.is_empty() {
411 (
412 None,
413 Some(fill(
414 Bounds::new(
415 point(bounds.left() + cursor_pos, bounds.top()),
416 size(px(2.), bounds.bottom() - bounds.top()),
417 ),
418 gpui::blue(),
419 )),
420 )
421 } else {
422 (
423 Some(fill(
424 Bounds::from_corners(
425 point(
426 bounds.left() + line.x_for_index(selected_range.start),
427 bounds.top(),
428 ),
429 point(
430 bounds.left() + line.x_for_index(selected_range.end),
431 bounds.bottom(),
432 ),
433 ),
434 rgba(0x3311FF30),
435 )),
436 None,
437 )
438 };
439 PrepaintState {
440 line: Some(line),
441 cursor,
442 selection,
443 }
444 }
445
446 fn paint(
447 &mut self,
448 _id: Option<&GlobalElementId>,
449 bounds: Bounds<Pixels>,
450 _request_layout: &mut Self::RequestLayoutState,
451 prepaint: &mut Self::PrepaintState,
452 cx: &mut WindowContext,
453 ) {
454 let focus_handle = self.input.read(cx).focus_handle.clone();
455 cx.handle_input(
456 &focus_handle,
457 ElementInputHandler::new(bounds, self.input.clone()),
458 );
459 if let Some(selection) = prepaint.selection.take() {
460 cx.paint_quad(selection)
461 }
462 let line = prepaint.line.take().unwrap();
463 line.paint(bounds.origin, cx.line_height(), cx).unwrap();
464
465 if let Some(cursor) = prepaint.cursor.take() {
466 cx.paint_quad(cursor);
467 }
468 self.input.update(cx, |input, _cx| {
469 input.last_layout = Some(line);
470 input.last_bounds = Some(bounds);
471 });
472 }
473}
474
475impl Render for TextInput {
476 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
477 div()
478 .flex()
479 .key_context("TextInput")
480 .track_focus(&self.focus_handle)
481 .cursor(CursorStyle::IBeam)
482 .on_action(cx.listener(Self::backspace))
483 .on_action(cx.listener(Self::delete))
484 .on_action(cx.listener(Self::left))
485 .on_action(cx.listener(Self::right))
486 .on_action(cx.listener(Self::select_left))
487 .on_action(cx.listener(Self::select_right))
488 .on_action(cx.listener(Self::select_all))
489 .on_action(cx.listener(Self::home))
490 .on_action(cx.listener(Self::end))
491 .on_action(cx.listener(Self::show_character_palette))
492 .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
493 .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
494 .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
495 .on_mouse_move(cx.listener(Self::on_mouse_move))
496 .bg(rgb(0xeeeeee))
497 .size_full()
498 .line_height(px(30.))
499 .text_size(px(24.))
500 .child(
501 div()
502 .h(px(30. + 4. * 2.))
503 .w_full()
504 .p(px(4.))
505 .bg(white())
506 .child(TextElement {
507 input: cx.view().clone(),
508 }),
509 )
510 }
511}
512
513impl FocusableView for TextInput {
514 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
515 self.focus_handle.clone()
516 }
517}
518
519struct InputExample {
520 text_input: View<TextInput>,
521 recent_keystrokes: Vec<Keystroke>,
522}
523
524impl InputExample {
525 fn on_reset_click(&mut self, _: &MouseUpEvent, cx: &mut ViewContext<Self>) {
526 self.recent_keystrokes.clear();
527 self.text_input
528 .update(cx, |text_input, _cx| text_input.reset());
529 cx.notify();
530 }
531}
532
533impl Render for InputExample {
534 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
535 let num_keystrokes = self.recent_keystrokes.len();
536 div()
537 .bg(rgb(0xaaaaaa))
538 .flex()
539 .flex_col()
540 .size_full()
541 .child(
542 div()
543 .bg(white())
544 .border_b_1()
545 .border_color(black())
546 .flex()
547 .flex_row()
548 .justify_between()
549 .child(format!("Keystrokes: {}", num_keystrokes))
550 .child(
551 div()
552 .border_1()
553 .border_color(black())
554 .px_2()
555 .bg(yellow())
556 .child("Reset")
557 .hover(|style| {
558 style
559 .bg(yellow().blend(opaque_grey(0.5, 0.5)))
560 .cursor_pointer()
561 })
562 .on_mouse_up(MouseButton::Left, cx.listener(Self::on_reset_click)),
563 ),
564 )
565 .child(self.text_input.clone())
566 .children(self.recent_keystrokes.iter().rev().map(|ks| {
567 format!(
568 "{:} {}",
569 ks,
570 if let Some(ime_key) = ks.ime_key.as_ref() {
571 format!("-> {}", ime_key)
572 } else {
573 "".to_owned()
574 }
575 )
576 }))
577 }
578}
579
580fn main() {
581 App::new().run(|cx: &mut AppContext| {
582 let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
583 cx.bind_keys([
584 KeyBinding::new("backspace", Backspace, None),
585 KeyBinding::new("delete", Delete, None),
586 KeyBinding::new("left", Left, None),
587 KeyBinding::new("right", Right, None),
588 KeyBinding::new("shift-left", SelectLeft, None),
589 KeyBinding::new("shift-right", SelectRight, None),
590 KeyBinding::new("cmd-a", SelectAll, None),
591 KeyBinding::new("home", Home, None),
592 KeyBinding::new("end", End, None),
593 KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
594 ]);
595 let window = cx
596 .open_window(
597 WindowOptions {
598 window_bounds: Some(WindowBounds::Windowed(bounds)),
599 ..Default::default()
600 },
601 |cx| {
602 let text_input = cx.new_view(|cx| TextInput {
603 focus_handle: cx.focus_handle(),
604 content: "".into(),
605 placeholder: "Type here...".into(),
606 selected_range: 0..0,
607 selection_reversed: false,
608 marked_range: None,
609 last_layout: None,
610 last_bounds: None,
611 is_selecting: false,
612 });
613 cx.new_view(|_| InputExample {
614 text_input,
615 recent_keystrokes: vec![],
616 })
617 },
618 )
619 .unwrap();
620 cx.observe_keystrokes(move |ev, cx| {
621 window
622 .update(cx, |view, cx| {
623 view.recent_keystrokes.push(ev.keystroke.clone());
624 cx.notify();
625 })
626 .unwrap();
627 })
628 .detach();
629 window
630 .update(cx, |view, cx| {
631 cx.focus_view(&view.text_input);
632 cx.activate(true);
633 })
634 .unwrap();
635 });
636}