Implement cursor movement in TextInput
This commit is contained in:
parent
374b54c3ec
commit
51a0e99097
4 changed files with 176 additions and 50 deletions
|
|
@ -1,9 +1,11 @@
|
||||||
use crate::Length;
|
use crate::Length;
|
||||||
|
|
||||||
|
use std::ops::{Index, RangeTo};
|
||||||
|
|
||||||
pub struct TextInput<'a, Message> {
|
pub struct TextInput<'a, Message> {
|
||||||
pub state: &'a mut State,
|
pub state: &'a mut State,
|
||||||
pub placeholder: String,
|
pub placeholder: String,
|
||||||
pub value: String,
|
pub value: Value,
|
||||||
pub width: Length,
|
pub width: Length,
|
||||||
pub max_width: Length,
|
pub max_width: Length,
|
||||||
pub padding: u16,
|
pub padding: u16,
|
||||||
|
|
@ -12,11 +14,6 @@ pub struct TextInput<'a, Message> {
|
||||||
pub on_submit: Option<Message>,
|
pub on_submit: Option<Message>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct State {
|
|
||||||
pub is_focused: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Message> TextInput<'a, Message> {
|
impl<'a, Message> TextInput<'a, Message> {
|
||||||
pub fn new<F>(
|
pub fn new<F>(
|
||||||
state: &'a mut State,
|
state: &'a mut State,
|
||||||
|
|
@ -30,7 +27,7 @@ impl<'a, Message> TextInput<'a, Message> {
|
||||||
Self {
|
Self {
|
||||||
state,
|
state,
|
||||||
placeholder: String::from(placeholder),
|
placeholder: String::from(placeholder),
|
||||||
value: String::from(value),
|
value: Value::new(value),
|
||||||
width: Length::Fill,
|
width: Length::Fill,
|
||||||
max_width: Length::Shrink,
|
max_width: Length::Shrink,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
|
@ -84,3 +81,84 @@ where
|
||||||
f.debug_struct("TextInput").finish()
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,26 +15,6 @@ struct Todos {
|
||||||
tasks: Vec<Task>,
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
InputChanged(String),
|
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
|
// Colors
|
||||||
const GRAY: Color = Color {
|
const GRAY: Color = Color {
|
||||||
r: 0.5,
|
r: 0.5,
|
||||||
|
|
|
||||||
|
|
@ -45,28 +45,55 @@ where
|
||||||
Event::Keyboard(keyboard::Event::CharacterReceived(c))
|
Event::Keyboard(keyboard::Event::CharacterReceived(c))
|
||||||
if self.state.is_focused && !c.is_control() =>
|
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());
|
self.value.insert(cursor_position, c);
|
||||||
|
self.state.move_cursor_right(&self.value);
|
||||||
|
|
||||||
|
let message = (self.on_change)(self.value.to_string());
|
||||||
messages.push(message);
|
messages.push(message);
|
||||||
}
|
}
|
||||||
Event::Keyboard(keyboard::Event::Input {
|
Event::Keyboard(keyboard::Event::Input {
|
||||||
key_code: keyboard::KeyCode::Backspace,
|
key_code,
|
||||||
state: ButtonState::Pressed,
|
state: ButtonState::Pressed,
|
||||||
}) => {
|
}) if self.state.is_focused => match key_code {
|
||||||
let _ = self.value.pop();
|
keyboard::KeyCode::Enter => {
|
||||||
|
if let Some(on_submit) = self.on_submit.clone() {
|
||||||
let message = (self.on_change)(self.value.clone());
|
messages.push(on_submit);
|
||||||
messages.push(message);
|
}
|
||||||
}
|
|
||||||
Event::Keyboard(keyboard::Event::Input {
|
|
||||||
key_code: keyboard::KeyCode::Enter,
|
|
||||||
state: ButtonState::Pressed,
|
|
||||||
}) => {
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,17 +55,18 @@ impl text_input::Renderer for Renderer {
|
||||||
};
|
};
|
||||||
|
|
||||||
let size = f32::from(text_input.size.unwrap_or(self.default_size()));
|
let size = f32::from(text_input.size.unwrap_or(self.default_size()));
|
||||||
|
let text = text_input.value.to_string();
|
||||||
|
|
||||||
let value = Primitive::Clip {
|
let value = Primitive::Clip {
|
||||||
bounds: text_bounds,
|
bounds: text_bounds,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
content: Box::new(Primitive::Text {
|
content: Box::new(Primitive::Text {
|
||||||
content: if text_input.value.is_empty() {
|
content: if text.is_empty() {
|
||||||
text_input.placeholder.clone()
|
text_input.placeholder.clone()
|
||||||
} else {
|
} else {
|
||||||
text_input.value.clone()
|
text.clone()
|
||||||
},
|
},
|
||||||
color: if text_input.value.is_empty() {
|
color: if text.is_empty() {
|
||||||
Color {
|
Color {
|
||||||
r: 0.7,
|
r: 0.7,
|
||||||
g: 0.7,
|
g: 0.7,
|
||||||
|
|
@ -95,11 +96,18 @@ impl text_input::Renderer for Renderer {
|
||||||
primitives: if text_input.state.is_focused {
|
primitives: if text_input.state.is_focused {
|
||||||
use wgpu_glyph::{GlyphCruncher, Scale, Section};
|
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
|
let mut text_value_width = self
|
||||||
.glyph_brush
|
.glyph_brush
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.glyph_bounds(Section {
|
.glyph_bounds(Section {
|
||||||
text: &text_input.value,
|
text: text_before_cursor,
|
||||||
bounds: (f32::INFINITY, text_bounds.height),
|
bounds: (f32::INFINITY, text_bounds.height),
|
||||||
scale: Scale { x: size, y: size },
|
scale: Scale { x: size, y: size },
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
@ -107,11 +115,24 @@ impl text_input::Renderer for Renderer {
|
||||||
.map(|bounds| bounds.width().round())
|
.map(|bounds| bounds.width().round())
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
let spaces_at_the_end = text_input.value.len()
|
let spaces_at_the_end = text_before_cursor.len()
|
||||||
- text_input.value.trim_end().len();
|
- text_before_cursor.trim_end().len();
|
||||||
|
|
||||||
if spaces_at_the_end > 0 {
|
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 {
|
let cursor = Primitive::Quad {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue