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