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}
30
31impl TextInput {
32 fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
33 if self.selected_range.is_empty() {
34 self.move_to(self.previous_boundary(self.cursor_offset()), cx);
35 } else {
36 self.move_to(self.selected_range.end, cx)
37 }
38 }
39
40 fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
41 if self.selected_range.is_empty() {
42 self.move_to(self.next_boundary(self.selected_range.end), cx);
43 } else {
44 self.move_to(self.selected_range.start, cx)
45 }
46 }
47
48 fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext<Self>) {
49 self.select_to(self.previous_boundary(self.cursor_offset()), cx);
50 }
51
52 fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext<Self>) {
53 self.select_to(self.next_boundary(self.cursor_offset()), cx);
54 }
55
56 fn select_all(&mut self, _: &SelectRight, cx: &mut ViewContext<Self>) {
57 self.move_to(0, cx);
58 self.select_to(self.content.len(), cx)
59 }
60
61 fn home(&mut self, _: &Home, cx: &mut ViewContext<Self>) {
62 self.move_to(0, cx);
63 }
64
65 fn end(&mut self, _: &End, cx: &mut ViewContext<Self>) {
66 self.move_to(self.content.len(), cx);
67 }
68
69 fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
70 if self.selected_range.is_empty() {
71 self.select_to(self.previous_boundary(self.cursor_offset()), cx)
72 }
73 self.replace_text_in_range(None, "", cx)
74 }
75
76 fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
77 if self.selected_range.is_empty() {
78 self.select_to(self.next_boundary(self.cursor_offset()), cx)
79 }
80 self.replace_text_in_range(None, "", cx)
81 }
82
83 fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
84 cx.show_character_palette();
85 }
86
87 fn move_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
88 self.selected_range = offset..offset;
89 cx.notify()
90 }
91
92 fn cursor_offset(&self) -> usize {
93 if self.selection_reversed {
94 self.selected_range.start
95 } else {
96 self.selected_range.end
97 }
98 }
99
100 fn select_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
101 if self.selection_reversed {
102 self.selected_range.start = offset
103 } else {
104 self.selected_range.end = offset
105 };
106 if self.selected_range.end < self.selected_range.start {
107 self.selection_reversed = !self.selection_reversed;
108 self.selected_range = self.selected_range.end..self.selected_range.start;
109 }
110 cx.notify()
111 }
112
113 fn offset_from_utf16(&self, offset: usize) -> usize {
114 let mut utf8_offset = 0;
115 let mut utf16_count = 0;
116
117 for ch in self.content.chars() {
118 if utf16_count >= offset {
119 break;
120 }
121 utf16_count += ch.len_utf16();
122 utf8_offset += ch.len_utf8();
123 }
124
125 utf8_offset
126 }
127
128 fn offset_to_utf16(&self, offset: usize) -> usize {
129 let mut utf16_offset = 0;
130 let mut utf8_count = 0;
131
132 for ch in self.content.chars() {
133 if utf8_count >= offset {
134 break;
135 }
136 utf8_count += ch.len_utf8();
137 utf16_offset += ch.len_utf16();
138 }
139
140 utf16_offset
141 }
142
143 fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
144 self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
145 }
146
147 fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
148 self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
149 }
150
151 fn previous_boundary(&self, offset: usize) -> usize {
152 self.content
153 .grapheme_indices(true)
154 .rev()
155 .find_map(|(idx, _)| (idx < offset).then_some(idx))
156 .unwrap_or(0)
157 }
158
159 fn next_boundary(&self, offset: usize) -> usize {
160 self.content
161 .grapheme_indices(true)
162 .find_map(|(idx, _)| (idx > offset).then_some(idx))
163 .unwrap_or(self.content.len())
164 }
165}
166
167impl ViewInputHandler for TextInput {
168 fn text_for_range(
169 &mut self,
170 range_utf16: Range<usize>,
171 _cx: &mut ViewContext<Self>,
172 ) -> Option<String> {
173 let range = self.range_from_utf16(&range_utf16);
174 Some(self.content[range].to_string())
175 }
176
177 fn selected_text_range(&mut self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
178 Some(self.range_to_utf16(&self.selected_range))
179 }
180
181 fn marked_text_range(&self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
182 self.marked_range
183 .as_ref()
184 .map(|range| self.range_to_utf16(range))
185 }
186
187 fn unmark_text(&mut self, _cx: &mut ViewContext<Self>) {
188 self.marked_range = None;
189 }
190
191 fn replace_text_in_range(
192 &mut self,
193 range_utf16: Option<Range<usize>>,
194 new_text: &str,
195 cx: &mut ViewContext<Self>,
196 ) {
197 let range = range_utf16
198 .as_ref()
199 .map(|range_utf16| self.range_from_utf16(range_utf16))
200 .or(self.marked_range.clone())
201 .unwrap_or(self.selected_range.clone());
202
203 self.content =
204 (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
205 .into();
206 self.selected_range = range.start + new_text.len()..range.start + new_text.len();
207 self.marked_range.take();
208 cx.notify();
209 }
210
211 fn replace_and_mark_text_in_range(
212 &mut self,
213 range_utf16: Option<Range<usize>>,
214 new_text: &str,
215 new_selected_range_utf16: Option<Range<usize>>,
216 cx: &mut ViewContext<Self>,
217 ) {
218 let range = range_utf16
219 .as_ref()
220 .map(|range_utf16| self.range_from_utf16(range_utf16))
221 .or(self.marked_range.clone())
222 .unwrap_or(self.selected_range.clone());
223
224 self.content =
225 (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
226 .into();
227 self.marked_range = Some(range.start..range.start + new_text.len());
228 self.selected_range = new_selected_range_utf16
229 .as_ref()
230 .map(|range_utf16| self.range_from_utf16(range_utf16))
231 .map(|new_range| new_range.start + range.start..new_range.end + range.end)
232 .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
233
234 cx.notify();
235 }
236
237 fn bounds_for_range(
238 &mut self,
239 range_utf16: Range<usize>,
240 bounds: Bounds<Pixels>,
241 _cx: &mut ViewContext<Self>,
242 ) -> Option<Bounds<Pixels>> {
243 let Some(last_layout) = self.last_layout.as_ref() else {
244 return None;
245 };
246 let range = self.range_from_utf16(&range_utf16);
247 Some(Bounds::from_corners(
248 point(
249 bounds.left() + last_layout.x_for_index(range.start),
250 bounds.top(),
251 ),
252 point(
253 bounds.left() + last_layout.x_for_index(range.end),
254 bounds.bottom(),
255 ),
256 ))
257 }
258}
259
260struct TextElement {
261 input: View<TextInput>,
262}
263
264struct PrepaintState {
265 line: Option<ShapedLine>,
266 cursor: Option<PaintQuad>,
267 selection: Option<PaintQuad>,
268}
269
270impl IntoElement for TextElement {
271 type Element = Self;
272
273 fn into_element(self) -> Self::Element {
274 self
275 }
276}
277
278impl Element for TextElement {
279 type RequestLayoutState = ();
280
281 type PrepaintState = PrepaintState;
282
283 fn id(&self) -> Option<ElementId> {
284 None
285 }
286
287 fn request_layout(
288 &mut self,
289 _id: Option<&GlobalElementId>,
290 cx: &mut WindowContext,
291 ) -> (LayoutId, Self::RequestLayoutState) {
292 let mut style = Style::default();
293 style.size.width = relative(1.).into();
294 style.size.height = cx.line_height().into();
295 (cx.request_layout(style, []), ())
296 }
297
298 fn prepaint(
299 &mut self,
300 _id: Option<&GlobalElementId>,
301 bounds: Bounds<Pixels>,
302 _request_layout: &mut Self::RequestLayoutState,
303 cx: &mut WindowContext,
304 ) -> Self::PrepaintState {
305 let input = self.input.read(cx);
306 let content = input.content.clone();
307 let selected_range = input.selected_range.clone();
308 let cursor = input.cursor_offset();
309 let style = cx.text_style();
310 let run = TextRun {
311 len: input.content.len(),
312 font: style.font(),
313 color: style.color,
314 background_color: None,
315 underline: None,
316 strikethrough: None,
317 };
318 let runs = if let Some(marked_range) = input.marked_range.as_ref() {
319 vec![
320 TextRun {
321 len: marked_range.start,
322 ..run.clone()
323 },
324 TextRun {
325 len: marked_range.end - marked_range.start,
326 underline: Some(UnderlineStyle {
327 color: Some(run.color),
328 thickness: px(1.0),
329 wavy: false,
330 }),
331 ..run.clone()
332 },
333 TextRun {
334 len: input.content.len() - marked_range.end,
335 ..run.clone()
336 },
337 ]
338 .into_iter()
339 .filter(|run| run.len > 0)
340 .collect()
341 } else {
342 vec![run]
343 };
344
345 let font_size = style.font_size.to_pixels(cx.rem_size());
346 let line = cx
347 .text_system()
348 .shape_line(content, font_size, &runs)
349 .unwrap();
350
351 let cursor_pos = line.x_for_index(cursor);
352 let (selection, cursor) = if selected_range.is_empty() {
353 (
354 None,
355 Some(fill(
356 Bounds::new(
357 point(bounds.left() + cursor_pos, bounds.top()),
358 size(px(2.), bounds.bottom() - bounds.top()),
359 ),
360 gpui::blue(),
361 )),
362 )
363 } else {
364 (
365 Some(fill(
366 Bounds::from_corners(
367 point(
368 bounds.left() + line.x_for_index(selected_range.start),
369 bounds.top(),
370 ),
371 point(
372 bounds.left() + line.x_for_index(selected_range.end),
373 bounds.bottom(),
374 ),
375 ),
376 rgba(0x3311FF30),
377 )),
378 None,
379 )
380 };
381 PrepaintState {
382 line: Some(line),
383 cursor,
384 selection,
385 }
386 }
387
388 fn paint(
389 &mut self,
390 _id: Option<&GlobalElementId>,
391 bounds: Bounds<Pixels>,
392 _request_layout: &mut Self::RequestLayoutState,
393 prepaint: &mut Self::PrepaintState,
394 cx: &mut WindowContext,
395 ) {
396 let focus_handle = self.input.read(cx).focus_handle.clone();
397 cx.handle_input(
398 &focus_handle,
399 ElementInputHandler::new(bounds, self.input.clone()),
400 );
401 if let Some(selection) = prepaint.selection.take() {
402 cx.paint_quad(selection)
403 }
404 let line = prepaint.line.take().unwrap();
405 line.paint(bounds.origin, cx.line_height(), cx).unwrap();
406
407 if let Some(cursor) = prepaint.cursor.take() {
408 cx.paint_quad(cursor);
409 }
410 self.input.update(cx, |input, _cx| {
411 input.last_layout = Some(line);
412 });
413 }
414}
415
416impl Render for TextInput {
417 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
418 div()
419 .flex()
420 .key_context("TextInput")
421 .track_focus(&self.focus_handle)
422 .on_action(cx.listener(Self::backspace))
423 .on_action(cx.listener(Self::delete))
424 .on_action(cx.listener(Self::left))
425 .on_action(cx.listener(Self::right))
426 .on_action(cx.listener(Self::select_left))
427 .on_action(cx.listener(Self::select_right))
428 .on_action(cx.listener(Self::select_all))
429 .on_action(cx.listener(Self::home))
430 .on_action(cx.listener(Self::end))
431 .on_action(cx.listener(Self::show_character_palette))
432 .bg(rgb(0xeeeeee))
433 .size_full()
434 .line_height(px(30.))
435 .text_size(px(24.))
436 .child(
437 div()
438 .h(px(30. + 4. * 2.))
439 .w_full()
440 .p(px(4.))
441 .bg(white())
442 .child(TextElement {
443 input: cx.view().clone(),
444 }),
445 )
446 }
447}
448
449fn main() {
450 App::new().run(|cx: &mut AppContext| {
451 let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
452 cx.bind_keys([
453 KeyBinding::new("backspace", Backspace, None),
454 KeyBinding::new("delete", Delete, None),
455 KeyBinding::new("left", Left, None),
456 KeyBinding::new("right", Right, None),
457 KeyBinding::new("shift-left", SelectLeft, None),
458 KeyBinding::new("shift-right", SelectRight, None),
459 KeyBinding::new("cmd-a", SelectAll, None),
460 KeyBinding::new("home", Home, None),
461 KeyBinding::new("end", End, None),
462 KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
463 ]);
464 let window = cx
465 .open_window(
466 WindowOptions {
467 window_bounds: Some(WindowBounds::Windowed(bounds)),
468 ..Default::default()
469 },
470 |cx| {
471 cx.new_view(|cx| TextInput {
472 focus_handle: cx.focus_handle(),
473 content: "".into(),
474 selected_range: 0..0,
475 selection_reversed: false,
476 marked_range: None,
477 last_layout: None,
478 })
479 },
480 )
481 .unwrap();
482 window
483 .update(cx, |view, cx| {
484 view.focus_handle.focus(cx);
485 cx.activate(true)
486 })
487 .unwrap();
488 });
489}