diff options
-rw-r--r-- | examples/styling/src/main.rs | 4 | ||||
-rw-r--r-- | native/src/input/mouse.rs | 3 | ||||
-rw-r--r-- | native/src/input/mouse/click.rs | 73 | ||||
-rw-r--r-- | native/src/widget/text_input.rs | 308 | ||||
-rw-r--r-- | native/src/widget/text_input/cursor.rs | 175 | ||||
-rw-r--r-- | style/src/text_input.rs | 6 | ||||
-rw-r--r-- | wgpu/src/renderer/widget/text_input.rs | 45 |
7 files changed, 505 insertions, 109 deletions
diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index d6f41b04..dfa168b6 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 { + Color::BLACK + } } 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..60ae056b --- /dev/null +++ b/native/src/input/mouse/click.rs @@ -0,0 +1,73 @@ +//! 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>) -> 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.duration_since(self.time).as_millis() <= 300 + } +} diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index c068b895..c379f0d1 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -4,10 +4,17 @@ //! //! [`TextInput`]: struct.TextInput.html //! [`State`]: struct.State.html +mod cursor; use crate::{ - input::{keyboard, mouse, ButtonState}, - layout, Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, - Rectangle, Size, Widget, + input::{ + keyboard, + mouse::{self, click}, + ButtonState, + }, + layout, + widget::text_input::cursor::Cursor, + Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, Rectangle, + Size, Widget, }; use std::u32; @@ -209,6 +216,80 @@ 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 size = self + .size + .unwrap_or(renderer.default_size()); + + let offset = renderer.offset( + text_layout.bounds(), + size, + &value, + &self.state, + self.font, + ); + + self.state.cursor.move_to( + find_cursor_position( + renderer, + target + offset, + &value, + size, + 0, + self.value.len(), + self.font, + ), + ); + } else { + self.state.cursor.move_to(0); + } + } + click::Kind::Double => { + if self.is_secure { + self.state.cursor.select_all(&self.value); + } else { + let end = self.state.cursor.end(); + self.state.cursor.select_range( + self.value.previous_start_of_word(end), + self.value.next_end_of_word(end), + ); + } + } + click::Kind::Triple => { + self.state.cursor.select_all(&self.value); + } + } + + self.state.last_click = Some(click); + } + + 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() @@ -226,7 +307,7 @@ where self.font, ); - self.state.cursor_position = find_cursor_position( + let pos = find_cursor_position( renderer, target + offset, &value, @@ -235,22 +316,28 @@ where self.value.len(), self.font, ); - } else { - self.state.cursor_position = 0; + + self.state + .cursor + .select_range(self.state.cursor.start(), pos); } } - - 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); - - self.value.insert(cursor_position, c); - self.state.move_cursor_right(&self.value); + match self.state.cursor.selection_position() { + Some((left, right)) => { + self.value.remove_many(left, right); + self.state.cursor.move_left(); + } + _ => (), + } + self.value + .insert(self.state.cursor.end().min(self.value.len()), c); + self.state.cursor.move_right(&self.value); let message = (self.on_change)(self.value.to_string()); messages.push(message); @@ -266,52 +353,91 @@ where } } keyboard::KeyCode::Backspace => { - let cursor_position = - 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 self.state.cursor.selection_position() { + Some((start, end)) => { + self.value.remove_many(start, end); + self.state.cursor.move_left(); + } + None => { + if self.state.cursor.start().min(self.value.len()) + > 0 + { + self.state.cursor.move_left(); + let _ = self + .value + .remove(self.state.cursor.start()); + } + } } + let message = (self.on_change)(self.value.to_string()); + messages.push(message); } keyboard::KeyCode::Delete => { - let cursor_position = - 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 self.state.cursor.selection_position() { + Some((start, end)) => { + self.value.remove_many(start, end); + self.state.cursor.move_left(); + } + None => { + if self.state.cursor.end() < self.value.len() { + let _ = + self.value.remove(self.state.cursor.end()); + } + } } + let message = (self.on_change)(self.value.to_string()); + 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() } else { - self.state.move_cursor_left(&self.value); + self.state.cursor.move_left(); } } 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(), 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.len(), + ); + } else { + self.state.cursor.move_to(self.value.len()); + } } keyboard::KeyCode::V => { if platform::is_copy_paste_modifier_pressed(modifiers) { @@ -330,13 +456,20 @@ where } }; - let cursor_position = - self.state.cursor_position(&self.value); + match self.state.cursor.selection_position() { + Some((left, right)) => { + self.value.remove_many(left, right); + self.state.cursor.move_left(); + } + _ => (), + } - self.value - .insert_many(cursor_position, content.clone()); + self.value.insert_many( + self.state.cursor.end().min(self.value.len()), + content.clone(), + ); - self.state.move_cursor_right_by_amount( + self.state.cursor.move_right_by_amount( &self.value, content.len(), ); @@ -350,6 +483,20 @@ where self.state.is_pasting = None; } } + // I think this doesn't work with the current version of the clipboard lib + /*keyboard::KeyCode::C => { + if platform::is_copy_paste_modifier_pressed(modifiers) { + match self.state.cursor.selection_position() { + None => (), + Some((left, right)) => () + } + } + }*/ + keyboard::KeyCode::A => { + if platform::is_copy_paste_modifier_pressed(modifiers) { + self.state.cursor.select_all(&self.value); + } + } _ => {} }, Event::Keyboard(keyboard::Event::Input { @@ -499,8 +646,10 @@ where #[derive(Debug, Default, Clone)] pub struct State { is_focused: bool, + is_pressed: bool, is_pasting: Option<Value>, - cursor_position: usize, + last_click: Option<mouse::Click>, + cursor: Cursor, // TODO: Add stateful horizontal scrolling offset } @@ -516,12 +665,12 @@ impl State { /// /// [`State`]: struct.State.html pub fn focused() -> Self { - use std::usize; - Self { is_focused: true, + is_pressed: false, is_pasting: None, - cursor_position: usize::MAX, + last_click: None, + cursor: Cursor::default(), } } @@ -532,67 +681,9 @@ impl State { self.is_focused } - /// 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()) - } - - /// 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(); + /// getter for cursor + pub fn cursor(&self) -> Cursor { + self.cursor } } @@ -711,6 +802,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/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs new file mode 100644 index 00000000..92fd2029 --- /dev/null +++ b/native/src/widget/text_input/cursor.rs @@ -0,0 +1,175 @@ +use crate::widget::text_input::Value; + +#[derive(Debug, Copy, Clone)] +enum State { + Index(usize), + Selection { start: usize, end: usize }, +} + +#[derive(Debug, Copy, Clone)] +pub struct Cursor { + state: State, +} + +impl Default for Cursor { + fn default() -> Self { + Cursor { + state: State::Index(0), + } + } +} + +impl Cursor { + /* index move methods */ + pub fn move_to(&mut self, position: usize) { + self.state = State::Index(position); + } + + pub fn move_right(&mut self, value: &Value) { + self.move_right_by_amount(value, 1) + } + + pub fn move_right_by_words(&mut self, value: &Value) { + self.move_to(value.next_end_of_word(self.right())) + } + + pub fn move_right_by_amount(&mut self, value: &Value, amount: usize) { + match self.state { + State::Index(index) => { + self.move_to(index.saturating_add(amount).min(value.len())) + } + State::Selection { .. } => self.move_to(self.right()), + } + } + + pub fn move_left(&mut self) { + match self.state { + State::Index(index) if index > 0 => self.move_to(index - 1), + State::Selection { .. } => self.move_to(self.left()), + _ => self.move_to(0), + } + } + + pub fn move_left_by_words(&mut self, value: &Value) { + self.move_to(value.previous_start_of_word(self.right())); + } + /* end of index move methods */ + + /* expand/shrink selection */ + pub fn select_range(&mut self, start: usize, end: usize) { + if start != end { + self.state = State::Selection { start, end }; + } else { + self.state = State::Index(start); + } + } + + pub fn select_left(&mut self) { + match self.state { + 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 fn select_right(&mut self, value: &Value) { + match self.state { + 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 fn select_left_by_words(&mut self, value: &Value) { + match self.state { + 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 fn select_right_by_words(&mut self, value: &Value) { + match self.state { + 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 fn select_all(&mut self, value: &Value) { + self.select_range(0, value.len()); + } + /* end of selection section */ + + /* helpers */ + // get start position of selection (can be left OR right boundary of selection) or index + pub(crate) fn start(&self) -> usize { + match self.state { + State::Index(index) => index, + State::Selection { start, .. } => start, + } + } + + // get end position of selection (can be left OR right boundary of selection) or index + pub fn end(&self) -> usize { + match self.state { + State::Index(index) => index, + State::Selection { end, .. } => end, + } + } + + // get left boundary of selection or index + pub fn left(&self) -> usize { + match self.state { + State::Index(index) => index, + State::Selection { start, end } => start.min(end), + } + } + + // get right boundary of selection or index + pub fn right(&self) -> usize { + match self.state { + State::Index(index) => index, + State::Selection { start, end } => start.max(end), + } + } + + pub fn cursor_position(&self, value: &Value) -> usize { + match self.state { + State::Index(index) => index.min(value.len()), + State::Selection { end, .. } => end.min(value.len()), + } + } + + // returns Option of left and right border of selection + // a second method that returns start and end may be useful (see below) + pub fn selection_position(&self) -> Option<(usize, usize)> { + match self.state { + State::Selection { start, end } => { + Some((start.min(end), start.max(end))) + } + _ => None, + } + } + + /* pub fn selection_position(&self) -> Option<(usize, usize)> { + match self.state { + State::Selection { start, end } => Some((start, end)), + _ => None, + } + } */ +} 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<dyn StyleSheet> { diff --git a/wgpu/src/renderer/widget/text_input.rs b/wgpu/src/renderer/widget/text_input.rs index e2a1b3a9..fa108d68 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().cursor_position(value), font, ); @@ -116,10 +116,49 @@ impl text_input::Renderer for Renderer { text_bounds, value, size, - state.cursor_position(value), + state.cursor().cursor_position(value), font, ); + let selection = match state.cursor().selection_position() { + None => Primitive::None, + Some(_) => { + let (cursor_left_offset, _) = + measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + state.cursor().left(), + font, + ); + let (cursor_right_offset, _) = + measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + state.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 +174,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), ) |