use std::ops::Range; use gpui::*; use unicode_segmentation::*; actions!( text_input, [ Backspace, Delete, Left, Right, SelectLeft, SelectRight, SelectAll, Home, End, ShowCharacterPalette ] ); struct TextInput { focus_handle: FocusHandle, content: SharedString, selected_range: Range, selection_reversed: bool, marked_range: Option>, last_layout: Option, last_bounds: Option>, is_selecting: bool, } impl TextInput { fn left(&mut self, _: &Left, cx: &mut ViewContext) { if self.selected_range.is_empty() { self.move_to(self.previous_boundary(self.cursor_offset()), cx); } else { self.move_to(self.selected_range.start, cx) } } fn right(&mut self, _: &Right, cx: &mut ViewContext) { if self.selected_range.is_empty() { self.move_to(self.next_boundary(self.selected_range.end), cx); } else { self.move_to(self.selected_range.end, cx) } } fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext) { self.select_to(self.previous_boundary(self.cursor_offset()), cx); } fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext) { self.select_to(self.next_boundary(self.cursor_offset()), cx); } fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext) { self.move_to(0, cx); self.select_to(self.content.len(), cx) } fn home(&mut self, _: &Home, cx: &mut ViewContext) { self.move_to(0, cx); } fn end(&mut self, _: &End, cx: &mut ViewContext) { self.move_to(self.content.len(), cx); } fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { if self.selected_range.is_empty() { self.select_to(self.previous_boundary(self.cursor_offset()), cx) } self.replace_text_in_range(None, "", cx) } fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { if self.selected_range.is_empty() { self.select_to(self.next_boundary(self.cursor_offset()), cx) } self.replace_text_in_range(None, "", cx) } fn on_mouse_down(&mut self, event: &MouseDownEvent, cx: &mut ViewContext) { self.is_selecting = true; self.move_to(self.index_for_mouse_position(event.position), cx) } fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut ViewContext) { self.is_selecting = false; } fn on_mouse_move(&mut self, event: &MouseMoveEvent, cx: &mut ViewContext) { if self.is_selecting { self.select_to(self.index_for_mouse_position(event.position), cx); } } fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext) { cx.show_character_palette(); } fn move_to(&mut self, offset: usize, cx: &mut ViewContext) { self.selected_range = offset..offset; cx.notify() } fn cursor_offset(&self) -> usize { if self.selection_reversed { self.selected_range.start } else { self.selected_range.end } } fn index_for_mouse_position(&self, position: Point) -> usize { let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref()) else { return 0; }; if position.y < bounds.top() { return 0; } if position.y > bounds.bottom() { return self.content.len(); } line.closest_index_for_x(position.x - bounds.left()) } fn select_to(&mut self, offset: usize, cx: &mut ViewContext) { if self.selection_reversed { self.selected_range.start = offset } else { self.selected_range.end = offset }; if self.selected_range.end < self.selected_range.start { self.selection_reversed = !self.selection_reversed; self.selected_range = self.selected_range.end..self.selected_range.start; } cx.notify() } fn offset_from_utf16(&self, offset: usize) -> usize { let mut utf8_offset = 0; let mut utf16_count = 0; for ch in self.content.chars() { if utf16_count >= offset { break; } utf16_count += ch.len_utf16(); utf8_offset += ch.len_utf8(); } utf8_offset } fn offset_to_utf16(&self, offset: usize) -> usize { let mut utf16_offset = 0; let mut utf8_count = 0; for ch in self.content.chars() { if utf8_count >= offset { break; } utf8_count += ch.len_utf8(); utf16_offset += ch.len_utf16(); } utf16_offset } fn range_to_utf16(&self, range: &Range) -> Range { self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end) } fn range_from_utf16(&self, range_utf16: &Range) -> Range { self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end) } fn previous_boundary(&self, offset: usize) -> usize { self.content .grapheme_indices(true) .rev() .find_map(|(idx, _)| (idx < offset).then_some(idx)) .unwrap_or(0) } fn next_boundary(&self, offset: usize) -> usize { self.content .grapheme_indices(true) .find_map(|(idx, _)| (idx > offset).then_some(idx)) .unwrap_or(self.content.len()) } } impl ViewInputHandler for TextInput { fn text_for_range( &mut self, range_utf16: Range, _cx: &mut ViewContext, ) -> Option { let range = self.range_from_utf16(&range_utf16); Some(self.content[range].to_string()) } fn selected_text_range(&mut self, _cx: &mut ViewContext) -> Option> { Some(self.range_to_utf16(&self.selected_range)) } fn marked_text_range(&self, _cx: &mut ViewContext) -> Option> { self.marked_range .as_ref() .map(|range| self.range_to_utf16(range)) } fn unmark_text(&mut self, _cx: &mut ViewContext) { self.marked_range = None; } fn replace_text_in_range( &mut self, range_utf16: Option>, new_text: &str, cx: &mut ViewContext, ) { let range = range_utf16 .as_ref() .map(|range_utf16| self.range_from_utf16(range_utf16)) .or(self.marked_range.clone()) .unwrap_or(self.selected_range.clone()); self.content = (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..]) .into(); self.selected_range = range.start + new_text.len()..range.start + new_text.len(); self.marked_range.take(); cx.notify(); } fn replace_and_mark_text_in_range( &mut self, range_utf16: Option>, new_text: &str, new_selected_range_utf16: Option>, cx: &mut ViewContext, ) { let range = range_utf16 .as_ref() .map(|range_utf16| self.range_from_utf16(range_utf16)) .or(self.marked_range.clone()) .unwrap_or(self.selected_range.clone()); self.content = (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..]) .into(); self.marked_range = Some(range.start..range.start + new_text.len()); self.selected_range = new_selected_range_utf16 .as_ref() .map(|range_utf16| self.range_from_utf16(range_utf16)) .map(|new_range| new_range.start + range.start..new_range.end + range.end) .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len()); cx.notify(); } fn bounds_for_range( &mut self, range_utf16: Range, bounds: Bounds, _cx: &mut ViewContext, ) -> Option> { let Some(last_layout) = self.last_layout.as_ref() else { return None; }; let range = self.range_from_utf16(&range_utf16); Some(Bounds::from_corners( point( bounds.left() + last_layout.x_for_index(range.start), bounds.top(), ), point( bounds.left() + last_layout.x_for_index(range.end), bounds.bottom(), ), )) } } struct TextElement { input: View, } struct PrepaintState { line: Option, cursor: Option, selection: Option, } impl IntoElement for TextElement { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for TextElement { type RequestLayoutState = (); type PrepaintState = PrepaintState; fn id(&self) -> Option { None } fn request_layout( &mut self, _id: Option<&GlobalElementId>, cx: &mut WindowContext, ) -> (LayoutId, Self::RequestLayoutState) { let mut style = Style::default(); style.size.width = relative(1.).into(); style.size.height = cx.line_height().into(); (cx.request_layout(style, []), ()) } fn prepaint( &mut self, _id: Option<&GlobalElementId>, bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, cx: &mut WindowContext, ) -> Self::PrepaintState { let input = self.input.read(cx); let content = input.content.clone(); let selected_range = input.selected_range.clone(); let cursor = input.cursor_offset(); let style = cx.text_style(); let run = TextRun { len: input.content.len(), font: style.font(), color: style.color, background_color: None, underline: None, strikethrough: None, }; let runs = if let Some(marked_range) = input.marked_range.as_ref() { vec![ TextRun { len: marked_range.start, ..run.clone() }, TextRun { len: marked_range.end - marked_range.start, underline: Some(UnderlineStyle { color: Some(run.color), thickness: px(1.0), wavy: false, }), ..run.clone() }, TextRun { len: input.content.len() - marked_range.end, ..run.clone() }, ] .into_iter() .filter(|run| run.len > 0) .collect() } else { vec![run] }; let font_size = style.font_size.to_pixels(cx.rem_size()); let line = cx .text_system() .shape_line(content, font_size, &runs) .unwrap(); let cursor_pos = line.x_for_index(cursor); let (selection, cursor) = if selected_range.is_empty() { ( None, Some(fill( Bounds::new( point(bounds.left() + cursor_pos, bounds.top()), size(px(2.), bounds.bottom() - bounds.top()), ), gpui::blue(), )), ) } else { ( Some(fill( Bounds::from_corners( point( bounds.left() + line.x_for_index(selected_range.start), bounds.top(), ), point( bounds.left() + line.x_for_index(selected_range.end), bounds.bottom(), ), ), rgba(0x3311FF30), )), None, ) }; PrepaintState { line: Some(line), cursor, selection, } } fn paint( &mut self, _id: Option<&GlobalElementId>, bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, cx: &mut WindowContext, ) { let focus_handle = self.input.read(cx).focus_handle.clone(); cx.handle_input( &focus_handle, ElementInputHandler::new(bounds, self.input.clone()), ); if let Some(selection) = prepaint.selection.take() { cx.paint_quad(selection) } let line = prepaint.line.take().unwrap(); line.paint(bounds.origin, cx.line_height(), cx).unwrap(); if let Some(cursor) = prepaint.cursor.take() { cx.paint_quad(cursor); } self.input.update(cx, |input, _cx| { input.last_layout = Some(line); input.last_bounds = Some(bounds); }); } } impl Render for TextInput { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { div() .flex() .key_context("TextInput") .track_focus(&self.focus_handle) .cursor(CursorStyle::IBeam) .on_action(cx.listener(Self::backspace)) .on_action(cx.listener(Self::delete)) .on_action(cx.listener(Self::left)) .on_action(cx.listener(Self::right)) .on_action(cx.listener(Self::select_left)) .on_action(cx.listener(Self::select_right)) .on_action(cx.listener(Self::select_all)) .on_action(cx.listener(Self::home)) .on_action(cx.listener(Self::end)) .on_action(cx.listener(Self::show_character_palette)) .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up)) .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up)) .on_mouse_move(cx.listener(Self::on_mouse_move)) .bg(rgb(0xeeeeee)) .size_full() .line_height(px(30.)) .text_size(px(24.)) .child( div() .h(px(30. + 4. * 2.)) .w_full() .p(px(4.)) .bg(white()) .child(TextElement { input: cx.view().clone(), }), ) } } impl FocusableView for TextInput { fn focus_handle(&self, _: &AppContext) -> FocusHandle { self.focus_handle.clone() } } struct InputExample { text_input: View, recent_keystrokes: Vec, } impl Render for InputExample { fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { div() .bg(rgb(0xaaaaaa)) .flex() .flex_col() .size_full() .child(self.text_input.clone()) .children(self.recent_keystrokes.iter().rev().map(|ks| { format!( "{:} {}", ks, if let Some(ime_key) = ks.ime_key.as_ref() { format!("-> {}", ime_key) } else { "".to_owned() } ) })) } } fn main() { App::new().run(|cx: &mut AppContext| { let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx); cx.bind_keys([ KeyBinding::new("backspace", Backspace, None), KeyBinding::new("delete", Delete, None), KeyBinding::new("left", Left, None), KeyBinding::new("right", Right, None), KeyBinding::new("shift-left", SelectLeft, None), KeyBinding::new("shift-right", SelectRight, None), KeyBinding::new("cmd-a", SelectAll, None), KeyBinding::new("home", Home, None), KeyBinding::new("end", End, None), KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None), ]); let window = cx .open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), ..Default::default() }, |cx| { let text_input = cx.new_view(|cx| TextInput { focus_handle: cx.focus_handle(), content: "".into(), selected_range: 0..0, selection_reversed: false, marked_range: None, last_layout: None, last_bounds: None, is_selecting: false, }); cx.new_view(|_| InputExample { text_input, recent_keystrokes: vec![], }) }, ) .unwrap(); cx.observe_keystrokes(move |ev, cx| { window .update(cx, |view, cx| { view.recent_keystrokes.push(ev.keystroke.clone()); cx.notify(); }) .unwrap(); }) .detach(); window .update(cx, |view, cx| { cx.focus_view(&view.text_input); cx.activate(true); }) .unwrap(); }); }