diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index d6f41b04..63ab9d62 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -355,6 +355,10 @@ mod style { fn value_color(&self) -> Color { Color::WHITE } + + fn selection_color(&self) -> Color { + ACTIVE + } } pub struct Button; diff --git a/native/src/input/mouse.rs b/native/src/input/mouse.rs index 69dc6b4c..7198b233 100644 --- a/native/src/input/mouse.rs +++ b/native/src/input/mouse.rs @@ -2,5 +2,8 @@ mod button; mod event; +pub mod click; + pub use button::Button; +pub use click::Click; pub use event::{Event, ScrollDelta}; diff --git a/native/src/input/mouse/click.rs b/native/src/input/mouse/click.rs new file mode 100644 index 00000000..d27bc67e --- /dev/null +++ b/native/src/input/mouse/click.rs @@ -0,0 +1,76 @@ +//! Track mouse clicks. +use crate::Point; +use std::time::Instant; + +/// A mouse click. +#[derive(Debug, Clone, Copy)] +pub struct Click { + kind: Kind, + position: Point, + time: Instant, +} + +/// The kind of mouse click. +#[derive(Debug, Clone, Copy)] +pub enum Kind { + /// A single click + Single, + + /// A double click + Double, + + /// A triple click + Triple, +} + +impl Kind { + fn next(&self) -> Kind { + match self { + Kind::Single => Kind::Double, + Kind::Double => Kind::Triple, + Kind::Triple => Kind::Double, + } + } +} + +impl Click { + /// Creates a new [`Click`] with the given position and previous last + /// [`Click`]. + /// + /// [`Click`]: struct.Click.html + pub fn new(position: Point, previous: Option) -> Click { + let time = Instant::now(); + + let kind = if let Some(previous) = previous { + if previous.is_consecutive(position, time) { + previous.kind.next() + } else { + Kind::Single + } + } else { + Kind::Single + }; + + Click { + kind, + position, + time, + } + } + + /// Returns the [`Kind`] of [`Click`]. + /// + /// [`Kind`]: enum.Kind.html + /// [`Click`]: struct.Click.html + pub fn kind(&self) -> Kind { + self.kind + } + + fn is_consecutive(&self, new_position: Point, time: Instant) -> bool { + self.position == new_position + && time + .checked_duration_since(self.time) + .map(|duration| duration.as_millis() <= 300) + .unwrap_or(false) + } +} diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index 0fcce5ad..9033a7da 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -114,10 +114,10 @@ impl text_input::Renderer for Null { fn offset( &self, _text_bounds: Rectangle, + _font: Font, _size: u16, _value: &text_input::Value, _state: &text_input::State, - _font: Font, ) -> f32 { 0.0 } @@ -127,8 +127,8 @@ impl text_input::Renderer for Null { _bounds: Rectangle, _text_bounds: Rectangle, _cursor_position: Point, - _size: u16, _font: Font, + _size: u16, _placeholder: &str, _value: &text_input::Value, _state: &text_input::State, diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index c068b895..c17a1d30 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -4,14 +4,27 @@ //! //! [`TextInput`]: struct.TextInput.html //! [`State`]: struct.State.html +mod editor; +mod value; + +pub mod cursor; + +pub use cursor::Cursor; +pub use value::Value; + +use editor::Editor; + use crate::{ - input::{keyboard, mouse, ButtonState}, + input::{ + keyboard, + mouse::{self, click}, + ButtonState, + }, layout, Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; use std::u32; -use unicode_segmentation::UnicodeSegmentation; /// A field that can be filled with text. /// @@ -209,6 +222,75 @@ where let text_layout = layout.children().next().unwrap(); let target = cursor_position.x - text_layout.bounds().x; + let click = mouse::Click::new( + cursor_position, + self.state.last_click, + ); + + match click.kind() { + click::Kind::Single => { + if target > 0.0 { + let value = if self.is_secure { + self.value.secure() + } else { + self.value.clone() + }; + + let position = renderer.find_cursor_position( + text_layout.bounds(), + self.font, + self.size, + &value, + &self.state, + target, + ); + + self.state.cursor.move_to(position); + } else { + self.state.cursor.move_to(0); + } + } + click::Kind::Double => { + if self.is_secure { + self.state.cursor.select_all(&self.value); + } else { + let position = renderer.find_cursor_position( + text_layout.bounds(), + self.font, + self.size, + &self.value, + &self.state, + target, + ); + + self.state.cursor.select_range( + self.value.previous_start_of_word(position), + self.value.next_end_of_word(position), + ); + } + } + click::Kind::Triple => { + self.state.cursor.select_all(&self.value); + } + } + + self.state.last_click = Some(click); + } + + self.state.is_dragging = is_clicked; + self.state.is_focused = is_clicked; + } + Event::Mouse(mouse::Event::Input { + button: mouse::Button::Left, + state: ButtonState::Released, + }) => { + self.state.is_dragging = false; + } + Event::Mouse(mouse::Event::CursorMoved { x, .. }) => { + if self.state.is_dragging { + 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() @@ -216,43 +298,33 @@ where self.value.clone() }; - let size = self.size.unwrap_or(renderer.default_size()); - - let offset = renderer.offset( + let position = renderer.find_cursor_position( text_layout.bounds(), - size, + self.font, + self.size, &value, &self.state, - self.font, + target, ); - self.state.cursor_position = find_cursor_position( - renderer, - target + offset, - &value, - size, - 0, - self.value.len(), - self.font, + self.state.cursor.select_range( + self.state.cursor.start(&value), + position, ); - } else { - self.state.cursor_position = 0; } } - - 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 mut editor = + Editor::new(&mut self.value, &mut self.state.cursor); - self.value.insert(cursor_position, c); - self.state.move_cursor_right(&self.value); + editor.insert(c); - let message = (self.on_change)(self.value.to_string()); + let message = (self.on_change)(editor.contents()); messages.push(message); } Event::Keyboard(keyboard::Event::Input { @@ -266,52 +338,74 @@ where } } keyboard::KeyCode::Backspace => { - let cursor_position = - self.state.cursor_position(&self.value); + let mut editor = + Editor::new(&mut self.value, &mut self.state.cursor); - if cursor_position > 0 { - self.state.move_cursor_left(&self.value); + editor.backspace(); - let _ = self.value.remove(cursor_position - 1); - - let message = (self.on_change)(self.value.to_string()); - messages.push(message); - } + let message = (self.on_change)(editor.contents()); + messages.push(message); } keyboard::KeyCode::Delete => { - let cursor_position = - self.state.cursor_position(&self.value); + let mut editor = + Editor::new(&mut self.value, &mut self.state.cursor); - if cursor_position < self.value.len() { - let _ = self.value.remove(cursor_position); + editor.delete(); - let message = (self.on_change)(self.value.to_string()); - messages.push(message); - } + let message = (self.on_change)(editor.contents()); + messages.push(message); } keyboard::KeyCode::Left => { if platform::is_jump_modifier_pressed(modifiers) && !self.is_secure { - self.state.move_cursor_left_by_words(&self.value); + if modifiers.shift { + self.state.cursor.select_left_by_words(&self.value); + } else { + self.state.cursor.move_left_by_words(&self.value); + } + } else if modifiers.shift { + self.state.cursor.select_left(&self.value) } else { - self.state.move_cursor_left(&self.value); + self.state.cursor.move_left(&self.value); } } keyboard::KeyCode::Right => { if platform::is_jump_modifier_pressed(modifiers) && !self.is_secure { - self.state.move_cursor_right_by_words(&self.value); + if modifiers.shift { + self.state + .cursor + .select_right_by_words(&self.value); + } else { + self.state.cursor.move_right_by_words(&self.value); + } + } else if modifiers.shift { + self.state.cursor.select_right(&self.value) } else { - self.state.move_cursor_right(&self.value); + self.state.cursor.move_right(&self.value); } } keyboard::KeyCode::Home => { - self.state.cursor_position = 0; + if modifiers.shift { + self.state.cursor.select_range( + self.state.cursor.start(&self.value), + 0, + ); + } else { + self.state.cursor.move_to(0); + } } keyboard::KeyCode::End => { - self.state.move_cursor_to_end(&self.value); + if modifiers.shift { + self.state.cursor.select_range( + self.state.cursor.start(&self.value), + self.value.len(), + ); + } else { + self.state.cursor.move_to(self.value.len()); + } } keyboard::KeyCode::V => { if platform::is_copy_paste_modifier_pressed(modifiers) { @@ -330,26 +424,27 @@ where } }; - let cursor_position = - self.state.cursor_position(&self.value); - - self.value - .insert_many(cursor_position, content.clone()); - - self.state.move_cursor_right_by_amount( - &self.value, - content.len(), + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, ); - self.state.is_pasting = Some(content); - let message = - (self.on_change)(self.value.to_string()); + editor.paste(content.clone()); + + let message = (self.on_change)(editor.contents()); messages.push(message); + + self.state.is_pasting = Some(content); } } else { self.state.is_pasting = None; } } + keyboard::KeyCode::A => { + if platform::is_copy_paste_modifier_pressed(modifiers) { + self.state.cursor.select_all(&self.value); + } + } _ => {} }, Event::Keyboard(keyboard::Event::Input { @@ -381,8 +476,8 @@ where bounds, text_bounds, cursor_position, - self.size.unwrap_or(renderer.default_size()), self.font, + self.size.unwrap_or(renderer.default_size()), &self.placeholder, &self.value.secure(), &self.state, @@ -393,8 +488,8 @@ where bounds, text_bounds, cursor_position, - self.size.unwrap_or(renderer.default_size()), self.font, + self.size.unwrap_or(renderer.default_size()), &self.placeholder, &self.value, &self.state, @@ -447,10 +542,10 @@ pub trait Renderer: crate::Renderer + Sized { fn offset( &self, text_bounds: Rectangle, + font: Font, size: u16, value: &Value, state: &State, - font: Font, ) -> f32; /// Draws a [`TextInput`]. @@ -471,13 +566,41 @@ pub trait Renderer: crate::Renderer + Sized { bounds: Rectangle, text_bounds: Rectangle, cursor_position: Point, - size: u16, font: Font, + size: u16, placeholder: &str, value: &Value, state: &State, style: &Self::Style, ) -> Self::Output; + + /// Computes the position of the text cursor at the given X coordinate of + /// a [`TextInput`]. + /// + /// [`TextInput`]: struct.TextInput.html + fn find_cursor_position( + &self, + text_bounds: Rectangle, + font: Font, + size: Option, + value: &Value, + state: &State, + x: f32, + ) -> usize { + let size = size.unwrap_or(self.default_size()); + + let offset = self.offset(text_bounds, font, size, &value, &state); + + find_cursor_position( + self, + &value, + font, + size, + x + offset, + 0, + value.len(), + ) + } } impl<'a, Message, Renderer> From> @@ -499,8 +622,10 @@ where #[derive(Debug, Default, Clone)] pub struct State { is_focused: bool, + is_dragging: bool, is_pasting: Option, - cursor_position: usize, + last_click: Option, + cursor: Cursor, // TODO: Add stateful horizontal scrolling offset } @@ -516,12 +641,12 @@ impl State { /// /// [`State`]: struct.State.html pub fn focused() -> Self { - use std::usize; - Self { is_focused: true, + is_dragging: false, is_pasting: None, - cursor_position: usize::MAX, + last_click: None, + cursor: Cursor::default(), } } @@ -532,207 +657,24 @@ impl State { self.is_focused } - /// Returns the cursor position of a [`TextInput`]. + /// Returns the [`Cursor`] of the [`TextInput`]. /// + /// [`Cursor`]: struct.Cursor.html /// [`TextInput`]: struct.TextInput.html - pub fn cursor_position(&self, value: &Value) -> usize { - self.cursor_position.min(value.len()) - } - - /// Moves the cursor of a [`TextInput`] to the left. - /// - /// [`TextInput`]: struct.TextInput.html - pub(crate) fn move_cursor_left(&mut self, value: &Value) { - let current = self.cursor_position(value); - - if current > 0 { - self.cursor_position = current - 1; - } - } - - /// Moves the cursor of a [`TextInput`] to the right. - /// - /// [`TextInput`]: struct.TextInput.html - pub(crate) fn move_cursor_right(&mut self, value: &Value) { - self.move_cursor_right_by_amount(value, 1) - } - - pub(crate) fn move_cursor_right_by_amount( - &mut self, - value: &Value, - 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; - } - } - - /// Moves the cursor of a [`TextInput`] to the previous start of a word. - /// - /// [`TextInput`]: struct.TextInput.html - 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); - } - - /// Moves the cursor of a [`TextInput`] to the next end of a word. - /// - /// [`TextInput`]: struct.TextInput.html - 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); - } - - /// 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(); - } -} - -/// The value of a [`TextInput`]. -/// -/// [`TextInput`]: struct.TextInput.html -// TODO: Reduce allocations, cache results (?) -#[derive(Debug, Clone)] -pub struct Value { - graphemes: Vec, -} - -impl Value { - /// Creates a new [`Value`] from a string slice. - /// - /// [`Value`]: struct.Value.html - pub fn new(string: &str) -> Self { - let graphemes = UnicodeSegmentation::graphemes(string, true) - .map(String::from) - .collect(); - - Self { graphemes } - } - - /// Returns the total amount of graphemes in the [`Value`]. - /// - /// [`Value`]: struct.Value.html - pub fn len(&self) -> usize { - self.graphemes.len() - } - - /// Returns the position of the previous start of a word from the given - /// grapheme `index`. - /// - /// [`Value`]: struct.Value.html - pub fn previous_start_of_word(&self, index: usize) -> usize { - let previous_string = - &self.graphemes[..index.min(self.graphemes.len())].concat(); - - UnicodeSegmentation::split_word_bound_indices(&previous_string as &str) - .filter(|(_, word)| !word.trim_start().is_empty()) - .next_back() - .map(|(i, previous_word)| { - index - - UnicodeSegmentation::graphemes(previous_word, true) - .count() - - UnicodeSegmentation::graphemes( - &previous_string[i + previous_word.len()..] as &str, - true, - ) - .count() - }) - .unwrap_or(0) - } - - /// Returns the position of the next end of a word from the given grapheme - /// `index`. - /// - /// [`Value`]: struct.Value.html - pub fn next_end_of_word(&self, index: usize) -> usize { - let next_string = &self.graphemes[index..].concat(); - - UnicodeSegmentation::split_word_bound_indices(&next_string as &str) - .filter(|(_, word)| !word.trim_start().is_empty()) - .next() - .map(|(i, next_word)| { - index - + UnicodeSegmentation::graphemes(next_word, true).count() - + UnicodeSegmentation::graphemes( - &next_string[..i] as &str, - true, - ) - .count() - }) - .unwrap_or(self.len()) - } - - /// Returns a new [`Value`] containing the graphemes until the given - /// `index`. - /// - /// [`Value`]: struct.Value.html - pub fn until(&self, index: usize) -> Self { - let graphemes = self.graphemes[..index.min(self.len())].to_vec(); - - Self { graphemes } - } - - /// Converts the [`Value`] into a `String`. - /// - /// [`Value`]: struct.Value.html - pub fn to_string(&self) -> String { - self.graphemes.concat() - } - - /// Inserts a new `char` at the given grapheme `index`. - pub fn insert(&mut self, index: usize, c: char) { - self.graphemes.insert(index, c.to_string()); - - self.graphemes = - UnicodeSegmentation::graphemes(&self.to_string() as &str, true) - .map(String::from) - .collect(); - } - - /// Inserts a bunch of graphemes at the given grapheme `index`. - pub fn insert_many(&mut self, index: usize, mut value: Value) { - let _ = self - .graphemes - .splice(index..index, value.graphemes.drain(..)); - } - - /// Removes the grapheme at the given `index`. - /// - /// [`Value`]: struct.Value.html - pub fn remove(&mut self, index: usize) { - let _ = self.graphemes.remove(index); - } - - /// Returns a new [`Value`] with all its graphemes replaced with the - /// dot ('•') character. - /// - /// [`Value`]: struct.Value.html - pub fn secure(&self) -> Self { - Self { - graphemes: std::iter::repeat(String::from("•")) - .take(self.graphemes.len()) - .collect(), - } + pub fn cursor(&self) -> Cursor { + self.cursor } } // TODO: Reduce allocations fn find_cursor_position( renderer: &Renderer, - target: f32, value: &Value, + font: Font, size: u16, + target: f32, start: usize, end: usize, - font: Font, ) -> usize { if start >= end { if start == 0 { @@ -760,22 +702,22 @@ fn find_cursor_position( if width > target { find_cursor_position( renderer, - target, value, + font, size, + target, start, start + index, - font, ) } else { find_cursor_position( renderer, - target, value, + font, size, + target, start + index + 1, end, - font, ) } } diff --git a/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs new file mode 100644 index 00000000..16e7a01b --- /dev/null +++ b/native/src/widget/text_input/cursor.rs @@ -0,0 +1,191 @@ +//! Track the cursor of a text input. +use crate::widget::text_input::Value; + +/// The cursor of a text input. +#[derive(Debug, Copy, Clone)] +pub struct Cursor { + state: State, +} + +/// The state of a [`Cursor`]. +/// +/// [`Cursor`]: struct.Cursor.html +#[derive(Debug, Copy, Clone)] +pub enum State { + /// Cursor without a selection + Index(usize), + + /// Cursor selecting a range of text + Selection { + /// The start of the selection + start: usize, + /// The end of the selection + end: usize, + }, +} + +impl Default for Cursor { + fn default() -> Self { + Cursor { + state: State::Index(0), + } + } +} + +impl Cursor { + /// Returns the [`State`] of the [`Cursor`]. + /// + /// [`State`]: struct.State.html + /// [`Cursor`]: struct.Cursor.html + pub fn state(&self, value: &Value) -> State { + match self.state { + State::Index(index) => State::Index(index.min(value.len())), + State::Selection { start, end } => { + let start = start.min(value.len()); + let end = end.min(value.len()); + + if start == end { + State::Index(start) + } else { + State::Selection { start, end } + } + } + } + } + + pub(crate) fn move_to(&mut self, position: usize) { + self.state = State::Index(position); + } + + pub(crate) fn move_right(&mut self, value: &Value) { + self.move_right_by_amount(value, 1) + } + + pub(crate) fn move_right_by_words(&mut self, value: &Value) { + self.move_to(value.next_end_of_word(self.right(value))) + } + + pub(crate) fn move_right_by_amount( + &mut self, + value: &Value, + amount: usize, + ) { + match self.state(value) { + State::Index(index) => { + self.move_to(index.saturating_add(amount).min(value.len())) + } + State::Selection { start, end } => self.move_to(end.max(start)), + } + } + + pub(crate) fn move_left(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) if index > 0 => self.move_to(index - 1), + State::Selection { start, end } => self.move_to(start.min(end)), + _ => self.move_to(0), + } + } + + pub(crate) fn move_left_by_words(&mut self, value: &Value) { + self.move_to(value.previous_start_of_word(self.left(value))); + } + + pub(crate) fn select_range(&mut self, start: usize, end: usize) { + if start == end { + self.state = State::Index(start); + } else { + self.state = State::Selection { start, end }; + } + } + + pub(crate) fn select_left(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) if index > 0 => { + self.select_range(index, index - 1) + } + State::Selection { start, end } if end > 0 => { + self.select_range(start, end - 1) + } + _ => (), + } + } + + pub(crate) fn select_right(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) if index < value.len() => { + self.select_range(index, index + 1) + } + State::Selection { start, end } if end < value.len() => { + self.select_range(start, end + 1) + } + _ => (), + } + } + + pub(crate) fn select_left_by_words(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) => { + self.select_range(index, value.previous_start_of_word(index)) + } + State::Selection { start, end } => { + self.select_range(start, value.previous_start_of_word(end)) + } + } + } + + pub(crate) fn select_right_by_words(&mut self, value: &Value) { + match self.state(value) { + State::Index(index) => { + self.select_range(index, value.next_end_of_word(index)) + } + State::Selection { start, end } => { + self.select_range(start, value.next_end_of_word(end)) + } + } + } + + pub(crate) fn select_all(&mut self, value: &Value) { + self.select_range(0, value.len()); + } + + pub(crate) fn start(&self, value: &Value) -> usize { + let start = match self.state { + State::Index(index) => index, + State::Selection { start, .. } => start, + }; + + start.min(value.len()) + } + + pub(crate) fn end(&self, value: &Value) -> usize { + let end = match self.state { + State::Index(index) => index, + State::Selection { end, .. } => end, + }; + + end.min(value.len()) + } + + pub(crate) fn selection(&self) -> Option<(usize, usize)> { + match self.state { + State::Selection { start, end } => { + Some((start.min(end), start.max(end))) + } + _ => None, + } + } + + fn left(&self, value: &Value) -> usize { + match self.state(value) { + State::Index(index) => index, + State::Selection { start, end } => start.min(end), + } + } + + fn right(&self, value: &Value) -> usize { + match self.state(value) { + State::Index(index) => index, + State::Selection { start, end } => start.max(end), + } + } +} diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs new file mode 100644 index 00000000..71c4f292 --- /dev/null +++ b/native/src/widget/text_input/editor.rs @@ -0,0 +1,78 @@ +use crate::text_input::{Cursor, Value}; + +pub struct Editor<'a> { + value: &'a mut Value, + cursor: &'a mut Cursor, +} + +impl<'a> Editor<'a> { + pub fn new(value: &'a mut Value, cursor: &'a mut Cursor) -> Editor<'a> { + Editor { value, cursor } + } + + pub fn contents(&self) -> String { + self.value.to_string() + } + + pub fn insert(&mut self, character: char) { + match self.cursor.selection() { + Some((left, right)) => { + self.cursor.move_left(self.value); + self.value.remove_many(left, right); + } + _ => (), + } + + self.value.insert(self.cursor.end(self.value), character); + self.cursor.move_right(self.value); + } + + pub fn paste(&mut self, content: Value) { + let length = content.len(); + + match self.cursor.selection() { + Some((left, right)) => { + self.cursor.move_left(self.value); + self.value.remove_many(left, right); + } + _ => (), + } + + self.value.insert_many(self.cursor.end(self.value), content); + + self.cursor.move_right_by_amount(self.value, length); + } + + pub fn backspace(&mut self) { + match self.cursor.selection() { + Some((start, end)) => { + self.cursor.move_left(self.value); + self.value.remove_many(start, end); + } + None => { + let start = self.cursor.start(self.value); + + if start > 0 { + self.cursor.move_left(self.value); + + let _ = self.value.remove(start - 1); + } + } + } + } + + pub fn delete(&mut self) { + match self.cursor.selection() { + Some(_) => { + self.backspace(); + } + None => { + let end = self.cursor.end(self.value); + + if end < self.value.len() { + let _ = self.value.remove(end); + } + } + } + } +} diff --git a/native/src/widget/text_input/value.rs b/native/src/widget/text_input/value.rs new file mode 100644 index 00000000..1e9ba45b --- /dev/null +++ b/native/src/widget/text_input/value.rs @@ -0,0 +1,134 @@ +use unicode_segmentation::UnicodeSegmentation; + +/// The value of a [`TextInput`]. +/// +/// [`TextInput`]: struct.TextInput.html +// TODO: Reduce allocations, cache results (?) +#[derive(Debug, Clone)] +pub struct Value { + graphemes: Vec, +} + +impl Value { + /// Creates a new [`Value`] from a string slice. + /// + /// [`Value`]: struct.Value.html + pub fn new(string: &str) -> Self { + let graphemes = UnicodeSegmentation::graphemes(string, true) + .map(String::from) + .collect(); + + Self { graphemes } + } + + /// Returns the total amount of graphemes in the [`Value`]. + /// + /// [`Value`]: struct.Value.html + pub fn len(&self) -> usize { + self.graphemes.len() + } + + /// Returns the position of the previous start of a word from the given + /// grapheme `index`. + /// + /// [`Value`]: struct.Value.html + pub fn previous_start_of_word(&self, index: usize) -> usize { + let previous_string = + &self.graphemes[..index.min(self.graphemes.len())].concat(); + + UnicodeSegmentation::split_word_bound_indices(&previous_string as &str) + .filter(|(_, word)| !word.trim_start().is_empty()) + .next_back() + .map(|(i, previous_word)| { + index + - UnicodeSegmentation::graphemes(previous_word, true) + .count() + - UnicodeSegmentation::graphemes( + &previous_string[i + previous_word.len()..] as &str, + true, + ) + .count() + }) + .unwrap_or(0) + } + + /// Returns the position of the next end of a word from the given grapheme + /// `index`. + /// + /// [`Value`]: struct.Value.html + pub fn next_end_of_word(&self, index: usize) -> usize { + let next_string = &self.graphemes[index..].concat(); + + UnicodeSegmentation::split_word_bound_indices(&next_string as &str) + .filter(|(_, word)| !word.trim_start().is_empty()) + .next() + .map(|(i, next_word)| { + index + + UnicodeSegmentation::graphemes(next_word, true).count() + + UnicodeSegmentation::graphemes( + &next_string[..i] as &str, + true, + ) + .count() + }) + .unwrap_or(self.len()) + } + + /// Returns a new [`Value`] containing the graphemes until the given + /// `index`. + /// + /// [`Value`]: struct.Value.html + pub fn until(&self, index: usize) -> Self { + let graphemes = self.graphemes[..index.min(self.len())].to_vec(); + + Self { graphemes } + } + + /// Converts the [`Value`] into a `String`. + /// + /// [`Value`]: struct.Value.html + pub fn to_string(&self) -> String { + self.graphemes.concat() + } + + /// Inserts a new `char` at the given grapheme `index`. + pub fn insert(&mut self, index: usize, c: char) { + self.graphemes.insert(index, c.to_string()); + + self.graphemes = + UnicodeSegmentation::graphemes(&self.to_string() as &str, true) + .map(String::from) + .collect(); + } + + /// Inserts a bunch of graphemes at the given grapheme `index`. + pub fn insert_many(&mut self, index: usize, mut value: Value) { + let _ = self + .graphemes + .splice(index..index, value.graphemes.drain(..)); + } + + /// Removes the grapheme at the given `index`. + /// + /// [`Value`]: struct.Value.html + pub fn remove(&mut self, index: usize) { + 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. + /// + /// [`Value`]: struct.Value.html + pub fn secure(&self) -> Self { + Self { + graphemes: std::iter::repeat(String::from("•")) + .take(self.graphemes.len()) + .collect(), + } + } +} 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..170ac3c5 100644 --- a/wgpu/src/renderer/widget/text_input.rs +++ b/wgpu/src/renderer/widget/text_input.rs @@ -1,8 +1,9 @@ use crate::{text_input::StyleSheet, Primitive, Renderer}; use iced_native::{ - text_input, Background, Color, Font, HorizontalAlignment, MouseCursor, - Point, Rectangle, Size, Vector, VerticalAlignment, + text_input::{self, cursor}, + Background, Color, Font, HorizontalAlignment, MouseCursor, Point, + Rectangle, Size, Vector, VerticalAlignment, }; use std::f32; @@ -35,18 +36,25 @@ impl text_input::Renderer for Renderer { fn offset( &self, text_bounds: Rectangle, + font: Font, size: u16, value: &text_input::Value, state: &text_input::State, - font: Font, ) -> f32 { if state.is_focused() { + let cursor = state.cursor(); + + let focus_position = match cursor.state(value) { + cursor::State::Index(i) => i, + cursor::State::Selection { end, .. } => end, + }; + let (_, offset) = measure_cursor_and_scroll_offset( self, text_bounds, value, size, - state.cursor_position(value), + focus_position, font, ); @@ -61,8 +69,8 @@ impl text_input::Renderer for Renderer { bounds: Rectangle, text_bounds: Rectangle, cursor_position: Point, - size: u16, font: Font, + size: u16, placeholder: &str, value: &text_input::Value, state: &text_input::State, @@ -111,31 +119,91 @@ impl text_input::Renderer for Renderer { }; let (contents_primitive, offset) = if state.is_focused() { - let (text_value_width, offset) = measure_cursor_and_scroll_offset( - self, - text_bounds, - value, - size, - state.cursor_position(value), - font, - ); + let cursor = state.cursor(); - let cursor = Primitive::Quad { - bounds: Rectangle { - x: text_bounds.x + text_value_width, - y: text_bounds.y, - width: 1.0, - height: text_bounds.height, - }, - background: Background::Color(style_sheet.value_color()), - border_radius: 0, - border_width: 0, - border_color: Color::TRANSPARENT, + let (cursor_primitive, offset) = match cursor.state(value) { + cursor::State::Index(position) => { + let (text_value_width, offset) = + measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + position, + font, + ); + + ( + Primitive::Quad { + bounds: Rectangle { + x: text_bounds.x + text_value_width, + y: text_bounds.y, + width: 1.0, + height: text_bounds.height, + }, + background: Background::Color( + style_sheet.value_color(), + ), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + offset, + ) + } + cursor::State::Selection { start, end } => { + let left = start.min(end); + let right = end.max(start); + + let (left_position, left_offset) = + measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + left, + font, + ); + + let (right_position, right_offset) = + measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + right, + font, + ); + + let width = right_position - left_position; + + ( + Primitive::Quad { + bounds: Rectangle { + x: text_bounds.x + left_position, + 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, + }, + if end == right { + right_offset + } else { + left_offset + }, + ) + } }; ( Primitive::Group { - primitives: vec![text_value, cursor], + primitives: vec![cursor_primitive, text_value], }, Vector::new(offset as u32, 0), )