diff options
author | 2019-10-31 03:50:40 +0100 | |
---|---|---|
committer | 2019-10-31 03:50:40 +0100 | |
commit | 51a0e99097f9ecb63eeb7f2ea7c38089977eb4d0 (patch) | |
tree | d6532ec8c98b80fa72e3b78a556ccf1b26b33c02 | |
parent | 374b54c3ecbe39a24cfa6b8eccb9b2a2098f65c7 (diff) | |
download | iced-51a0e99097f9ecb63eeb7f2ea7c38089977eb4d0.tar.gz iced-51a0e99097f9ecb63eeb7f2ea7c38089977eb4d0.tar.bz2 iced-51a0e99097f9ecb63eeb7f2ea7c38089977eb4d0.zip |
Implement cursor movement in `TextInput`
-rw-r--r-- | core/src/widget/text_input.rs | 92 | ||||
-rw-r--r-- | examples/todos.rs | 40 | ||||
-rw-r--r-- | native/src/widget/text_input.rs | 57 | ||||
-rw-r--r-- | wgpu/src/renderer/text_input.rs | 35 |
4 files changed, 175 insertions, 49 deletions
diff --git a/core/src/widget/text_input.rs b/core/src/widget/text_input.rs index e0f0744b..95064ab3 100644 --- a/core/src/widget/text_input.rs +++ b/core/src/widget/text_input.rs @@ -1,9 +1,11 @@ use crate::Length; +use std::ops::{Index, RangeTo}; + pub struct TextInput<'a, Message> { pub state: &'a mut State, pub placeholder: String, - pub value: String, + pub value: Value, pub width: Length, pub max_width: Length, pub padding: u16, @@ -12,11 +14,6 @@ pub struct TextInput<'a, Message> { pub on_submit: Option<Message>, } -#[derive(Debug, Default)] -pub struct State { - pub is_focused: bool, -} - impl<'a, Message> TextInput<'a, Message> { pub fn new<F>( state: &'a mut State, @@ -30,7 +27,7 @@ impl<'a, Message> TextInput<'a, Message> { Self { state, placeholder: String::from(placeholder), - value: String::from(value), + value: Value::new(value), width: Length::Fill, max_width: Length::Shrink, padding: 0, @@ -84,3 +81,84 @@ where f.debug_struct("TextInput").finish() } } + +#[derive(Debug, Default)] +pub struct State { + pub is_focused: bool, + cursor_position: usize, +} + +impl State { + pub fn new() -> Self { + Self::default() + } + + pub fn focused() -> Self { + Self { + is_focused: true, + ..Self::default() + } + } + + pub fn move_cursor_right(&mut self, value: &Value) { + let current = self.cursor_position(value); + + if current < value.len() { + self.cursor_position = current + 1; + } + } + + pub fn move_cursor_left(&mut self, value: &Value) { + let current = self.cursor_position(value); + + if current > 0 { + self.cursor_position = current - 1; + } + } + + pub fn cursor_position(&self, value: &Value) -> usize { + self.cursor_position.min(value.len()) + } +} + +pub struct Value(Vec<char>); + +impl Value { + pub fn new(string: &str) -> Self { + Self(string.chars().collect()) + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn until(&self, index: usize) -> Self { + Self(self.0[..index].iter().cloned().collect()) + } + + pub fn to_string(&self) -> String { + let mut string = String::new(); + + for c in self.0.iter() { + string.push(*c); + } + + string + } + + pub fn insert(&mut self, index: usize, c: char) { + self.0.insert(index, c); + } + + pub fn remove(&mut self, index: usize) { + self.0.remove(index); + } +} + +impl Index<RangeTo<usize>> for Value { + type Output = [char]; + + fn index(&self, index: RangeTo<usize>) -> &[char] { + &self.0[index] + } +} diff --git a/examples/todos.rs b/examples/todos.rs index 6189c8db..9581262a 100644 --- a/examples/todos.rs +++ b/examples/todos.rs @@ -15,26 +15,6 @@ struct Todos { tasks: Vec<Task>, } -#[derive(Debug)] -struct Task { - description: String, - completed: bool, -} - -impl Task { - fn new(description: String) -> Self { - Task { - description, - completed: false, - } - } - - fn view(&mut self) -> Element<bool> { - Checkbox::new(self.completed, &self.description, |checked| checked) - .into() - } -} - #[derive(Debug, Clone)] pub enum Message { InputChanged(String), @@ -107,6 +87,26 @@ impl Application for Todos { } } +#[derive(Debug)] +struct Task { + description: String, + completed: bool, +} + +impl Task { + fn new(description: String) -> Self { + Task { + description, + completed: false, + } + } + + fn view(&mut self) -> Element<bool> { + Checkbox::new(self.completed, &self.description, |checked| checked) + .into() + } +} + // Colors const GRAY: Color = Color { r: 0.5, diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 976ca995..d9837b61 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -45,28 +45,55 @@ where Event::Keyboard(keyboard::Event::CharacterReceived(c)) if self.state.is_focused && !c.is_control() => { - self.value.push(c); + let cursor_position = self.state.cursor_position(&self.value); - let message = (self.on_change)(self.value.clone()); - messages.push(message); - } - Event::Keyboard(keyboard::Event::Input { - key_code: keyboard::KeyCode::Backspace, - state: ButtonState::Pressed, - }) => { - let _ = self.value.pop(); + self.value.insert(cursor_position, c); + self.state.move_cursor_right(&self.value); - let message = (self.on_change)(self.value.clone()); + let message = (self.on_change)(self.value.to_string()); messages.push(message); } Event::Keyboard(keyboard::Event::Input { - key_code: keyboard::KeyCode::Enter, + key_code, state: ButtonState::Pressed, - }) => { - if let Some(on_submit) = self.on_submit.clone() { - messages.push(on_submit); + }) if self.state.is_focused => match key_code { + keyboard::KeyCode::Enter => { + if let Some(on_submit) = self.on_submit.clone() { + messages.push(on_submit); + } } - } + keyboard::KeyCode::Backspace => { + let cursor_position = + self.state.cursor_position(&self.value); + + if cursor_position > 0 { + self.state.move_cursor_left(&self.value); + + let _ = self.value.remove(cursor_position - 1); + + let message = (self.on_change)(self.value.to_string()); + messages.push(message); + } + } + keyboard::KeyCode::Delete => { + let cursor_position = + self.state.cursor_position(&self.value); + + if cursor_position < self.value.len() { + let _ = self.value.remove(cursor_position); + + let message = (self.on_change)(self.value.to_string()); + messages.push(message); + } + } + keyboard::KeyCode::Left => { + self.state.move_cursor_left(&self.value); + } + keyboard::KeyCode::Right => { + self.state.move_cursor_right(&self.value); + } + _ => {} + }, _ => {} } } diff --git a/wgpu/src/renderer/text_input.rs b/wgpu/src/renderer/text_input.rs index 30e06878..f119ae6a 100644 --- a/wgpu/src/renderer/text_input.rs +++ b/wgpu/src/renderer/text_input.rs @@ -55,17 +55,18 @@ impl text_input::Renderer for Renderer { }; let size = f32::from(text_input.size.unwrap_or(self.default_size())); + let text = text_input.value.to_string(); let value = Primitive::Clip { bounds: text_bounds, offset: 0, content: Box::new(Primitive::Text { - content: if text_input.value.is_empty() { + content: if text.is_empty() { text_input.placeholder.clone() } else { - text_input.value.clone() + text.clone() }, - color: if text_input.value.is_empty() { + color: if text.is_empty() { Color { r: 0.7, g: 0.7, @@ -95,11 +96,18 @@ impl text_input::Renderer for Renderer { primitives: if text_input.state.is_focused { use wgpu_glyph::{GlyphCruncher, Scale, Section}; + let text_before_cursor = &text_input + .value + .until( + text_input.state.cursor_position(&text_input.value), + ) + .to_string(); + let mut text_value_width = self .glyph_brush .borrow_mut() .glyph_bounds(Section { - text: &text_input.value, + text: text_before_cursor, bounds: (f32::INFINITY, text_bounds.height), scale: Scale { x: size, y: size }, ..Default::default() @@ -107,11 +115,24 @@ impl text_input::Renderer for Renderer { .map(|bounds| bounds.width().round()) .unwrap_or(0.0); - let spaces_at_the_end = text_input.value.len() - - text_input.value.trim_end().len(); + let spaces_at_the_end = text_before_cursor.len() + - text_before_cursor.trim_end().len(); if spaces_at_the_end > 0 { - text_value_width += spaces_at_the_end as f32 * 5.0; + let space_width = { + let glyph_brush = self.glyph_brush.borrow(); + + // TODO: Select appropriate font + let font = &glyph_brush.fonts()[0]; + + font.glyph(' ') + .scaled(Scale { x: size, y: size }) + .h_metrics() + .advance_width + }; + + text_value_width += + spaces_at_the_end as f32 * space_width; } let cursor = Primitive::Quad { |