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