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