Merge pull request #202 from FabianLars/master
Text Selection for text_input widget
This commit is contained in:
commit
643fa18cae
@ -355,6 +355,10 @@ mod style {
|
|||||||
fn value_color(&self) -> Color {
|
fn value_color(&self) -> Color {
|
||||||
Color::WHITE
|
Color::WHITE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selection_color(&self) -> Color {
|
||||||
|
ACTIVE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Button;
|
pub struct Button;
|
||||||
|
@ -2,5 +2,8 @@
|
|||||||
mod button;
|
mod button;
|
||||||
mod event;
|
mod event;
|
||||||
|
|
||||||
|
pub mod click;
|
||||||
|
|
||||||
pub use button::Button;
|
pub use button::Button;
|
||||||
|
pub use click::Click;
|
||||||
pub use event::{Event, ScrollDelta};
|
pub use event::{Event, ScrollDelta};
|
||||||
|
76
native/src/input/mouse/click.rs
Normal file
76
native/src/input/mouse/click.rs
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -114,10 +114,10 @@ impl text_input::Renderer for Null {
|
|||||||
fn offset(
|
fn offset(
|
||||||
&self,
|
&self,
|
||||||
_text_bounds: Rectangle,
|
_text_bounds: Rectangle,
|
||||||
|
_font: Font,
|
||||||
_size: u16,
|
_size: u16,
|
||||||
_value: &text_input::Value,
|
_value: &text_input::Value,
|
||||||
_state: &text_input::State,
|
_state: &text_input::State,
|
||||||
_font: Font,
|
|
||||||
) -> f32 {
|
) -> f32 {
|
||||||
0.0
|
0.0
|
||||||
}
|
}
|
||||||
@ -127,8 +127,8 @@ impl text_input::Renderer for Null {
|
|||||||
_bounds: Rectangle,
|
_bounds: Rectangle,
|
||||||
_text_bounds: Rectangle,
|
_text_bounds: Rectangle,
|
||||||
_cursor_position: Point,
|
_cursor_position: Point,
|
||||||
_size: u16,
|
|
||||||
_font: Font,
|
_font: Font,
|
||||||
|
_size: u16,
|
||||||
_placeholder: &str,
|
_placeholder: &str,
|
||||||
_value: &text_input::Value,
|
_value: &text_input::Value,
|
||||||
_state: &text_input::State,
|
_state: &text_input::State,
|
||||||
|
@ -4,14 +4,27 @@
|
|||||||
//!
|
//!
|
||||||
//! [`TextInput`]: struct.TextInput.html
|
//! [`TextInput`]: struct.TextInput.html
|
||||||
//! [`State`]: struct.State.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::{
|
use crate::{
|
||||||
input::{keyboard, mouse, ButtonState},
|
input::{
|
||||||
|
keyboard,
|
||||||
|
mouse::{self, click},
|
||||||
|
ButtonState,
|
||||||
|
},
|
||||||
layout, Clipboard, Element, Event, Font, Hasher, Layout, Length, Point,
|
layout, Clipboard, Element, Event, Font, Hasher, Layout, Length, Point,
|
||||||
Rectangle, Size, Widget,
|
Rectangle, Size, Widget,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::u32;
|
use std::u32;
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
|
||||||
|
|
||||||
/// A field that can be filled with text.
|
/// A field that can be filled with text.
|
||||||
///
|
///
|
||||||
@ -209,6 +222,75 @@ where
|
|||||||
let text_layout = layout.children().next().unwrap();
|
let text_layout = layout.children().next().unwrap();
|
||||||
let target = cursor_position.x - text_layout.bounds().x;
|
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 {
|
if target > 0.0 {
|
||||||
let value = if self.is_secure {
|
let value = if self.is_secure {
|
||||||
self.value.secure()
|
self.value.secure()
|
||||||
@ -216,43 +298,33 @@ where
|
|||||||
self.value.clone()
|
self.value.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let size = self.size.unwrap_or(renderer.default_size());
|
let position = renderer.find_cursor_position(
|
||||||
|
|
||||||
let offset = renderer.offset(
|
|
||||||
text_layout.bounds(),
|
text_layout.bounds(),
|
||||||
size,
|
self.font,
|
||||||
|
self.size,
|
||||||
&value,
|
&value,
|
||||||
&self.state,
|
&self.state,
|
||||||
self.font,
|
target,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.state.cursor_position = find_cursor_position(
|
self.state.cursor.select_range(
|
||||||
renderer,
|
self.state.cursor.start(&value),
|
||||||
target + offset,
|
position,
|
||||||
&value,
|
|
||||||
size,
|
|
||||||
0,
|
|
||||||
self.value.len(),
|
|
||||||
self.font,
|
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
self.state.cursor_position = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.state.is_focused = is_clicked;
|
|
||||||
}
|
}
|
||||||
Event::Keyboard(keyboard::Event::CharacterReceived(c))
|
Event::Keyboard(keyboard::Event::CharacterReceived(c))
|
||||||
if self.state.is_focused
|
if self.state.is_focused
|
||||||
&& self.state.is_pasting.is_none()
|
&& self.state.is_pasting.is_none()
|
||||||
&& !c.is_control() =>
|
&& !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);
|
editor.insert(c);
|
||||||
self.state.move_cursor_right(&self.value);
|
|
||||||
|
|
||||||
let message = (self.on_change)(self.value.to_string());
|
let message = (self.on_change)(editor.contents());
|
||||||
messages.push(message);
|
messages.push(message);
|
||||||
}
|
}
|
||||||
Event::Keyboard(keyboard::Event::Input {
|
Event::Keyboard(keyboard::Event::Input {
|
||||||
@ -266,52 +338,74 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyboard::KeyCode::Backspace => {
|
keyboard::KeyCode::Backspace => {
|
||||||
let cursor_position =
|
let mut editor =
|
||||||
self.state.cursor_position(&self.value);
|
Editor::new(&mut self.value, &mut self.state.cursor);
|
||||||
|
|
||||||
if cursor_position > 0 {
|
editor.backspace();
|
||||||
self.state.move_cursor_left(&self.value);
|
|
||||||
|
|
||||||
let _ = self.value.remove(cursor_position - 1);
|
let message = (self.on_change)(editor.contents());
|
||||||
|
messages.push(message);
|
||||||
let message = (self.on_change)(self.value.to_string());
|
|
||||||
messages.push(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
keyboard::KeyCode::Delete => {
|
keyboard::KeyCode::Delete => {
|
||||||
let cursor_position =
|
let mut editor =
|
||||||
self.state.cursor_position(&self.value);
|
Editor::new(&mut self.value, &mut self.state.cursor);
|
||||||
|
|
||||||
if cursor_position < self.value.len() {
|
editor.delete();
|
||||||
let _ = self.value.remove(cursor_position);
|
|
||||||
|
|
||||||
let message = (self.on_change)(self.value.to_string());
|
let message = (self.on_change)(editor.contents());
|
||||||
messages.push(message);
|
messages.push(message);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
keyboard::KeyCode::Left => {
|
keyboard::KeyCode::Left => {
|
||||||
if platform::is_jump_modifier_pressed(modifiers)
|
if platform::is_jump_modifier_pressed(modifiers)
|
||||||
&& !self.is_secure
|
&& !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 {
|
} else {
|
||||||
self.state.move_cursor_left(&self.value);
|
self.state.cursor.move_left(&self.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyboard::KeyCode::Right => {
|
keyboard::KeyCode::Right => {
|
||||||
if platform::is_jump_modifier_pressed(modifiers)
|
if platform::is_jump_modifier_pressed(modifiers)
|
||||||
&& !self.is_secure
|
&& !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 {
|
} else {
|
||||||
self.state.move_cursor_right(&self.value);
|
self.state.cursor.move_right(&self.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyboard::KeyCode::Home => {
|
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 => {
|
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 => {
|
keyboard::KeyCode::V => {
|
||||||
if platform::is_copy_paste_modifier_pressed(modifiers) {
|
if platform::is_copy_paste_modifier_pressed(modifiers) {
|
||||||
@ -330,26 +424,27 @@ where
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let cursor_position =
|
let mut editor = Editor::new(
|
||||||
self.state.cursor_position(&self.value);
|
&mut self.value,
|
||||||
|
&mut self.state.cursor,
|
||||||
self.value
|
|
||||||
.insert_many(cursor_position, content.clone());
|
|
||||||
|
|
||||||
self.state.move_cursor_right_by_amount(
|
|
||||||
&self.value,
|
|
||||||
content.len(),
|
|
||||||
);
|
);
|
||||||
self.state.is_pasting = Some(content);
|
|
||||||
|
|
||||||
let message =
|
editor.paste(content.clone());
|
||||||
(self.on_change)(self.value.to_string());
|
|
||||||
|
let message = (self.on_change)(editor.contents());
|
||||||
messages.push(message);
|
messages.push(message);
|
||||||
|
|
||||||
|
self.state.is_pasting = Some(content);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.state.is_pasting = None;
|
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 {
|
Event::Keyboard(keyboard::Event::Input {
|
||||||
@ -381,8 +476,8 @@ where
|
|||||||
bounds,
|
bounds,
|
||||||
text_bounds,
|
text_bounds,
|
||||||
cursor_position,
|
cursor_position,
|
||||||
self.size.unwrap_or(renderer.default_size()),
|
|
||||||
self.font,
|
self.font,
|
||||||
|
self.size.unwrap_or(renderer.default_size()),
|
||||||
&self.placeholder,
|
&self.placeholder,
|
||||||
&self.value.secure(),
|
&self.value.secure(),
|
||||||
&self.state,
|
&self.state,
|
||||||
@ -393,8 +488,8 @@ where
|
|||||||
bounds,
|
bounds,
|
||||||
text_bounds,
|
text_bounds,
|
||||||
cursor_position,
|
cursor_position,
|
||||||
self.size.unwrap_or(renderer.default_size()),
|
|
||||||
self.font,
|
self.font,
|
||||||
|
self.size.unwrap_or(renderer.default_size()),
|
||||||
&self.placeholder,
|
&self.placeholder,
|
||||||
&self.value,
|
&self.value,
|
||||||
&self.state,
|
&self.state,
|
||||||
@ -447,10 +542,10 @@ pub trait Renderer: crate::Renderer + Sized {
|
|||||||
fn offset(
|
fn offset(
|
||||||
&self,
|
&self,
|
||||||
text_bounds: Rectangle,
|
text_bounds: Rectangle,
|
||||||
|
font: Font,
|
||||||
size: u16,
|
size: u16,
|
||||||
value: &Value,
|
value: &Value,
|
||||||
state: &State,
|
state: &State,
|
||||||
font: Font,
|
|
||||||
) -> f32;
|
) -> f32;
|
||||||
|
|
||||||
/// Draws a [`TextInput`].
|
/// Draws a [`TextInput`].
|
||||||
@ -471,13 +566,41 @@ pub trait Renderer: crate::Renderer + Sized {
|
|||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
text_bounds: Rectangle,
|
text_bounds: Rectangle,
|
||||||
cursor_position: Point,
|
cursor_position: Point,
|
||||||
size: u16,
|
|
||||||
font: Font,
|
font: Font,
|
||||||
|
size: u16,
|
||||||
placeholder: &str,
|
placeholder: &str,
|
||||||
value: &Value,
|
value: &Value,
|
||||||
state: &State,
|
state: &State,
|
||||||
style: &Self::Style,
|
style: &Self::Style,
|
||||||
) -> Self::Output;
|
) -> 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>>
|
impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>>
|
||||||
@ -499,8 +622,10 @@ where
|
|||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
is_focused: bool,
|
is_focused: bool,
|
||||||
|
is_dragging: bool,
|
||||||
is_pasting: Option<Value>,
|
is_pasting: Option<Value>,
|
||||||
cursor_position: usize,
|
last_click: Option<mouse::Click>,
|
||||||
|
cursor: Cursor,
|
||||||
// TODO: Add stateful horizontal scrolling offset
|
// TODO: Add stateful horizontal scrolling offset
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -516,12 +641,12 @@ impl State {
|
|||||||
///
|
///
|
||||||
/// [`State`]: struct.State.html
|
/// [`State`]: struct.State.html
|
||||||
pub fn focused() -> Self {
|
pub fn focused() -> Self {
|
||||||
use std::usize;
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
is_focused: true,
|
is_focused: true,
|
||||||
|
is_dragging: false,
|
||||||
is_pasting: None,
|
is_pasting: None,
|
||||||
cursor_position: usize::MAX,
|
last_click: None,
|
||||||
|
cursor: Cursor::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -532,207 +657,24 @@ impl State {
|
|||||||
self.is_focused
|
self.is_focused
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the cursor position of a [`TextInput`].
|
/// Returns the [`Cursor`] of the [`TextInput`].
|
||||||
///
|
///
|
||||||
|
/// [`Cursor`]: struct.Cursor.html
|
||||||
/// [`TextInput`]: struct.TextInput.html
|
/// [`TextInput`]: struct.TextInput.html
|
||||||
pub fn cursor_position(&self, value: &Value) -> usize {
|
pub fn cursor(&self) -> Cursor {
|
||||||
self.cursor_position.min(value.len())
|
self.cursor
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves the cursor of a [`TextInput`] to the left.
|
|
||||||
///
|
|
||||||
/// [`TextInput`]: struct.TextInput.html
|
|
||||||
pub(crate) fn move_cursor_left(&mut self, value: &Value) {
|
|
||||||
let current = self.cursor_position(value);
|
|
||||||
|
|
||||||
if current > 0 {
|
|
||||||
self.cursor_position = current - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves the cursor of a [`TextInput`] to the right.
|
|
||||||
///
|
|
||||||
/// [`TextInput`]: struct.TextInput.html
|
|
||||||
pub(crate) fn move_cursor_right(&mut self, value: &Value) {
|
|
||||||
self.move_cursor_right_by_amount(value, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn move_cursor_right_by_amount(
|
|
||||||
&mut self,
|
|
||||||
value: &Value,
|
|
||||||
amount: usize,
|
|
||||||
) {
|
|
||||||
let current = self.cursor_position(value);
|
|
||||||
let new_position = current.saturating_add(amount);
|
|
||||||
|
|
||||||
if new_position < value.len() + 1 {
|
|
||||||
self.cursor_position = new_position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves the cursor of a [`TextInput`] to the previous start of a word.
|
|
||||||
///
|
|
||||||
/// [`TextInput`]: struct.TextInput.html
|
|
||||||
pub(crate) fn move_cursor_left_by_words(&mut self, value: &Value) {
|
|
||||||
let current = self.cursor_position(value);
|
|
||||||
|
|
||||||
self.cursor_position = value.previous_start_of_word(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves the cursor of a [`TextInput`] to the next end of a word.
|
|
||||||
///
|
|
||||||
/// [`TextInput`]: struct.TextInput.html
|
|
||||||
pub(crate) fn move_cursor_right_by_words(&mut self, value: &Value) {
|
|
||||||
let current = self.cursor_position(value);
|
|
||||||
|
|
||||||
self.cursor_position = value.next_end_of_word(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves the cursor of a [`TextInput`] to the end.
|
|
||||||
///
|
|
||||||
/// [`TextInput`]: struct.TextInput.html
|
|
||||||
pub(crate) fn move_cursor_to_end(&mut self, value: &Value) {
|
|
||||||
self.cursor_position = value.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Reduce allocations
|
// TODO: Reduce allocations
|
||||||
fn find_cursor_position<Renderer: self::Renderer>(
|
fn find_cursor_position<Renderer: self::Renderer>(
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
target: f32,
|
|
||||||
value: &Value,
|
value: &Value,
|
||||||
|
font: Font,
|
||||||
size: u16,
|
size: u16,
|
||||||
|
target: f32,
|
||||||
start: usize,
|
start: usize,
|
||||||
end: usize,
|
end: usize,
|
||||||
font: Font,
|
|
||||||
) -> usize {
|
) -> usize {
|
||||||
if start >= end {
|
if start >= end {
|
||||||
if start == 0 {
|
if start == 0 {
|
||||||
@ -760,22 +702,22 @@ fn find_cursor_position<Renderer: self::Renderer>(
|
|||||||
if width > target {
|
if width > target {
|
||||||
find_cursor_position(
|
find_cursor_position(
|
||||||
renderer,
|
renderer,
|
||||||
target,
|
|
||||||
value,
|
value,
|
||||||
|
font,
|
||||||
size,
|
size,
|
||||||
|
target,
|
||||||
start,
|
start,
|
||||||
start + index,
|
start + index,
|
||||||
font,
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
find_cursor_position(
|
find_cursor_position(
|
||||||
renderer,
|
renderer,
|
||||||
target,
|
|
||||||
value,
|
value,
|
||||||
|
font,
|
||||||
size,
|
size,
|
||||||
|
target,
|
||||||
start + index + 1,
|
start + index + 1,
|
||||||
end,
|
end,
|
||||||
font,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
191
native/src/widget/text_input/cursor.rs
Normal file
191
native/src/widget/text_input/cursor.rs
Normal file
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
native/src/widget/text_input/editor.rs
Normal file
78
native/src/widget/text_input/editor.rs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
134
native/src/widget/text_input/value.rs
Normal file
134
native/src/widget/text_input/value.rs
Normal file
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -33,6 +33,8 @@ pub trait StyleSheet {
|
|||||||
|
|
||||||
fn value_color(&self) -> Color;
|
fn value_color(&self) -> Color;
|
||||||
|
|
||||||
|
fn selection_color(&self) -> Color;
|
||||||
|
|
||||||
/// Produces the style of an hovered text input.
|
/// Produces the style of an hovered text input.
|
||||||
fn hovered(&self) -> Style {
|
fn hovered(&self) -> Style {
|
||||||
self.focused()
|
self.focused()
|
||||||
@ -65,6 +67,10 @@ impl StyleSheet for Default {
|
|||||||
fn value_color(&self) -> Color {
|
fn value_color(&self) -> Color {
|
||||||
Color::from_rgb(0.3, 0.3, 0.3)
|
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> {
|
impl std::default::Default for Box<dyn StyleSheet> {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use crate::{text_input::StyleSheet, Primitive, Renderer};
|
use crate::{text_input::StyleSheet, Primitive, Renderer};
|
||||||
|
|
||||||
use iced_native::{
|
use iced_native::{
|
||||||
text_input, Background, Color, Font, HorizontalAlignment, MouseCursor,
|
text_input::{self, cursor},
|
||||||
Point, Rectangle, Size, Vector, VerticalAlignment,
|
Background, Color, Font, HorizontalAlignment, MouseCursor, Point,
|
||||||
|
Rectangle, Size, Vector, VerticalAlignment,
|
||||||
};
|
};
|
||||||
use std::f32;
|
use std::f32;
|
||||||
|
|
||||||
@ -35,18 +36,25 @@ impl text_input::Renderer for Renderer {
|
|||||||
fn offset(
|
fn offset(
|
||||||
&self,
|
&self,
|
||||||
text_bounds: Rectangle,
|
text_bounds: Rectangle,
|
||||||
|
font: Font,
|
||||||
size: u16,
|
size: u16,
|
||||||
value: &text_input::Value,
|
value: &text_input::Value,
|
||||||
state: &text_input::State,
|
state: &text_input::State,
|
||||||
font: Font,
|
|
||||||
) -> f32 {
|
) -> f32 {
|
||||||
if state.is_focused() {
|
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(
|
let (_, offset) = measure_cursor_and_scroll_offset(
|
||||||
self,
|
self,
|
||||||
text_bounds,
|
text_bounds,
|
||||||
value,
|
value,
|
||||||
size,
|
size,
|
||||||
state.cursor_position(value),
|
focus_position,
|
||||||
font,
|
font,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -61,8 +69,8 @@ impl text_input::Renderer for Renderer {
|
|||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
text_bounds: Rectangle,
|
text_bounds: Rectangle,
|
||||||
cursor_position: Point,
|
cursor_position: Point,
|
||||||
size: u16,
|
|
||||||
font: Font,
|
font: Font,
|
||||||
|
size: u16,
|
||||||
placeholder: &str,
|
placeholder: &str,
|
||||||
value: &text_input::Value,
|
value: &text_input::Value,
|
||||||
state: &text_input::State,
|
state: &text_input::State,
|
||||||
@ -111,31 +119,91 @@ impl text_input::Renderer for Renderer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (contents_primitive, offset) = if state.is_focused() {
|
let (contents_primitive, offset) = if state.is_focused() {
|
||||||
let (text_value_width, offset) = measure_cursor_and_scroll_offset(
|
let cursor = state.cursor();
|
||||||
self,
|
|
||||||
text_bounds,
|
|
||||||
value,
|
|
||||||
size,
|
|
||||||
state.cursor_position(value),
|
|
||||||
font,
|
|
||||||
);
|
|
||||||
|
|
||||||
let cursor = Primitive::Quad {
|
let (cursor_primitive, offset) = match cursor.state(value) {
|
||||||
bounds: Rectangle {
|
cursor::State::Index(position) => {
|
||||||
x: text_bounds.x + text_value_width,
|
let (text_value_width, offset) =
|
||||||
y: text_bounds.y,
|
measure_cursor_and_scroll_offset(
|
||||||
width: 1.0,
|
self,
|
||||||
height: text_bounds.height,
|
text_bounds,
|
||||||
},
|
value,
|
||||||
background: Background::Color(style_sheet.value_color()),
|
size,
|
||||||
border_radius: 0,
|
position,
|
||||||
border_width: 0,
|
font,
|
||||||
border_color: Color::TRANSPARENT,
|
);
|
||||||
|
|
||||||
|
(
|
||||||
|
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 {
|
Primitive::Group {
|
||||||
primitives: vec![text_value, cursor],
|
primitives: vec![cursor_primitive, text_value],
|
||||||
},
|
},
|
||||||
Vector::new(offset as u32, 0),
|
Vector::new(offset as u32, 0),
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user