From b2344a852e98af734e9d8f7fe3d493f9e82ca80c Mon Sep 17 00:00:00 2001 From: FabianLars Date: Sat, 22 Feb 2020 21:33:45 +0100 Subject: [PATCH] inital patch by Finnerale --- native/src/widget/text_input.rs | 249 +++++++++++++++++++++---- style/src/text_input.rs | 6 + wgpu/src/renderer/widget/text_input.rs | 46 ++++- 3 files changed, 259 insertions(+), 42 deletions(-) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index c068b895..bfe5244f 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -226,7 +226,53 @@ where self.font, ); - self.state.cursor_position = find_cursor_position( + self.state.cursor_position = + Cursor::Index(find_cursor_position( + renderer, + target + offset, + &value, + size, + 0, + self.value.len(), + self.font, + )); + } else { + self.state.cursor_position = Cursor::Index(0); + } + } + + self.state.is_pressed = is_clicked; + self.state.is_focused = is_clicked; + } + Event::Mouse(mouse::Event::Input { + button: mouse::Button::Left, + state: ButtonState::Released, + }) => { + self.state.is_pressed = false; + } + Event::Mouse(mouse::Event::CursorMoved { x, .. }) => { + if self.state.is_pressed { + let text_layout = layout.children().next().unwrap(); + let target = x - text_layout.bounds().x; + + if target > 0.0 { + let value = if self.is_secure { + self.value.secure() + } else { + self.value.clone() + }; + + let size = self.size.unwrap_or(renderer.default_size()); + + let offset = renderer.offset( + text_layout.bounds(), + size, + &value, + &self.state, + self.font, + ); + + let pos = find_cursor_position( renderer, target + offset, &value, @@ -235,21 +281,34 @@ where self.value.len(), self.font, ); - } else { - self.state.cursor_position = 0; + + let start = match self.state.cursor_position { + Cursor::Index(index) => index, + Cursor::Selection { start, .. } => start, + }; + + self.state.cursor_position = + Cursor::Selection { start, end: pos }.simplify(); } } - - self.state.is_focused = is_clicked; } Event::Keyboard(keyboard::Event::CharacterReceived(c)) if self.state.is_focused && self.state.is_pasting.is_none() && !c.is_control() => { - let cursor_position = self.state.cursor_position(&self.value); + let cursor = self.state.cursor_position(&self.value); - self.value.insert(cursor_position, c); + match cursor { + Cursor::Index(index) => { + self.value.insert(index, c); + } + Cursor::Selection { .. } => { + self.state.move_cursor_left(&self.value); + self.value.remove_many(cursor.left(), cursor.right()); + self.value.insert(cursor.left(), c); + } + } self.state.move_cursor_right(&self.value); let message = (self.on_change)(self.value.to_string()); @@ -266,27 +325,46 @@ where } } keyboard::KeyCode::Backspace => { - let cursor_position = - self.state.cursor_position(&self.value); + let cursor = self.state.cursor_position(&self.value); - if cursor_position > 0 { - self.state.move_cursor_left(&self.value); - - let _ = self.value.remove(cursor_position - 1); - - let message = (self.on_change)(self.value.to_string()); - messages.push(message); + match cursor { + Cursor::Index(index) if index > 0 => { + self.state.move_cursor_left(&self.value); + let _ = self.value.remove(index - 1); + let message = + (self.on_change)(self.value.to_string()); + messages.push(message); + } + Cursor::Selection { .. } => { + self.state.move_cursor_left(&self.value); + self.value + .remove_many(cursor.left(), cursor.right()); + let message = + (self.on_change)(self.value.to_string()); + messages.push(message); + } + _ => {} } } keyboard::KeyCode::Delete => { - let cursor_position = - self.state.cursor_position(&self.value); + let cursor = self.state.cursor_position(&self.value); - if cursor_position < self.value.len() { - let _ = self.value.remove(cursor_position); - - let message = (self.on_change)(self.value.to_string()); - messages.push(message); + match cursor { + Cursor::Index(index) if index < self.value.len() => { + let _ = self.value.remove(index); + let message = + (self.on_change)(self.value.to_string()); + messages.push(message); + } + Cursor::Selection { .. } => { + self.state.move_cursor_left(&self.value); + self.value + .remove_many(cursor.left(), cursor.right()); + let message = + (self.on_change)(self.value.to_string()); + messages.push(message); + } + _ => {} } } keyboard::KeyCode::Left => { @@ -308,7 +386,7 @@ where } } keyboard::KeyCode::Home => { - self.state.cursor_position = 0; + self.state.cursor_position = Cursor::Index(0); } keyboard::KeyCode::End => { self.state.move_cursor_to_end(&self.value); @@ -333,8 +411,19 @@ where let cursor_position = self.state.cursor_position(&self.value); + let insert_position = match cursor_position { + Cursor::Index(index) => index, + Cursor::Selection { .. } => { + self.state.move_cursor_left(&self.value); + self.value.remove_many( + cursor_position.left(), + cursor_position.right(), + ); + cursor_position.left() + } + }; self.value - .insert_many(cursor_position, content.clone()); + .insert_many(insert_position, content.clone()); self.state.move_cursor_right_by_amount( &self.value, @@ -499,11 +588,75 @@ where #[derive(Debug, Default, Clone)] pub struct State { is_focused: bool, + is_pressed: bool, is_pasting: Option, - cursor_position: usize, + cursor_position: Cursor, // TODO: Add stateful horizontal scrolling offset } +/// The cursor position of a [`TextInput`]. +/// +/// [`TextInput`]: struct.TextInput.html +#[derive(Debug, Clone, Copy)] +pub enum Cursor { + /// The cursor represents a position. + Index(usize), + /// The cursor represents a range. + Selection { + /// Where the selection started. + start: usize, + /// Where the selection was moved to. + end: usize, + }, +} + +impl Default for Cursor { + fn default() -> Self { + Cursor::Index(0) + } +} + +impl Cursor { + /// Simplify representation to `Cursor::Index` + /// if `start` and `end` are the same. + pub fn simplify(&self) -> Self { + match self { + Cursor::Index(_) => *self, + Cursor::Selection { start, end } => { + if start == end { + Cursor::Index(*start) + } else { + *self + } + } + } + } + + /// Position at which the cursor should be drawn. + pub fn position(&self) -> usize { + match *self { + Cursor::Index(index) => index, + Cursor::Selection { end, .. } => end, + } + } + + /// The cursor index or left end of the selection. + pub fn left(&self) -> usize { + match *self { + Cursor::Index(index) => index, + Cursor::Selection { start, end } => start.min(end), + } + } + + /// The cursor index or right end of the selection. + pub fn right(&self) -> usize { + match *self { + Cursor::Index(index) => index, + Cursor::Selection { start, end } => start.max(end), + } + } +} + impl State { /// Creates a new [`State`], representing an unfocused [`TextInput`]. /// @@ -520,8 +673,9 @@ impl State { Self { is_focused: true, + is_pressed: false, is_pasting: None, - cursor_position: usize::MAX, + cursor_position: Cursor::Index(usize::MAX), } } @@ -535,8 +689,15 @@ impl State { /// Returns the cursor position of a [`TextInput`]. /// /// [`TextInput`]: struct.TextInput.html - pub fn cursor_position(&self, value: &Value) -> usize { - self.cursor_position.min(value.len()) + pub fn cursor_position(&self, value: &Value) -> Cursor { + match self.cursor_position { + Cursor::Index(index) => Cursor::Index(index.min(value.len())), + Cursor::Selection { start, end } => Cursor::Selection { + start: start.min(value.len()), + end: end.min(value.len()), + } + .simplify(), + } } /// Moves the cursor of a [`TextInput`] to the left. @@ -545,8 +706,10 @@ impl State { pub(crate) fn move_cursor_left(&mut self, value: &Value) { let current = self.cursor_position(value); - if current > 0 { - self.cursor_position = current - 1; + self.cursor_position = match current { + Cursor::Index(index) if index > 0 => Cursor::Index(index - 1), + Cursor::Selection { .. } => Cursor::Index(current.left()), + _ => Cursor::Index(0), } } @@ -563,11 +726,12 @@ impl State { amount: usize, ) { let current = self.cursor_position(value); - let new_position = current.saturating_add(amount); - - if new_position < value.len() + 1 { - self.cursor_position = new_position; - } + self.cursor_position = match current { + Cursor::Index(index) => { + Cursor::Index(index.saturating_add(amount).min(value.len())) + } + Cursor::Selection { .. } => Cursor::Index(current.right()), + }; } /// Moves the cursor of a [`TextInput`] to the previous start of a word. @@ -576,7 +740,8 @@ impl State { pub(crate) fn move_cursor_left_by_words(&mut self, value: &Value) { let current = self.cursor_position(value); - self.cursor_position = value.previous_start_of_word(current); + self.cursor_position = + Cursor::Index(value.previous_start_of_word(current.left())); } /// Moves the cursor of a [`TextInput`] to the next end of a word. @@ -585,14 +750,15 @@ impl State { pub(crate) fn move_cursor_right_by_words(&mut self, value: &Value) { let current = self.cursor_position(value); - self.cursor_position = value.next_end_of_word(current); + self.cursor_position = + Cursor::Index(value.next_end_of_word(current.right())); } /// Moves the cursor of a [`TextInput`] to the end. /// /// [`TextInput`]: struct.TextInput.html pub(crate) fn move_cursor_to_end(&mut self, value: &Value) { - self.cursor_position = value.len(); + self.cursor_position = Cursor::Index(value.len()); } } @@ -711,6 +877,11 @@ impl Value { let _ = self.graphemes.remove(index); } + /// Removes the graphemes from `start` to `end`. + pub fn remove_many(&mut self, start: usize, end: usize) { + let _ = self.graphemes.splice(start..end, std::iter::empty()); + } + /// Returns a new [`Value`] with all its graphemes replaced with the /// dot ('•') character. /// diff --git a/style/src/text_input.rs b/style/src/text_input.rs index c5123b20..1cb72364 100644 --- a/style/src/text_input.rs +++ b/style/src/text_input.rs @@ -33,6 +33,8 @@ pub trait StyleSheet { fn value_color(&self) -> Color; + fn selection_color(&self) -> Color; + /// Produces the style of an hovered text input. fn hovered(&self) -> Style { self.focused() @@ -65,6 +67,10 @@ impl StyleSheet for Default { fn value_color(&self) -> Color { Color::from_rgb(0.3, 0.3, 0.3) } + + fn selection_color(&self) -> Color { + Color::from_rgb(0.8, 0.8, 1.0) + } } impl std::default::Default for Box { diff --git a/wgpu/src/renderer/widget/text_input.rs b/wgpu/src/renderer/widget/text_input.rs index e2a1b3a9..72da85f4 100644 --- a/wgpu/src/renderer/widget/text_input.rs +++ b/wgpu/src/renderer/widget/text_input.rs @@ -46,7 +46,7 @@ impl text_input::Renderer for Renderer { text_bounds, value, size, - state.cursor_position(value), + state.cursor_position(value).position(), font, ); @@ -111,15 +111,55 @@ impl text_input::Renderer for Renderer { }; let (contents_primitive, offset) = if state.is_focused() { + let cursor = state.cursor_position(value); let (text_value_width, offset) = measure_cursor_and_scroll_offset( self, text_bounds, value, size, - state.cursor_position(value), + cursor.position(), font, ); + let selection = match cursor { + text_input::Cursor::Index(_) => Primitive::None, + text_input::Cursor::Selection { .. } => { + let (cursor_left_offset, _) = + measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + cursor.left(), + font, + ); + let (cursor_right_offset, _) = + measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + cursor.right(), + font, + ); + let width = cursor_right_offset - cursor_left_offset; + Primitive::Quad { + bounds: Rectangle { + x: text_bounds.x + cursor_left_offset, + y: text_bounds.y, + width, + height: text_bounds.height, + }, + background: Background::Color( + style_sheet.selection_color(), + ), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + } + } + }; + let cursor = Primitive::Quad { bounds: Rectangle { x: text_bounds.x + text_value_width, @@ -135,7 +175,7 @@ impl text_input::Renderer for Renderer { ( Primitive::Group { - primitives: vec![text_value, cursor], + primitives: vec![selection, text_value, cursor], }, Vector::new(offset as u32, 0), )