From b2344a852e98af734e9d8f7fe3d493f9e82ca80c Mon Sep 17 00:00:00 2001 From: FabianLars Date: Sat, 22 Feb 2020 21:33:45 +0100 Subject: [PATCH 01/27] 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), ) From 33ca29f3954960800da2f5e35068b31fe6c1c586 Mon Sep 17 00:00:00 2001 From: FabianLars Date: Sat, 22 Feb 2020 22:17:51 +0100 Subject: [PATCH 02/27] ctrl + a selection for text input --- native/src/widget/text_input.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index bfe5244f..4aed2767 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -439,6 +439,16 @@ where self.state.is_pasting = None; } } + keyboard::KeyCode::A => { + if platform::is_copy_paste_modifier_pressed(modifiers) { + self.state.cursor_position = { + Cursor::Selection { + start: 0, + end: self.value.len(), + } + } + } + } _ => {} }, Event::Keyboard(keyboard::Event::Input { From f72b1f8c45b55c48210161568ee8a916706ec00a Mon Sep 17 00:00:00 2001 From: FabianLars Date: Sun, 23 Feb 2020 00:26:15 +0100 Subject: [PATCH 03/27] double click for word selection 3 clicks to select all --- native/src/widget/text_input.rs | 40 ++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 4aed2767..96f9d96f 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -209,7 +209,34 @@ where let text_layout = layout.children().next().unwrap(); let target = cursor_position.x - text_layout.bounds().x; - if target > 0.0 { + if cursor_position + == self + .state + .last_position + .unwrap_or(Point { x: 0.0, y: 0.0 }) + && self.state.click_count < 2 + { + self.state.click_count += 1; + + if self.state.click_count == 1 { + let current = + self.state.cursor_position(&self.value); + + self.state.cursor_position = Cursor::Selection { + start: self + .value + .previous_start_of_word(current.left()), + end: self + .value + .next_end_of_word(current.right()), + } + } else if self.state.click_count == 2 { + self.state.cursor_position = Cursor::Selection { + start: 0, + end: self.value.len(), + } + } + } else if target > 0.0 { let value = if self.is_secure { self.value.secure() } else { @@ -236,7 +263,13 @@ where self.value.len(), self.font, )); + self.state.click_count = 0; + self.state.last_position = + Option::from(cursor_position); } else { + self.state.click_count = 0; + self.state.last_position = + Option::from(cursor_position); self.state.cursor_position = Cursor::Index(0); } } @@ -601,6 +634,9 @@ pub struct State { is_pressed: bool, is_pasting: Option, cursor_position: Cursor, + /// Double- / Tripleclick + click_count: usize, + last_position: Option, // TODO: Add stateful horizontal scrolling offset } @@ -686,6 +722,8 @@ impl State { is_pressed: false, is_pasting: None, cursor_position: Cursor::Index(usize::MAX), + click_count: 0, + last_position: None, } } From 883843a72df44bf420f43771165dd8e7c9312e17 Mon Sep 17 00:00:00 2001 From: FabianLars Date: Sun, 23 Feb 2020 01:23:57 +0100 Subject: [PATCH 04/27] max time window for double click --- native/src/widget/text_input.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 96f9d96f..22913289 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -10,7 +10,10 @@ use crate::{ Rectangle, Size, Widget, }; -use std::u32; +use std::{ + time::{Duration, SystemTime}, + u32, +}; use unicode_segmentation::UnicodeSegmentation; /// A field that can be filled with text. @@ -215,6 +218,15 @@ where .last_position .unwrap_or(Point { x: 0.0, y: 0.0 }) && self.state.click_count < 2 + && SystemTime::now() + .duration_since( + self.state + .last_timestamp + .unwrap_or(SystemTime::now()), + ) + .unwrap_or(Duration::from_secs(1)) + .as_millis() + <= 500 { self.state.click_count += 1; @@ -236,6 +248,9 @@ where end: self.value.len(), } } + + self.state.last_timestamp = + Option::from(SystemTime::now()); } else if target > 0.0 { let value = if self.is_secure { self.value.secure() @@ -266,6 +281,8 @@ where self.state.click_count = 0; self.state.last_position = Option::from(cursor_position); + self.state.last_timestamp = + Option::from(SystemTime::now()); } else { self.state.click_count = 0; self.state.last_position = @@ -637,6 +654,7 @@ pub struct State { /// Double- / Tripleclick click_count: usize, last_position: Option, + last_timestamp: Option, // TODO: Add stateful horizontal scrolling offset } @@ -724,6 +742,7 @@ impl State { cursor_position: Cursor::Index(usize::MAX), click_count: 0, last_position: None, + last_timestamp: None, } } From e8bf0fc0997740096cad74daa4b5a338ce808aad Mon Sep 17 00:00:00 2001 From: FabianLars Date: Sun, 23 Feb 2020 11:25:48 +0100 Subject: [PATCH 05/27] styling example fixed (added selection_color) --- examples/styling/src/main.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 47408624..e97389e9 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -354,6 +354,10 @@ mod style { fn value_color(&self) -> Color { Color::WHITE } + + fn selection_color(&self) -> Color { + Color::BLACK + } } pub struct Button; From 190dcef1553efa181b68caa91b109f83fe289412 Mon Sep 17 00:00:00 2001 From: FabianLars Date: Mon, 24 Feb 2020 04:14:32 +0100 Subject: [PATCH 06/27] Text Selection completely rewritten --- native/src/widget/text_input.rs | 581 ++++++++++++------------- wgpu/src/renderer/widget/text_input.rs | 47 +- 2 files changed, 321 insertions(+), 307 deletions(-) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 22913289..59d86f63 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -10,10 +10,7 @@ use crate::{ Rectangle, Size, Widget, }; -use std::{ - time::{Duration, SystemTime}, - u32, -}; +use std::u32; use unicode_segmentation::UnicodeSegmentation; /// A field that can be filled with text. @@ -212,82 +209,50 @@ where let text_layout = layout.children().next().unwrap(); let target = cursor_position.x - text_layout.bounds().x; - if cursor_position - == self - .state - .last_position - .unwrap_or(Point { x: 0.0, y: 0.0 }) - && self.state.click_count < 2 - && SystemTime::now() - .duration_since( - self.state - .last_timestamp - .unwrap_or(SystemTime::now()), - ) - .unwrap_or(Duration::from_secs(1)) - .as_millis() - <= 500 - { - self.state.click_count += 1; + match self.state.cursor.process_click(cursor_position) { + 1 => self.state.cursor.select_range( + self.value.previous_start_of_word( + self.state.cursor.end(), + ), + self.value + .next_end_of_word(self.state.cursor.end()), + ), + 2 => self.state.cursor.select_all(self.value.len()), + _ => { + if target > 0.0 { + let value = if self.is_secure { + self.value.secure() + } else { + self.value.clone() + }; - if self.state.click_count == 1 { - let current = - self.state.cursor_position(&self.value); + let size = self + .size + .unwrap_or(renderer.default_size()); - self.state.cursor_position = Cursor::Selection { - start: self - .value - .previous_start_of_word(current.left()), - end: self - .value - .next_end_of_word(current.right()), - } - } else if self.state.click_count == 2 { - self.state.cursor_position = Cursor::Selection { - start: 0, - end: self.value.len(), + 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); } } - - self.state.last_timestamp = - Option::from(SystemTime::now()); - } else 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_position = - Cursor::Index(find_cursor_position( - renderer, - target + offset, - &value, - size, - 0, - self.value.len(), - self.font, - )); - self.state.click_count = 0; - self.state.last_position = - Option::from(cursor_position); - self.state.last_timestamp = - Option::from(SystemTime::now()); - } else { - self.state.click_count = 0; - self.state.last_position = - Option::from(cursor_position); - self.state.cursor_position = Cursor::Index(0); } } @@ -332,13 +297,9 @@ where self.font, ); - 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 + .cursor + .select_range(self.state.cursor.start(), pos); } } } @@ -347,19 +308,18 @@ where && self.state.is_pasting.is_none() && !c.is_control() => { - let cursor = self.state.cursor_position(&self.value); - - 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); - } + if !self.state.cursor.is_selection() { + self.value.insert(self.state.cursor.end(), c); + } else { + self.value.remove_many( + self.state.cursor.left(), + self.state.cursor.right(), + ); + self.state.cursor.move_left(); + self.value.insert(self.state.cursor.end(), c); } - self.state.move_cursor_right(&self.value); + + self.state.cursor.move_right(&self.value); let message = (self.on_change)(self.value.to_string()); messages.push(message); @@ -375,71 +335,66 @@ where } } keyboard::KeyCode::Backspace => { - let cursor = self.state.cursor_position(&self.value); - - match cursor { - Cursor::Index(index) if index > 0 => { - self.state.move_cursor_left(&self.value); - let _ = self.value.remove(index - 1); + if !self.state.cursor.is_selection() { + if self.state.cursor.start() > 0 { + self.state.cursor.move_left(); + let _ = + self.value.remove(self.state.cursor.end() - 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); - } - _ => {} + } else { + self.value.remove_many( + self.state.cursor.left(), + self.state.cursor.right(), + ); + self.state.cursor.move_left(); + let message = (self.on_change)(self.value.to_string()); + messages.push(message); } } keyboard::KeyCode::Delete => { - let cursor = self.state.cursor_position(&self.value); - - match cursor { - Cursor::Index(index) if index < self.value.len() => { - let _ = self.value.remove(index); + if !self.state.cursor.is_selection() { + 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); } - 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); - } - _ => {} + } else { + self.value.remove_many( + self.state.cursor.left(), + self.state.cursor.right(), + ); + self.state.cursor.move_left(); + 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); + self.state.cursor.move_left_by_words(&self.value); } 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); + self.state.cursor.move_right_by_words(&self.value); } else { - self.state.move_cursor_right(&self.value); + self.state.cursor.move_right(&self.value); } } keyboard::KeyCode::Home => { - self.state.cursor_position = Cursor::Index(0); + self.state.cursor.move_to(0); } keyboard::KeyCode::End => { - self.state.move_cursor_to_end(&self.value); + self.state.cursor.move_to(self.value.len()); } keyboard::KeyCode::V => { if platform::is_copy_paste_modifier_pressed(modifiers) { @@ -458,24 +413,20 @@ where } }; - let cursor_position = - self.state.cursor_position(&self.value); + if self.state.cursor.is_selection() { + self.value.remove_many( + self.state.cursor.left(), + self.state.cursor.right(), + ); + self.state.cursor.move_left(); + } - 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(insert_position, content.clone()); + self.value.insert_many( + self.state.cursor.end(), + content.clone(), + ); - self.state.move_cursor_right_by_amount( + self.state.cursor.move_right_by_amount( &self.value, content.len(), ); @@ -491,12 +442,7 @@ where } keyboard::KeyCode::A => { if platform::is_copy_paste_modifier_pressed(modifiers) { - self.state.cursor_position = { - Cursor::Selection { - start: 0, - end: self.value.len(), - } - } + self.state.cursor.select_range(0, self.value.len()); } } _ => {} @@ -650,77 +596,11 @@ pub struct State { is_focused: bool, is_pressed: bool, is_pasting: Option, - cursor_position: Cursor, - /// Double- / Tripleclick - click_count: usize, - last_position: Option, - last_timestamp: Option, + /// TODO: Compiler wants documentation here + pub cursor: cursor::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`]. /// @@ -733,16 +613,11 @@ 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: Cursor::Index(usize::MAX), - click_count: 0, - last_position: None, - last_timestamp: None, + cursor: cursor::Cursor::default(), } } @@ -752,81 +627,6 @@ impl State { pub fn is_focused(&self) -> bool { self.is_focused } - - /// Returns the cursor position of a [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html - 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. - /// - /// [`TextInput`]: struct.TextInput.html - pub(crate) fn move_cursor_left(&mut self, value: &Value) { - let current = self.cursor_position(value); - - self.cursor_position = match current { - Cursor::Index(index) if index > 0 => Cursor::Index(index - 1), - Cursor::Selection { .. } => Cursor::Index(current.left()), - _ => Cursor::Index(0), - } - } - - /// 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); - 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. - /// - /// [`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 = - Cursor::Index(value.previous_start_of_word(current.left())); - } - - /// 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 = - 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 = Cursor::Index(value.len()); - } } /// The value of a [`TextInput`]. @@ -1041,3 +841,182 @@ mod platform { } } } + +mod cursor { + use crate::widget::text_input::Value; + use iced_core::Point; + use std::time::{Duration, SystemTime}; + + /// Even the compiler bullies me for not writing documentation + #[derive(Debug, Copy, Clone)] + pub struct Cursor { + start: usize, + end: usize, + click_count: usize, + last_click_position: Option, + last_click_timestamp: Option, + } + + impl Default for Cursor { + fn default() -> Self { + Cursor { + start: 0, + end: 0, + click_count: 0, + last_click_position: None, + last_click_timestamp: None, + } + } + } + + impl Cursor { + /* Move section */ + pub fn move_to(&mut self, position: usize) { + self.start = position; + self.end = position; + } + + pub fn move_right(&mut self, value: &Value) { + if self.is_selection() { + let dest = self.right(); + self.start = dest; + self.end = dest; + } else if self.end < value.len() { + self.start += 1; + self.end += 1; + } + } + + pub fn move_left(&mut self) { + if self.is_selection() { + let dest = self.left(); + self.start = dest; + self.end = dest; + } else if self.left() > 0 { + self.start -= 1; + self.end -= 1; + } + } + + pub fn move_right_by_amount(&mut self, value: &Value, amount: usize) { + self.start = self.start.saturating_add(amount).min(value.len()); + self.end = self.end.saturating_add(amount).min(value.len()); + } + + pub fn move_left_by_words(&mut self, value: &Value) { + let (left, _) = self.cursor_position(value); + + self.move_to(value.previous_start_of_word(left)); + } + + pub fn move_right_by_words(&mut self, value: &Value) { + let (_, right) = self.cursor_position(value); + + self.move_to(value.next_end_of_word(right)); + } + /* Move section end */ + + /* Selection section */ + pub fn select_range(&mut self, start: usize, end: usize) { + self.start = start; + self.end = end; + } + + pub fn select_left(&mut self) { + if self.end > 0 { + self.end -= 1; + } + } + + pub fn select_right(&mut self, value: &Value) { + if self.end < value.len() { + self.end += 1; + } + } + + pub fn select_left_by_words(&mut self, value: &Value) { + self.end = value.previous_start_of_word(self.start); + } + + pub fn select_right_by_words(&mut self, value: &Value) { + self.end = value.next_end_of_word(self.start); + } + + pub fn select_all(&mut self, len: usize) { + self.start = 0; + self.end = len; + } + /* Selection section end */ + + /* Double/Triple click section */ + // returns the amount of clicks on the same position in specific timeframe + // (1=double click, 2=triple click) + pub fn process_click(&mut self, position: Point) -> usize { + if position + == self.last_click_position.unwrap_or(Point { x: 0.0, y: 0.0 }) + && self.click_count < 2 + && SystemTime::now() + .duration_since( + self.last_click_timestamp + .unwrap_or(SystemTime::UNIX_EPOCH), + ) + .unwrap_or(Duration::from_secs(1)) + .as_millis() + <= 500 + { + self.click_count += 1; + } else { + self.click_count = 0; + } + self.last_click_position = Option::from(position); + self.last_click_timestamp = Option::from(SystemTime::now()); + self.click_count + } + /* Double/Triple click section end */ + + /* "get info about cursor/selection" section */ + pub fn is_selection(&self) -> bool { + self.start != self.end + } + + // get start position of selection (can be left OR right boundary of selection) + pub(crate) fn start(&self) -> usize { + self.start + } + + // get end position of selection (can be left OR right boundary of selection) + pub fn end(&self) -> usize { + self.end + } + + // get left boundary of selection + pub fn left(&self) -> usize { + self.start.min(self.end) + } + + // get right boundary of selection + pub fn right(&self) -> usize { + self.start.max(self.end) + } + + pub fn cursor_position(&self, value: &Value) -> (usize, usize) { + (self.start.min(value.len()), self.end.min(value.len())) + } + + pub fn cursor_position_left(&self, value: &Value) -> usize { + let (a, b) = self.cursor_position(value); + a.min(b) + } + + pub fn cursor_position_right(&self, value: &Value) -> usize { + let (a, b) = self.cursor_position(value); + a.max(b) + } + + pub fn draw_position(&self, value: &Value) -> usize { + let (_, end) = self.cursor_position(value); + end + } + /* "get info about cursor/selection" section end */ + } +} diff --git a/wgpu/src/renderer/widget/text_input.rs b/wgpu/src/renderer/widget/text_input.rs index 72da85f4..4e0274b8 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).position(), + state.cursor.draw_position(value), font, ); @@ -111,17 +111,16 @@ 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, - cursor.position(), + state.cursor.draw_position(value), font, ); - let selection = match cursor { + /*let selection = match cursor { text_input::Cursor::Index(_) => Primitive::None, text_input::Cursor::Selection { .. } => { let (cursor_left_offset, _) = @@ -130,7 +129,7 @@ impl text_input::Renderer for Renderer { text_bounds, value, size, - cursor.left(), + state.cursor.left(), font, ); let (cursor_right_offset, _) = @@ -139,7 +138,7 @@ impl text_input::Renderer for Renderer { text_bounds, value, size, - cursor.right(), + state.cursor.right(), font, ); let width = cursor_right_offset - cursor_left_offset; @@ -158,6 +157,42 @@ impl text_input::Renderer for Renderer { border_color: Color::TRANSPARENT, } } + };*/ + + let selection = if !state.cursor.is_selection() { + Primitive::None + } else { + 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 { From c6c8cabdaf03f90b1739be828cf140d8ddb92e65 Mon Sep 17 00:00:00 2001 From: FabianLars Date: Mon, 24 Feb 2020 18:03:42 +0100 Subject: [PATCH 07/27] moved cursor into own file moved click tracking as a new State struct to input::mouse made cursor field of text_input state private brought back cursor type(Index, Selection) representation with a state enum cleaned out some stuff (but not enough/all) TODO: Documentation (sigh) TODO: Editor struct TODO: some (hopefully) small improvements here and there --- native/src/input/mouse.rs | 64 +++++ native/src/widget/text_input.rs | 309 ++++++------------------- native/src/widget/text_input/cursor.rs | 176 ++++++++++++++ wgpu/src/renderer/widget/text_input.rs | 50 +--- 4 files changed, 315 insertions(+), 284 deletions(-) create mode 100644 native/src/widget/text_input/cursor.rs diff --git a/native/src/input/mouse.rs b/native/src/input/mouse.rs index 69dc6b4c..22c81e15 100644 --- a/native/src/input/mouse.rs +++ b/native/src/input/mouse.rs @@ -2,5 +2,69 @@ mod button; mod event; +use crate::Point; pub use button::Button; pub use event::{Event, ScrollDelta}; +use std::time::{Duration, SystemTime}; + +/// enum to track the type of the last click +#[derive(Debug, Copy, Clone)] +pub enum Interaction { + /// Last Click was a single click + Click(Point), + /// Last Click was a double click + DoubleClick(Point), + /// Last Click was a triple click + TripleClick(Point), +} + +/// Compiler bully +#[derive(Debug, Copy, Clone)] +pub struct State { + last_click: Option, + last_click_timestamp: Option, +} + +impl Default for State { + fn default() -> Self { + State { + last_click: None, + last_click_timestamp: None, + } + } +} + +impl State { + /// processes left click to check for double/triple clicks + /// return amount of repetitive mouse clicks + /// (1 -> double click, 2 -> triple click) + pub fn update(&mut self, position: Point) -> Interaction { + self.last_click_timestamp = Some(SystemTime::now()); + self.last_click = match self.last_click { + None => Some(Interaction::Click(position)), + Some(x) => match x { + Interaction::Click(p) if self.process_click(p, position) => { + Some(Interaction::DoubleClick(position)) + } + Interaction::DoubleClick(p) + if self.process_click(p, position) => + { + Some(Interaction::TripleClick(position)) + } + _ => Some(Interaction::Click(position)), + }, + }; + self.last_click.unwrap_or(Interaction::Click(position)) + } + + fn process_click(&self, old_position: Point, new_position: Point) -> bool { + old_position == new_position + && SystemTime::now() + .duration_since( + self.last_click_timestamp.unwrap_or(SystemTime::UNIX_EPOCH), + ) + .unwrap_or(Duration::from_secs(1)) + .as_millis() + <= 500 + } +} diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 59d86f63..4e7906a8 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -4,10 +4,13 @@ //! //! [`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, mouse::Interaction, ButtonState}, + layout, + widget::text_input::cursor::Cursor, + Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, Rectangle, + Size, Widget, }; use std::u32; @@ -209,16 +212,20 @@ where let text_layout = layout.children().next().unwrap(); let target = cursor_position.x - text_layout.bounds().x; - match self.state.cursor.process_click(cursor_position) { - 1 => self.state.cursor.select_range( - self.value.previous_start_of_word( - self.state.cursor.end(), - ), - self.value - .next_end_of_word(self.state.cursor.end()), - ), - 2 => self.state.cursor.select_all(self.value.len()), - _ => { + match self.state.mouse.update(cursor_position) { + Interaction::DoubleClick(_) => { + self.state.cursor.select_range( + self.value.previous_start_of_word( + self.state.cursor.end(), + ), + self.value + .next_end_of_word(self.state.cursor.end()), + ) + } + Interaction::TripleClick(_) => { + self.state.cursor.select_all(&self.value) + } + Interaction::Click(_) => { if target > 0.0 { let value = if self.is_secure { self.value.secure() @@ -308,17 +315,14 @@ where && self.state.is_pasting.is_none() && !c.is_control() => { - if !self.state.cursor.is_selection() { - self.value.insert(self.state.cursor.end(), c); - } else { - self.value.remove_many( - self.state.cursor.left(), - self.state.cursor.right(), - ); - self.state.cursor.move_left(); - self.value.insert(self.state.cursor.end(), c); + 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(), c); self.state.cursor.move_right(&self.value); let message = (self.on_change)(self.value.to_string()); @@ -335,42 +339,38 @@ where } } keyboard::KeyCode::Backspace => { - if !self.state.cursor.is_selection() { - if self.state.cursor.start() > 0 { + match self.state.cursor.selection_position() { + Some((start, end)) => { + self.value.remove_many(start, end); self.state.cursor.move_left(); - let _ = - self.value.remove(self.state.cursor.end() - 1); - let message = - (self.on_change)(self.value.to_string()); - messages.push(message); } - } else { - self.value.remove_many( - self.state.cursor.left(), - self.state.cursor.right(), - ); - self.state.cursor.move_left(); - let message = (self.on_change)(self.value.to_string()); - messages.push(message); + None => { + if self.state.cursor.start() > 0 { + self.state.cursor.move_left(); + let _ = self + .value + .remove(self.state.cursor.start() - 1); + } + } } + let message = (self.on_change)(self.value.to_string()); + messages.push(message); } keyboard::KeyCode::Delete => { - if !self.state.cursor.is_selection() { - 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); + 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()); + } } - } else { - self.value.remove_many( - self.state.cursor.left(), - self.state.cursor.right(), - ); - self.state.cursor.move_left(); - let message = (self.on_change)(self.value.to_string()); - messages.push(message); } + let message = (self.on_change)(self.value.to_string()); + messages.push(message); } keyboard::KeyCode::Left => { if platform::is_jump_modifier_pressed(modifiers) @@ -413,12 +413,12 @@ where } }; - if self.state.cursor.is_selection() { - self.value.remove_many( - self.state.cursor.left(), - self.state.cursor.right(), - ); - self.state.cursor.move_left(); + match self.state.cursor.selection_position() { + Some((left, right)) => { + self.value.remove_many(left, right); + self.state.cursor.move_left(); + } + _ => (), } self.value.insert_many( @@ -442,7 +442,7 @@ where } keyboard::KeyCode::A => { if platform::is_copy_paste_modifier_pressed(modifiers) { - self.state.cursor.select_range(0, self.value.len()); + self.state.cursor.select_all(&self.value); } } _ => {} @@ -596,8 +596,8 @@ pub struct State { is_focused: bool, is_pressed: bool, is_pasting: Option, - /// TODO: Compiler wants documentation here - pub cursor: cursor::Cursor, + cursor: Cursor, + mouse: crate::input::mouse::State, // TODO: Add stateful horizontal scrolling offset } @@ -617,7 +617,8 @@ impl State { is_focused: true, is_pressed: false, is_pasting: None, - cursor: cursor::Cursor::default(), + cursor: Cursor::default(), + mouse: crate::input::mouse::State::default(), } } @@ -627,6 +628,11 @@ impl State { pub fn is_focused(&self) -> bool { self.is_focused } + + /// getter for cursor + pub fn cursor(&self) -> Cursor { + self.cursor + } } /// The value of a [`TextInput`]. @@ -841,182 +847,3 @@ mod platform { } } } - -mod cursor { - use crate::widget::text_input::Value; - use iced_core::Point; - use std::time::{Duration, SystemTime}; - - /// Even the compiler bullies me for not writing documentation - #[derive(Debug, Copy, Clone)] - pub struct Cursor { - start: usize, - end: usize, - click_count: usize, - last_click_position: Option, - last_click_timestamp: Option, - } - - impl Default for Cursor { - fn default() -> Self { - Cursor { - start: 0, - end: 0, - click_count: 0, - last_click_position: None, - last_click_timestamp: None, - } - } - } - - impl Cursor { - /* Move section */ - pub fn move_to(&mut self, position: usize) { - self.start = position; - self.end = position; - } - - pub fn move_right(&mut self, value: &Value) { - if self.is_selection() { - let dest = self.right(); - self.start = dest; - self.end = dest; - } else if self.end < value.len() { - self.start += 1; - self.end += 1; - } - } - - pub fn move_left(&mut self) { - if self.is_selection() { - let dest = self.left(); - self.start = dest; - self.end = dest; - } else if self.left() > 0 { - self.start -= 1; - self.end -= 1; - } - } - - pub fn move_right_by_amount(&mut self, value: &Value, amount: usize) { - self.start = self.start.saturating_add(amount).min(value.len()); - self.end = self.end.saturating_add(amount).min(value.len()); - } - - pub fn move_left_by_words(&mut self, value: &Value) { - let (left, _) = self.cursor_position(value); - - self.move_to(value.previous_start_of_word(left)); - } - - pub fn move_right_by_words(&mut self, value: &Value) { - let (_, right) = self.cursor_position(value); - - self.move_to(value.next_end_of_word(right)); - } - /* Move section end */ - - /* Selection section */ - pub fn select_range(&mut self, start: usize, end: usize) { - self.start = start; - self.end = end; - } - - pub fn select_left(&mut self) { - if self.end > 0 { - self.end -= 1; - } - } - - pub fn select_right(&mut self, value: &Value) { - if self.end < value.len() { - self.end += 1; - } - } - - pub fn select_left_by_words(&mut self, value: &Value) { - self.end = value.previous_start_of_word(self.start); - } - - pub fn select_right_by_words(&mut self, value: &Value) { - self.end = value.next_end_of_word(self.start); - } - - pub fn select_all(&mut self, len: usize) { - self.start = 0; - self.end = len; - } - /* Selection section end */ - - /* Double/Triple click section */ - // returns the amount of clicks on the same position in specific timeframe - // (1=double click, 2=triple click) - pub fn process_click(&mut self, position: Point) -> usize { - if position - == self.last_click_position.unwrap_or(Point { x: 0.0, y: 0.0 }) - && self.click_count < 2 - && SystemTime::now() - .duration_since( - self.last_click_timestamp - .unwrap_or(SystemTime::UNIX_EPOCH), - ) - .unwrap_or(Duration::from_secs(1)) - .as_millis() - <= 500 - { - self.click_count += 1; - } else { - self.click_count = 0; - } - self.last_click_position = Option::from(position); - self.last_click_timestamp = Option::from(SystemTime::now()); - self.click_count - } - /* Double/Triple click section end */ - - /* "get info about cursor/selection" section */ - pub fn is_selection(&self) -> bool { - self.start != self.end - } - - // get start position of selection (can be left OR right boundary of selection) - pub(crate) fn start(&self) -> usize { - self.start - } - - // get end position of selection (can be left OR right boundary of selection) - pub fn end(&self) -> usize { - self.end - } - - // get left boundary of selection - pub fn left(&self) -> usize { - self.start.min(self.end) - } - - // get right boundary of selection - pub fn right(&self) -> usize { - self.start.max(self.end) - } - - pub fn cursor_position(&self, value: &Value) -> (usize, usize) { - (self.start.min(value.len()), self.end.min(value.len())) - } - - pub fn cursor_position_left(&self, value: &Value) -> usize { - let (a, b) = self.cursor_position(value); - a.min(b) - } - - pub fn cursor_position_right(&self, value: &Value) -> usize { - let (a, b) = self.cursor_position(value); - a.max(b) - } - - pub fn draw_position(&self, value: &Value) -> usize { - let (_, end) = self.cursor_position(value); - end - } - /* "get info about cursor/selection" section end */ - } -} diff --git a/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs new file mode 100644 index 00000000..bbf5448b --- /dev/null +++ b/native/src/widget/text_input/cursor.rs @@ -0,0 +1,176 @@ +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 */ + // TODO: (whole section): Return State::Cursor if start == end after operation + pub fn select_range(&mut self, start: usize, end: usize) { + self.state = State::Selection { start, end }; + } + + 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 draw_position(&self, value: &Value) -> usize { + self.cursor_position(value) + } + + 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 return 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/wgpu/src/renderer/widget/text_input.rs b/wgpu/src/renderer/widget/text_input.rs index 4e0274b8..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.draw_position(value), + state.cursor().cursor_position(value), font, ); @@ -116,20 +116,20 @@ impl text_input::Renderer for Renderer { text_bounds, value, size, - state.cursor.draw_position(value), + state.cursor().cursor_position(value), font, ); - /*let selection = match cursor { - text_input::Cursor::Index(_) => Primitive::None, - text_input::Cursor::Selection { .. } => { + 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(), + state.cursor().left(), font, ); let (cursor_right_offset, _) = @@ -138,7 +138,7 @@ impl text_input::Renderer for Renderer { text_bounds, value, size, - state.cursor.right(), + state.cursor().right(), font, ); let width = cursor_right_offset - cursor_left_offset; @@ -157,42 +157,6 @@ impl text_input::Renderer for Renderer { border_color: Color::TRANSPARENT, } } - };*/ - - let selection = if !state.cursor.is_selection() { - Primitive::None - } else { - 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 { From c47e30e960a403631b6dff7a522a775050d59f87 Mon Sep 17 00:00:00 2001 From: FabianLars Date: Mon, 24 Feb 2020 20:10:20 +0100 Subject: [PATCH 08/27] double click fixed --- native/src/input/mouse.rs | 2 +- native/src/widget/text_input.rs | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/native/src/input/mouse.rs b/native/src/input/mouse.rs index 22c81e15..eaac52f3 100644 --- a/native/src/input/mouse.rs +++ b/native/src/input/mouse.rs @@ -39,7 +39,6 @@ impl State { /// return amount of repetitive mouse clicks /// (1 -> double click, 2 -> triple click) pub fn update(&mut self, position: Point) -> Interaction { - self.last_click_timestamp = Some(SystemTime::now()); self.last_click = match self.last_click { None => Some(Interaction::Click(position)), Some(x) => match x { @@ -54,6 +53,7 @@ impl State { _ => Some(Interaction::Click(position)), }, }; + self.last_click_timestamp = Some(SystemTime::now()); self.last_click.unwrap_or(Interaction::Click(position)) } diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 4e7906a8..cadef11d 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -214,12 +214,10 @@ where match self.state.mouse.update(cursor_position) { Interaction::DoubleClick(_) => { + let end = self.state.cursor.end(); self.state.cursor.select_range( - self.value.previous_start_of_word( - self.state.cursor.end(), - ), - self.value - .next_end_of_word(self.state.cursor.end()), + self.value.previous_start_of_word(end), + self.value.next_end_of_word(end), ) } Interaction::TripleClick(_) => { From 0d8d236be65abf98a09a13b2e22b677f3d95f195 Mon Sep 17 00:00:00 2001 From: FabianLars Date: Tue, 25 Feb 2020 17:03:52 +0100 Subject: [PATCH 09/27] More selection actions: (Ctrl +) Shift + Left/Right, Shift + Home/End --- native/src/widget/text_input.rs | 44 +++++++++++++++++++++++--- native/src/widget/text_input/cursor.rs | 6 +++- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index cadef11d..b39c2ee8 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -374,7 +374,13 @@ where if platform::is_jump_modifier_pressed(modifiers) && !self.is_secure { - self.state.cursor.move_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.cursor.move_left(); } @@ -383,16 +389,37 @@ where if platform::is_jump_modifier_pressed(modifiers) && !self.is_secure { - self.state.cursor.move_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.cursor.move_right(&self.value); } } keyboard::KeyCode::Home => { - self.state.cursor.move_to(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.cursor.move_to(self.value.len()); + 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) { @@ -438,6 +465,15 @@ 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); diff --git a/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs index bbf5448b..307040eb 100644 --- a/native/src/widget/text_input/cursor.rs +++ b/native/src/widget/text_input/cursor.rs @@ -58,7 +58,11 @@ impl Cursor { /* expand/shrink selection */ // TODO: (whole section): Return State::Cursor if start == end after operation pub fn select_range(&mut self, start: usize, end: usize) { - self.state = State::Selection { start, end }; + if start != end { + self.state = State::Selection { start, end }; + } else { + self.state = State::Index(start); + } } pub fn select_left(&mut self) { From 1ad83889be32b1bdb746e751aa620fad85b864b0 Mon Sep 17 00:00:00 2001 From: FabianLars Date: Wed, 26 Feb 2020 21:58:34 +0100 Subject: [PATCH 10/27] really small cleanup --- native/src/input/mouse.rs | 3 +-- native/src/widget/text_input/cursor.rs | 7 +------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/native/src/input/mouse.rs b/native/src/input/mouse.rs index eaac52f3..ca3daeb1 100644 --- a/native/src/input/mouse.rs +++ b/native/src/input/mouse.rs @@ -36,8 +36,7 @@ impl Default for State { impl State { /// processes left click to check for double/triple clicks - /// return amount of repetitive mouse clicks - /// (1 -> double click, 2 -> triple click) + /// return amount of repetitive mouse clicks as enum pub fn update(&mut self, position: Point) -> Interaction { self.last_click = match self.last_click { None => Some(Interaction::Click(position)), diff --git a/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs index 307040eb..92fd2029 100644 --- a/native/src/widget/text_input/cursor.rs +++ b/native/src/widget/text_input/cursor.rs @@ -56,7 +56,6 @@ impl Cursor { /* end of index move methods */ /* expand/shrink selection */ - // TODO: (whole section): Return State::Cursor if start == end after operation pub fn select_range(&mut self, start: usize, end: usize) { if start != end { self.state = State::Selection { start, end }; @@ -149,10 +148,6 @@ impl Cursor { } } - pub fn draw_position(&self, value: &Value) -> usize { - self.cursor_position(value) - } - pub fn cursor_position(&self, value: &Value) -> usize { match self.state { State::Index(index) => index.min(value.len()), @@ -161,7 +156,7 @@ impl Cursor { } // returns Option of left and right border of selection - // a second method return start and end may be useful (see below) + // 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 } => { From 767096b9bbe091f6fed2a3ff31a4c6cc1131ecea Mon Sep 17 00:00:00 2001 From: FabianLars Date: Thu, 27 Feb 2020 12:13:47 +0100 Subject: [PATCH 11/27] disable word selection if is_secure == true --- native/src/widget/text_input.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index b39c2ee8..68fd7dfb 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -214,14 +214,18 @@ where match self.state.mouse.update(cursor_position) { Interaction::DoubleClick(_) => { - 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), - ) + 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), + ); + } } Interaction::TripleClick(_) => { - self.state.cursor.select_all(&self.value) + self.state.cursor.select_all(&self.value); } Interaction::Click(_) => { if target > 0.0 { From 731e6752eb2130fb4ff2b92fd9c3274ea1b3110d Mon Sep 17 00:00:00 2001 From: FabianLars Date: Fri, 13 Mar 2020 17:54:02 +0100 Subject: [PATCH 12/27] keep cursor inside value boundaries more reliable --- native/src/widget/text_input.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 68fd7dfb..82406b5a 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -324,7 +324,7 @@ where } _ => (), } - self.value.insert(self.state.cursor.end(), c); + 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()); @@ -347,11 +347,11 @@ where self.state.cursor.move_left(); } None => { - if self.state.cursor.start() > 0 { + if self.state.cursor.start().min(self.value.len()) > 0 { self.state.cursor.move_left(); let _ = self .value - .remove(self.state.cursor.start() - 1); + .remove(self.state.cursor.start()); } } } From b632dce0daffece3d1d7c7ca9d01f9feb79e2c4c Mon Sep 17 00:00:00 2001 From: FabianLars Date: Sat, 14 Mar 2020 10:51:36 +0100 Subject: [PATCH 13/27] fixed panic on pasting into non updating input --- native/src/widget/text_input.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 82406b5a..b41bfd6a 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -451,7 +451,7 @@ where } self.value.insert_many( - self.state.cursor.end(), + self.state.cursor.end().min(self.value.len()), content.clone(), ); From 7cb1452d29ddfdcd29fd7ecc7c96a79ea2681fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Mar 2020 19:03:17 +0100 Subject: [PATCH 14/27] Convert `mouse::State` into `mouse::Click` --- native/src/input/mouse.rs | 66 ++--------------------------- native/src/input/mouse/click.rs | 73 +++++++++++++++++++++++++++++++++ native/src/widget/text_input.rs | 56 +++++++++++++++---------- 3 files changed, 111 insertions(+), 84 deletions(-) create mode 100644 native/src/input/mouse/click.rs diff --git a/native/src/input/mouse.rs b/native/src/input/mouse.rs index ca3daeb1..7198b233 100644 --- a/native/src/input/mouse.rs +++ b/native/src/input/mouse.rs @@ -2,68 +2,8 @@ mod button; mod event; -use crate::Point; +pub mod click; + pub use button::Button; +pub use click::Click; pub use event::{Event, ScrollDelta}; -use std::time::{Duration, SystemTime}; - -/// enum to track the type of the last click -#[derive(Debug, Copy, Clone)] -pub enum Interaction { - /// Last Click was a single click - Click(Point), - /// Last Click was a double click - DoubleClick(Point), - /// Last Click was a triple click - TripleClick(Point), -} - -/// Compiler bully -#[derive(Debug, Copy, Clone)] -pub struct State { - last_click: Option, - last_click_timestamp: Option, -} - -impl Default for State { - fn default() -> Self { - State { - last_click: None, - last_click_timestamp: None, - } - } -} - -impl State { - /// processes left click to check for double/triple clicks - /// return amount of repetitive mouse clicks as enum - pub fn update(&mut self, position: Point) -> Interaction { - self.last_click = match self.last_click { - None => Some(Interaction::Click(position)), - Some(x) => match x { - Interaction::Click(p) if self.process_click(p, position) => { - Some(Interaction::DoubleClick(position)) - } - Interaction::DoubleClick(p) - if self.process_click(p, position) => - { - Some(Interaction::TripleClick(position)) - } - _ => Some(Interaction::Click(position)), - }, - }; - self.last_click_timestamp = Some(SystemTime::now()); - self.last_click.unwrap_or(Interaction::Click(position)) - } - - fn process_click(&self, old_position: Point, new_position: Point) -> bool { - old_position == new_position - && SystemTime::now() - .duration_since( - self.last_click_timestamp.unwrap_or(SystemTime::UNIX_EPOCH), - ) - .unwrap_or(Duration::from_secs(1)) - .as_millis() - <= 500 - } -} 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 { + 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 b41bfd6a..c379f0d1 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -6,7 +6,11 @@ //! [`State`]: struct.State.html mod cursor; use crate::{ - input::{keyboard, mouse, mouse::Interaction, ButtonState}, + input::{ + keyboard, + mouse::{self, click}, + ButtonState, + }, layout, widget::text_input::cursor::Cursor, Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, Rectangle, @@ -212,22 +216,13 @@ where let text_layout = layout.children().next().unwrap(); let target = cursor_position.x - text_layout.bounds().x; - match self.state.mouse.update(cursor_position) { - Interaction::DoubleClick(_) => { - 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), - ); - } - } - Interaction::TripleClick(_) => { - self.state.cursor.select_all(&self.value); - } - Interaction::Click(_) => { + 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() @@ -262,7 +257,23 @@ where 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; @@ -324,7 +335,8 @@ where } _ => (), } - self.value.insert(self.state.cursor.end().min(self.value.len()), c); + 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()); @@ -347,7 +359,9 @@ where self.state.cursor.move_left(); } None => { - if self.state.cursor.start().min(self.value.len()) > 0 { + if self.state.cursor.start().min(self.value.len()) + > 0 + { self.state.cursor.move_left(); let _ = self .value @@ -634,8 +648,8 @@ pub struct State { is_focused: bool, is_pressed: bool, is_pasting: Option, + last_click: Option, cursor: Cursor, - mouse: crate::input::mouse::State, // TODO: Add stateful horizontal scrolling offset } @@ -655,8 +669,8 @@ impl State { is_focused: true, is_pressed: false, is_pasting: None, + last_click: None, cursor: Cursor::default(), - mouse: crate::input::mouse::State::default(), } } From 763f64b653e69b46715dcd8f7918ed769639098c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Mar 2020 19:14:27 +0100 Subject: [PATCH 15/27] Avoid panic in `Click::is_consecutive` --- native/src/input/mouse/click.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/native/src/input/mouse/click.rs b/native/src/input/mouse/click.rs index 60ae056b..d27bc67e 100644 --- a/native/src/input/mouse/click.rs +++ b/native/src/input/mouse/click.rs @@ -68,6 +68,9 @@ impl Click { fn is_consecutive(&self, new_position: Point, time: Instant) -> bool { self.position == new_position - && time.duration_since(self.time).as_millis() <= 300 + && time + .checked_duration_since(self.time) + .map(|duration| duration.as_millis() <= 300) + .unwrap_or(false) } } From 6b89dd7db9715dd46738677e13ca9d9bd12f9636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Mar 2020 20:23:31 +0100 Subject: [PATCH 16/27] Improve `text_input::cursor` API --- native/src/lib.rs | 2 +- native/src/widget/text_input.rs | 78 ++++++++------- native/src/widget/text_input/cursor.rs | 98 +++++++++--------- wgpu/src/renderer/widget/text_input.rs | 133 +++++++++++++++---------- 4 files changed, 170 insertions(+), 141 deletions(-) diff --git a/native/src/lib.rs b/native/src/lib.rs index d17dd918..5c6ab3ee 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -34,7 +34,7 @@ //! [`window::Renderer`]: window/trait.Renderer.html //! [`UserInterface`]: struct.UserInterface.html //! [renderer]: renderer/index.html -#![deny(missing_docs)] +//#![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] #![forbid(unsafe_code)] diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index c379f0d1..f5ca16e1 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -4,17 +4,18 @@ //! //! [`TextInput`]: struct.TextInput.html //! [`State`]: struct.State.html -mod cursor; +pub mod cursor; + +pub use cursor::Cursor; + use crate::{ input::{ keyboard, mouse::{self, click}, ButtonState, }, - layout, - widget::text_input::cursor::Cursor, - Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, Rectangle, - Size, Widget, + layout, Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, + Rectangle, Size, Widget, }; use std::u32; @@ -261,7 +262,8 @@ where if self.is_secure { self.state.cursor.select_all(&self.value); } else { - let end = self.state.cursor.end(); + let end = self.state.cursor.end(&self.value); + self.state.cursor.select_range( self.value.previous_start_of_word(end), self.value.next_end_of_word(end), @@ -307,7 +309,7 @@ where self.font, ); - let pos = find_cursor_position( + let position = find_cursor_position( renderer, target + offset, &value, @@ -317,9 +319,10 @@ where self.font, ); - self.state - .cursor - .select_range(self.state.cursor.start(), pos); + self.state.cursor.select_range( + self.state.cursor.start(&value), + position, + ); } } } @@ -328,15 +331,15 @@ where && self.state.is_pasting.is_none() && !c.is_control() => { - match self.state.cursor.selection_position() { + match self.state.cursor.selection() { Some((left, right)) => { self.value.remove_many(left, right); - self.state.cursor.move_left(); + self.state.cursor.move_left(&self.value); } _ => (), } - self.value - .insert(self.state.cursor.end().min(self.value.len()), c); + + self.value.insert(self.state.cursor.end(&self.value), c); self.state.cursor.move_right(&self.value); let message = (self.on_change)(self.value.to_string()); @@ -353,19 +356,18 @@ where } } keyboard::KeyCode::Backspace => { - match self.state.cursor.selection_position() { + match self.state.cursor.selection() { Some((start, end)) => { self.value.remove_many(start, end); - self.state.cursor.move_left(); + self.state.cursor.move_left(&self.value); } None => { - if self.state.cursor.start().min(self.value.len()) - > 0 - { - self.state.cursor.move_left(); - let _ = self - .value - .remove(self.state.cursor.start()); + if self.state.cursor.start(&self.value) > 0 { + self.state.cursor.move_left(&self.value); + + let _ = self.value.remove( + self.state.cursor.start(&self.value), + ); } } } @@ -373,15 +375,16 @@ where messages.push(message); } keyboard::KeyCode::Delete => { - match self.state.cursor.selection_position() { + match self.state.cursor.selection() { Some((start, end)) => { self.value.remove_many(start, end); - self.state.cursor.move_left(); + self.state.cursor.move_left(&self.value); } None => { - if self.state.cursor.end() < self.value.len() { - let _ = - self.value.remove(self.state.cursor.end()); + let end = self.state.cursor.end(&self.value); + + if end > 0 { + let _ = self.value.remove(end); } } } @@ -398,9 +401,9 @@ where self.state.cursor.move_left_by_words(&self.value); } } else if modifiers.shift { - self.state.cursor.select_left() + self.state.cursor.select_left(&self.value) } else { - self.state.cursor.move_left(); + self.state.cursor.move_left(&self.value); } } keyboard::KeyCode::Right => { @@ -422,9 +425,10 @@ where } keyboard::KeyCode::Home => { if modifiers.shift { - self.state - .cursor - .select_range(self.state.cursor.start(), 0); + self.state.cursor.select_range( + self.state.cursor.start(&self.value), + 0, + ); } else { self.state.cursor.move_to(0); } @@ -432,7 +436,7 @@ where keyboard::KeyCode::End => { if modifiers.shift { self.state.cursor.select_range( - self.state.cursor.start(), + self.state.cursor.start(&self.value), self.value.len(), ); } else { @@ -456,16 +460,16 @@ where } }; - match self.state.cursor.selection_position() { + match self.state.cursor.selection() { Some((left, right)) => { self.value.remove_many(left, right); - self.state.cursor.move_left(); + self.state.cursor.move_left(&self.value); } _ => (), } self.value.insert_many( - self.state.cursor.end().min(self.value.len()), + self.state.cursor.end(&self.value), content.clone(), ); diff --git a/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs index 92fd2029..d8938777 100644 --- a/native/src/widget/text_input/cursor.rs +++ b/native/src/widget/text_input/cursor.rs @@ -1,7 +1,8 @@ +//! Track the cursor of a text input. use crate::widget::text_input::Value; #[derive(Debug, Copy, Clone)] -enum State { +pub enum State { Index(usize), Selection { start: usize, end: usize }, } @@ -20,7 +21,6 @@ impl Default for Cursor { } impl Cursor { - /* index move methods */ pub fn move_to(&mut self, position: usize) { self.state = State::Index(position); } @@ -30,42 +30,40 @@ impl Cursor { } pub fn move_right_by_words(&mut self, value: &Value) { - self.move_to(value.next_end_of_word(self.right())) + self.move_to(value.next_end_of_word(self.right(value))) } pub fn move_right_by_amount(&mut self, value: &Value, amount: usize) { - match self.state { + match self.state(value) { State::Index(index) => { self.move_to(index.saturating_add(amount).min(value.len())) } - State::Selection { .. } => self.move_to(self.right()), + State::Selection { start, end } => self.move_to(end.max(start)), } } - pub fn move_left(&mut self) { - match self.state { + pub fn move_left(&mut self, value: &Value) { + match self.state(value) { State::Index(index) if index > 0 => self.move_to(index - 1), - State::Selection { .. } => self.move_to(self.left()), + State::Selection { start, end } => self.move_to(start.min(end)), _ => self.move_to(0), } } pub fn move_left_by_words(&mut self, value: &Value) { - self.move_to(value.previous_start_of_word(self.right())); + self.move_to(value.previous_start_of_word(self.left(value))); } - /* 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 { + if start == end { self.state = State::Index(start); + } else { + self.state = State::Selection { start, end }; } } - pub fn select_left(&mut self) { - match self.state { + pub fn select_left(&mut self, value: &Value) { + match self.state(value) { State::Index(index) if index > 0 => { self.select_range(index, index - 1) } @@ -77,7 +75,7 @@ impl Cursor { } pub fn select_right(&mut self, value: &Value) { - match self.state { + match self.state(value) { State::Index(index) if index < value.len() => { self.select_range(index, index + 1) } @@ -89,7 +87,7 @@ impl Cursor { } pub fn select_left_by_words(&mut self, value: &Value) { - match self.state { + match self.state(value) { State::Index(index) => { self.select_range(index, value.previous_start_of_word(index)) } @@ -100,7 +98,7 @@ impl Cursor { } pub fn select_right_by_words(&mut self, value: &Value) { - match self.state { + match self.state(value) { State::Index(index) => { self.select_range(index, value.next_end_of_word(index)) } @@ -113,51 +111,56 @@ impl Cursor { 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 { + 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 fn start(&self, value: &Value) -> usize { + let start = match self.state { State::Index(index) => index, State::Selection { start, .. } => start, - } + }; + + start.min(value.len()) } - // get end position of selection (can be left OR right boundary of selection) or index - pub fn end(&self) -> usize { - match self.state { + pub fn end(&self, value: &Value) -> usize { + let end = match self.state { State::Index(index) => index, State::Selection { end, .. } => end, - } + }; + + end.min(value.len()) } - // get left boundary of selection or index - pub fn left(&self) -> usize { - match self.state { + fn left(&self, value: &Value) -> usize { + match self.state(value) { 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 { + fn right(&self, value: &Value) -> usize { + match self.state(value) { 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)> { + pub fn selection(&self) -> Option<(usize, usize)> { match self.state { State::Selection { start, end } => { Some((start.min(end), start.max(end))) @@ -165,11 +168,4 @@ impl Cursor { _ => None, } } - - /* pub fn selection_position(&self) -> Option<(usize, usize)> { - match self.state { - State::Selection { start, end } => Some((start, end)), - _ => None, - } - } */ } diff --git a/wgpu/src/renderer/widget/text_input.rs b/wgpu/src/renderer/widget/text_input.rs index fa108d68..74f30be1 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; @@ -41,12 +42,19 @@ impl text_input::Renderer for Renderer { 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().cursor_position(value), + focus_position, font, ); @@ -111,70 +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().cursor_position(value), - font, - ); + let cursor = state.cursor(); - let selection = match state.cursor().selection_position() { - None => Primitive::None, - Some(_) => { - let (cursor_left_offset, _) = + 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, - state.cursor().left(), + position, 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, + + ( + 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, }, - background: Background::Color( - style_sheet.selection_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 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 (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![selection, text_value, cursor], + primitives: vec![cursor_primitive, text_value], }, Vector::new(offset as u32, 0), ) From 28382a47d3abd4f79064b610f1a2eca478a08595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Mar 2020 20:36:33 +0100 Subject: [PATCH 17/27] Move `Value` to its own module --- native/src/widget/text_input.rs | 147 ++------------------------ native/src/widget/text_input/value.rs | 134 +++++++++++++++++++++++ 2 files changed, 142 insertions(+), 139 deletions(-) create mode 100644 native/src/widget/text_input/value.rs diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index f5ca16e1..7e77b76a 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -4,9 +4,12 @@ //! //! [`TextInput`]: struct.TextInput.html //! [`State`]: struct.State.html +mod value; + pub mod cursor; pub use cursor::Cursor; +pub use value::Value; use crate::{ input::{ @@ -19,7 +22,6 @@ use crate::{ }; use std::u32; -use unicode_segmentation::UnicodeSegmentation; /// A field that can be filled with text. /// @@ -362,12 +364,12 @@ where self.state.cursor.move_left(&self.value); } None => { - if self.state.cursor.start(&self.value) > 0 { + let start = self.state.cursor.start(&self.value); + + if start > 0 { self.state.cursor.move_left(&self.value); - let _ = self.value.remove( - self.state.cursor.start(&self.value), - ); + let _ = self.value.remove(start - 1); } } } @@ -383,7 +385,7 @@ where None => { let end = self.state.cursor.end(&self.value); - if end > 0 { + if end < self.value.len() { let _ = self.value.remove(end); } } @@ -691,139 +693,6 @@ impl State { } } -/// 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(), - } - } -} - // TODO: Reduce allocations fn find_cursor_position( renderer: &Renderer, 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(), + } + } +} From 6c47a40730938fb59aa7fb738b460dd37f756766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Mar 2020 20:51:22 +0100 Subject: [PATCH 18/27] Create `text_input::Editor` to hold editing logic --- native/src/widget/text_input.rs | 78 ++++++++----------------- native/src/widget/text_input/editor.rs | 80 ++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 55 deletions(-) create mode 100644 native/src/widget/text_input/editor.rs diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 7e77b76a..9fc7c6e6 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -4,6 +4,7 @@ //! //! [`TextInput`]: struct.TextInput.html //! [`State`]: struct.State.html +mod editor; mod value; pub mod cursor; @@ -11,6 +12,8 @@ pub mod cursor; pub use cursor::Cursor; pub use value::Value; +use editor::Editor; + use crate::{ input::{ keyboard, @@ -333,18 +336,12 @@ where && self.state.is_pasting.is_none() && !c.is_control() => { - match self.state.cursor.selection() { - Some((left, right)) => { - self.value.remove_many(left, right); - self.state.cursor.move_left(&self.value); - } - _ => (), - } + let mut editor = + Editor::new(&mut self.value, &mut self.state.cursor); - self.value.insert(self.state.cursor.end(&self.value), c); - self.state.cursor.move_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 { @@ -358,39 +355,21 @@ where } } keyboard::KeyCode::Backspace => { - match self.state.cursor.selection() { - Some((start, end)) => { - self.value.remove_many(start, end); - self.state.cursor.move_left(&self.value); - } - None => { - let start = self.state.cursor.start(&self.value); + let mut editor = + Editor::new(&mut self.value, &mut self.state.cursor); - if start > 0 { - self.state.cursor.move_left(&self.value); + editor.backspace(); - let _ = self.value.remove(start - 1); - } - } - } - let message = (self.on_change)(self.value.to_string()); + let message = (self.on_change)(editor.contents()); messages.push(message); } keyboard::KeyCode::Delete => { - match self.state.cursor.selection() { - Some((start, end)) => { - self.value.remove_many(start, end); - self.state.cursor.move_left(&self.value); - } - None => { - let end = self.state.cursor.end(&self.value); + let mut editor = + Editor::new(&mut self.value, &mut self.state.cursor); - if end < self.value.len() { - let _ = self.value.remove(end); - } - } - } - let message = (self.on_change)(self.value.to_string()); + editor.delete(); + + let message = (self.on_change)(editor.contents()); messages.push(message); } keyboard::KeyCode::Left => { @@ -462,28 +441,17 @@ where } }; - match self.state.cursor.selection() { - Some((left, right)) => { - self.value.remove_many(left, right); - self.state.cursor.move_left(&self.value); - } - _ => (), - } - - self.value.insert_many( - self.state.cursor.end(&self.value), - content.clone(), + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, ); - self.state.cursor.move_right_by_amount( - &self.value, - content.len(), - ); - self.state.is_pasting = Some(content); + editor.paste(content.clone()); - let message = - (self.on_change)(self.value.to_string()); + let message = (self.on_change)(editor.contents()); messages.push(message); + + self.state.is_pasting = Some(content); } } else { self.state.is_pasting = None; diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs new file mode 100644 index 00000000..de235d52 --- /dev/null +++ b/native/src/widget/text_input/editor.rs @@ -0,0 +1,80 @@ +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.value.remove_many(left, right); + self.cursor.move_left(&self.value); + } + _ => (), + } + + 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.value.remove_many(left, right); + self.cursor.move_left(&self.value); + } + _ => (), + } + + 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.value.remove_many(start, end); + self.cursor.move_left(&self.value); + } + 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((start, end)) => { + self.value.remove_many(start, end); + self.cursor.move_left(&self.value); + } + None => { + let end = self.cursor.end(&self.value); + + if end < self.value.len() { + let _ = self.value.remove(end); + } + } + } + } +} From 735d9f049c1dc3872f2699c87817a3c5e3d3c034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Mar 2020 20:57:03 +0100 Subject: [PATCH 19/27] Fix edge case in `Editor::backspace` --- native/src/widget/text_input/editor.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs index de235d52..30025016 100644 --- a/native/src/widget/text_input/editor.rs +++ b/native/src/widget/text_input/editor.rs @@ -47,8 +47,8 @@ impl<'a> Editor<'a> { pub fn backspace(&mut self) { match self.cursor.selection() { Some((start, end)) => { - self.value.remove_many(start, end); self.cursor.move_left(&self.value); + self.value.remove_many(start, end); } None => { let start = self.cursor.start(&self.value); @@ -64,9 +64,8 @@ impl<'a> Editor<'a> { pub fn delete(&mut self) { match self.cursor.selection() { - Some((start, end)) => { - self.value.remove_many(start, end); - self.cursor.move_left(&self.value); + Some(_) => { + self.backspace(); } None => { let end = self.cursor.end(&self.value); From 857d65c1ce276e7c03157f5d271a44cd6a047a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Mar 2020 21:43:28 +0100 Subject: [PATCH 20/27] Write documentation for `text_input::Cursor` --- native/src/lib.rs | 2 +- native/src/widget/text_input/cursor.rs | 236 ++++++++++++++----------- 2 files changed, 129 insertions(+), 109 deletions(-) diff --git a/native/src/lib.rs b/native/src/lib.rs index 5c6ab3ee..d17dd918 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -34,7 +34,7 @@ //! [`window::Renderer`]: window/trait.Renderer.html //! [`UserInterface`]: struct.UserInterface.html //! [renderer]: renderer/index.html -//#![deny(missing_docs)] +#![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] #![forbid(unsafe_code)] diff --git a/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs index d8938777..16e7a01b 100644 --- a/native/src/widget/text_input/cursor.rs +++ b/native/src/widget/text_input/cursor.rs @@ -1,17 +1,29 @@ //! Track the cursor of a text input. use crate::widget::text_input::Value; -#[derive(Debug, Copy, Clone)] -pub enum State { - Index(usize), - Selection { start: usize, end: usize }, -} - +/// 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 { @@ -21,97 +33,10 @@ impl Default for Cursor { } impl Cursor { - 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(value))) - } - - pub 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 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 fn move_left_by_words(&mut self, value: &Value) { - self.move_to(value.previous_start_of_word(self.left(value))); - } - - pub 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 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 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 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 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 fn select_all(&mut self, value: &Value) { - self.select_range(0, value.len()); - } - + /// 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())), @@ -128,7 +53,102 @@ impl Cursor { } } - pub fn start(&self, value: &Value) -> usize { + 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, @@ -137,7 +157,7 @@ impl Cursor { start.min(value.len()) } - pub fn end(&self, value: &Value) -> usize { + pub(crate) fn end(&self, value: &Value) -> usize { let end = match self.state { State::Index(index) => index, State::Selection { end, .. } => end, @@ -146,6 +166,15 @@ impl Cursor { 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, @@ -159,13 +188,4 @@ impl Cursor { State::Selection { start, end } => start.max(end), } } - - pub fn selection(&self) -> Option<(usize, usize)> { - match self.state { - State::Selection { start, end } => { - Some((start.min(end), start.max(end))) - } - _ => None, - } - } } From 6791c0b20820c104344a8456909be94d4530b43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Mar 2020 21:46:20 +0100 Subject: [PATCH 21/27] Remove commented code in `text_input` --- native/src/widget/text_input.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 9fc7c6e6..43e48d00 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -457,15 +457,6 @@ 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); From 5e6970b6156928b010453f5199acfff0791ecf29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Mar 2020 21:47:43 +0100 Subject: [PATCH 22/27] Improve docs of `text_input::State::cursor` --- native/src/widget/text_input.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 43e48d00..dddabfdf 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -646,7 +646,10 @@ impl State { self.is_focused } - /// getter for cursor + /// Returns the [`Cursor`] of the [`TextInput`]. + /// + /// [`Cursor`]: struct.Cursor.html + /// [`TextInput`]: struct.TextInput.html pub fn cursor(&self) -> Cursor { self.cursor } From d6914d79a1867d517ce45d11ced8d4b31fdadcfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Mar 2020 21:48:54 +0100 Subject: [PATCH 23/27] Rename `is_pressed` to `is_dragging` in `text_input` --- native/src/widget/text_input.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index dddabfdf..b0dc9f9a 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -283,17 +283,17 @@ where self.state.last_click = Some(click); } - self.state.is_pressed = is_clicked; + 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_pressed = false; + self.state.is_dragging = false; } Event::Mouse(mouse::Event::CursorMoved { x, .. }) => { - if self.state.is_pressed { + if self.state.is_dragging { let text_layout = layout.children().next().unwrap(); let target = x - text_layout.bounds().x; @@ -611,7 +611,7 @@ where #[derive(Debug, Default, Clone)] pub struct State { is_focused: bool, - is_pressed: bool, + is_dragging: bool, is_pasting: Option, last_click: Option, cursor: Cursor, @@ -632,7 +632,7 @@ impl State { pub fn focused() -> Self { Self { is_focused: true, - is_pressed: false, + is_dragging: false, is_pasting: None, last_click: None, cursor: Cursor::default(), From b30ddf90d2c74ab709aaadd9dd60815d17e2de63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Mar 2020 21:51:56 +0100 Subject: [PATCH 24/27] Change selection color in `styling` example --- examples/styling/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index dfa168b6..63ab9d62 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -357,7 +357,7 @@ mod style { } fn selection_color(&self) -> Color { - Color::BLACK + ACTIVE } } From 30f02345a88fc03a5a71d6563f385ac090063bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 25 Mar 2020 13:57:02 +0100 Subject: [PATCH 25/27] Implement `Renderer::find_cursor_position` --- native/src/renderer/null.rs | 4 +- native/src/widget/text_input.rs | 103 ++++++++++++++----------- wgpu/src/renderer/widget/text_input.rs | 4 +- 3 files changed, 61 insertions(+), 50 deletions(-) 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 b0dc9f9a..c17a1d30 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -236,29 +236,16 @@ 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.move_to( - find_cursor_position( - renderer, - target + offset, - &value, - size, - 0, - self.value.len(), - self.font, - ), - ); + self.state.cursor.move_to(position); } else { self.state.cursor.move_to(0); } @@ -267,11 +254,18 @@ where if self.is_secure { self.state.cursor.select_all(&self.value); } else { - let end = self.state.cursor.end(&self.value); + 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(end), - self.value.next_end_of_word(end), + self.value.previous_start_of_word(position), + self.value.next_end_of_word(position), ); } } @@ -304,24 +298,13 @@ 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, - ); - - let position = find_cursor_position( - renderer, - target + offset, - &value, - size, - 0, - self.value.len(), - self.font, + target, ); self.state.cursor.select_range( @@ -493,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, @@ -505,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, @@ -559,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`]. @@ -583,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> @@ -658,12 +669,12 @@ impl State { // 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 { @@ -691,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/wgpu/src/renderer/widget/text_input.rs b/wgpu/src/renderer/widget/text_input.rs index 74f30be1..170ac3c5 100644 --- a/wgpu/src/renderer/widget/text_input.rs +++ b/wgpu/src/renderer/widget/text_input.rs @@ -36,10 +36,10 @@ 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(); @@ -69,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, From cb32326fe6a57c3b4989741d88998fcafbf8e1ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 25 Mar 2020 14:03:15 +0100 Subject: [PATCH 26/27] Fix edge cases when inserting text in `Editor` --- native/src/widget/text_input/editor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs index 30025016..37093a70 100644 --- a/native/src/widget/text_input/editor.rs +++ b/native/src/widget/text_input/editor.rs @@ -17,8 +17,8 @@ impl<'a> Editor<'a> { pub fn insert(&mut self, character: char) { match self.cursor.selection() { Some((left, right)) => { - self.value.remove_many(left, right); self.cursor.move_left(&self.value); + self.value.remove_many(left, right); } _ => (), } @@ -32,8 +32,8 @@ impl<'a> Editor<'a> { match self.cursor.selection() { Some((left, right)) => { - self.value.remove_many(left, right); self.cursor.move_left(&self.value); + self.value.remove_many(left, right); } _ => (), } From bc10ca501ba012dbd379ade93e27bc012c08c2f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 25 Mar 2020 14:07:32 +0100 Subject: [PATCH 27/27] Remove unnecessary borrows in `Editor` --- native/src/widget/text_input/editor.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs index 37093a70..71c4f292 100644 --- a/native/src/widget/text_input/editor.rs +++ b/native/src/widget/text_input/editor.rs @@ -17,14 +17,14 @@ impl<'a> Editor<'a> { pub fn insert(&mut self, character: char) { match self.cursor.selection() { Some((left, right)) => { - self.cursor.move_left(&self.value); + 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); + self.value.insert(self.cursor.end(self.value), character); + self.cursor.move_right(self.value); } pub fn paste(&mut self, content: Value) { @@ -32,29 +32,28 @@ impl<'a> Editor<'a> { match self.cursor.selection() { Some((left, right)) => { - self.cursor.move_left(&self.value); + self.cursor.move_left(self.value); self.value.remove_many(left, right); } _ => (), } - self.value - .insert_many(self.cursor.end(&self.value), content); + self.value.insert_many(self.cursor.end(self.value), content); - self.cursor.move_right_by_amount(&self.value, length); + 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.cursor.move_left(self.value); self.value.remove_many(start, end); } None => { - let start = self.cursor.start(&self.value); + let start = self.cursor.start(self.value); if start > 0 { - self.cursor.move_left(&self.value); + self.cursor.move_left(self.value); let _ = self.value.remove(start - 1); } @@ -68,7 +67,7 @@ impl<'a> Editor<'a> { self.backspace(); } None => { - let end = self.cursor.end(&self.value); + let end = self.cursor.end(self.value); if end < self.value.len() { let _ = self.value.remove(end);