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