improv(text_input): optimize, fix, and improve the text inputs

This commit is contained in:
Michael Aaron Murphy 2025-03-19 16:43:43 +01:00
parent 92b2756e26
commit c538d672df
No known key found for this signature in database
GPG key ID: B2732D4240C9212C
4 changed files with 291 additions and 209 deletions

View file

@ -27,6 +27,7 @@ pub enum State {
} }
impl Default for Cursor { impl Default for Cursor {
#[inline]
fn default() -> Self { fn default() -> Self {
Self { Self {
state: State::Index(0), state: State::Index(0),
@ -37,6 +38,7 @@ impl Default for Cursor {
impl Cursor { impl Cursor {
/// Returns the [`State`] of the [`Cursor`]. /// Returns the [`State`] of the [`Cursor`].
#[must_use] #[must_use]
#[inline(never)]
pub fn state(&self, value: &Value) -> State { pub fn state(&self, value: &Value) -> State {
match self.state { match self.state {
State::Index(index) => State::Index(index.min(value.len())), State::Index(index) => State::Index(index.min(value.len())),
@ -57,6 +59,7 @@ impl Cursor {
/// ///
/// `start` is guaranteed to be <= than `end`. /// `start` is guaranteed to be <= than `end`.
#[must_use] #[must_use]
#[inline]
pub fn selection(&self, value: &Value) -> Option<(usize, usize)> { pub fn selection(&self, value: &Value) -> Option<(usize, usize)> {
match self.state(value) { match self.state(value) {
State::Selection { start, end } => Some((start.min(end), start.max(end))), State::Selection { start, end } => Some((start.min(end), start.max(end))),
@ -64,18 +67,22 @@ impl Cursor {
} }
} }
#[inline]
pub(crate) fn move_to(&mut self, position: usize) { pub(crate) fn move_to(&mut self, position: usize) {
self.state = State::Index(position); self.state = State::Index(position);
} }
#[inline]
pub(crate) fn move_right(&mut self, value: &Value) { pub(crate) fn move_right(&mut self, value: &Value) {
self.move_right_by_amount(value, 1); self.move_right_by_amount(value, 1);
} }
#[inline]
pub(crate) fn move_right_by_words(&mut self, value: &Value) { pub(crate) fn move_right_by_words(&mut self, value: &Value) {
self.move_to(value.next_end_of_word(self.right(value))); self.move_to(value.next_end_of_word(self.right(value)));
} }
#[inline]
pub(crate) fn move_right_by_amount(&mut self, value: &Value, amount: usize) { pub(crate) fn move_right_by_amount(&mut self, value: &Value, amount: usize) {
match self.state(value) { match self.state(value) {
State::Index(index) => self.move_to(index.saturating_add(amount).min(value.len())), State::Index(index) => self.move_to(index.saturating_add(amount).min(value.len())),
@ -83,6 +90,7 @@ impl Cursor {
} }
} }
#[inline]
pub(crate) fn move_left(&mut self, value: &Value) { pub(crate) fn move_left(&mut self, value: &Value) {
match self.state(value) { match self.state(value) {
State::Index(index) if index > 0 => self.move_to(index - 1), State::Index(index) if index > 0 => self.move_to(index - 1),
@ -91,18 +99,21 @@ impl Cursor {
} }
} }
#[inline]
pub(crate) fn move_left_by_words(&mut self, value: &Value) { pub(crate) fn move_left_by_words(&mut self, value: &Value) {
self.move_to(value.previous_start_of_word(self.left(value))); self.move_to(value.previous_start_of_word(self.left(value)));
} }
#[inline]
pub(crate) fn select_range(&mut self, start: usize, end: usize) { pub(crate) fn select_range(&mut self, start: usize, end: usize) {
if start == end { self.state = if start == end {
self.state = State::Index(start); State::Index(start)
} else { } else {
self.state = State::Selection { start, end }; State::Selection { start, end }
} };
} }
#[inline]
pub(crate) fn select_left(&mut self, value: &Value) { pub(crate) fn select_left(&mut self, value: &Value) {
match self.state(value) { match self.state(value) {
State::Index(index) if index > 0 => self.select_range(index, index - 1), State::Index(index) if index > 0 => self.select_range(index, index - 1),
@ -111,6 +122,7 @@ impl Cursor {
} }
} }
#[inline]
pub(crate) fn select_right(&mut self, value: &Value) { pub(crate) fn select_right(&mut self, value: &Value) {
match self.state(value) { match self.state(value) {
State::Index(index) if index < value.len() => self.select_range(index, index + 1), State::Index(index) if index < value.len() => self.select_range(index, index + 1),
@ -121,6 +133,7 @@ impl Cursor {
} }
} }
#[inline]
pub(crate) fn select_left_by_words(&mut self, value: &Value) { pub(crate) fn select_left_by_words(&mut self, value: &Value) {
match self.state(value) { match self.state(value) {
State::Index(index) => self.select_range(index, value.previous_start_of_word(index)), State::Index(index) => self.select_range(index, value.previous_start_of_word(index)),
@ -130,6 +143,7 @@ impl Cursor {
} }
} }
#[inline]
pub(crate) fn select_right_by_words(&mut self, value: &Value) { pub(crate) fn select_right_by_words(&mut self, value: &Value) {
match self.state(value) { match self.state(value) {
State::Index(index) => self.select_range(index, value.next_end_of_word(index)), State::Index(index) => self.select_range(index, value.next_end_of_word(index)),
@ -139,10 +153,12 @@ impl Cursor {
} }
} }
#[inline]
pub(crate) fn select_all(&mut self, value: &Value) { pub(crate) fn select_all(&mut self, value: &Value) {
self.select_range(0, value.len()); self.select_range(0, value.len());
} }
#[inline]
pub(crate) fn start(&self, value: &Value) -> usize { pub(crate) fn start(&self, value: &Value) -> usize {
let start = match self.state { let start = match self.state {
State::Index(index) => index, State::Index(index) => index,
@ -152,6 +168,7 @@ impl Cursor {
start.min(value.len()) start.min(value.len())
} }
#[inline]
pub(crate) fn end(&self, value: &Value) -> usize { pub(crate) fn end(&self, value: &Value) -> usize {
let end = match self.state { let end = match self.state {
State::Index(index) => index, State::Index(index) => index,
@ -161,6 +178,7 @@ impl Cursor {
end.min(value.len()) end.min(value.len())
} }
#[inline]
fn left(&self, value: &Value) -> usize { fn left(&self, value: &Value) -> usize {
match self.state(value) { match self.state(value) {
State::Index(index) => index, State::Index(index) => index,
@ -168,6 +186,7 @@ impl Cursor {
} }
} }
#[inline]
fn right(&self, value: &Value) -> usize { fn right(&self, value: &Value) -> usize {
match self.state(value) { match self.state(value) {
State::Index(index) => index, State::Index(index) => index,

View file

@ -10,11 +10,13 @@ pub struct Editor<'a> {
} }
impl<'a> Editor<'a> { impl<'a> Editor<'a> {
#[inline]
pub fn new(value: &'a mut Value, cursor: &'a mut Cursor) -> Editor<'a> { pub fn new(value: &'a mut Value, cursor: &'a mut Cursor) -> Editor<'a> {
Editor { value, cursor } Editor { value, cursor }
} }
#[must_use] #[must_use]
#[inline]
pub fn contents(&self) -> String { pub fn contents(&self) -> String {
self.value.to_string() self.value.to_string()
} }

View file

@ -18,9 +18,9 @@ use super::style::StyleSheet;
pub use super::value::Value; pub use super::value::Value;
use apply::Apply; use apply::Apply;
use iced::Limits;
use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent}; use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent};
use iced::clipboard::mime::AsMimeTypes; use iced::clipboard::mime::AsMimeTypes;
use iced::Limits;
use iced_core::event::{self, Event}; use iced_core::event::{self, Event};
use iced_core::mouse::{self, click}; use iced_core::mouse::{self, click};
use iced_core::overlay::Group; use iced_core::overlay::Group;
@ -28,18 +28,18 @@ use iced_core::renderer::{self, Renderer as CoreRenderer};
use iced_core::text::{self, Paragraph, Renderer, Text}; use iced_core::text::{self, Paragraph, Renderer, Text};
use iced_core::time::{Duration, Instant}; use iced_core::time::{Duration, Instant};
use iced_core::touch; use iced_core::touch;
use iced_core::widget::Id;
use iced_core::widget::operation::{self, Operation}; use iced_core::widget::operation::{self, Operation};
use iced_core::widget::tree::{self, Tree}; use iced_core::widget::tree::{self, Tree};
use iced_core::widget::Id;
use iced_core::window; use iced_core::window;
use iced_core::{alignment, Background}; use iced_core::{Background, alignment};
use iced_core::{keyboard, Border, Shadow}; use iced_core::{Border, Shadow, keyboard};
use iced_core::{layout, overlay};
use iced_core::{ use iced_core::{
Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size,
Vector, Widget, Vector, Widget,
}; };
use iced_runtime::{task, Action, Task}; use iced_core::{layout, overlay};
use iced_runtime::{Action, Task, task};
thread_local! { thread_local! {
// Prevents two inputs from being focused at the same time. // Prevents two inputs from being focused at the same time.
@ -59,7 +59,7 @@ where
TextInput::new(placeholder, value) TextInput::new(placeholder, value)
} }
/// A text label whiich can transform into a text input on activation. /// A text label which can transform into a text input on activation.
pub fn editable_input<'a, Message: Clone + 'static>( pub fn editable_input<'a, Message: Clone + 'static>(
placeholder: impl Into<Cow<'a, str>>, placeholder: impl Into<Cow<'a, str>>,
text: impl Into<Cow<'a, str>>, text: impl Into<Cow<'a, str>>,
@ -182,7 +182,7 @@ pub struct TextInput<'a, Message> {
placeholder: Cow<'a, str>, placeholder: Cow<'a, str>,
value: Value, value: Value,
is_secure: bool, is_secure: bool,
is_editable: bool, is_editable_variant: bool,
is_read_only: bool, is_read_only: bool,
select_on_focus: bool, select_on_focus: bool,
font: Option<<crate::Renderer as iced_core::text::Renderer>::Font>, font: Option<<crate::Renderer as iced_core::text::Renderer>::Font>,
@ -193,8 +193,11 @@ pub struct TextInput<'a, Message> {
label: Option<Cow<'a, str>>, label: Option<Cow<'a, str>>,
helper_text: Option<Cow<'a, str>>, helper_text: Option<Cow<'a, str>>,
error: Option<Cow<'a, str>>, error: Option<Cow<'a, str>>,
on_focus: Option<Message>,
on_unfocus: Option<Message>,
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>, on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_tab: Option<Message>,
on_submit: Option<Box<dyn Fn(String) -> Message + 'a>>, on_submit: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_toggle_edit: Option<Box<dyn Fn(bool) -> Message + 'a>>, on_toggle_edit: Option<Box<dyn Fn(bool) -> Message + 'a>>,
leading_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>, leading_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>,
@ -228,7 +231,7 @@ where
placeholder: placeholder.into(), placeholder: placeholder.into(),
value: Value::new(v.as_ref()), value: Value::new(v.as_ref()),
is_secure: false, is_secure: false,
is_editable: false, is_editable_variant: false,
is_read_only: false, is_read_only: false,
select_on_focus: false, select_on_focus: false,
font: None, font: None,
@ -237,9 +240,12 @@ where
size: None, size: None,
helper_size: 10.0, helper_size: 10.0,
helper_line_height: text::LineHeight::Absolute(14.0.into()), helper_line_height: text::LineHeight::Absolute(14.0.into()),
on_focus: None,
on_unfocus: None,
on_input: None, on_input: None,
on_paste: None, on_paste: None,
on_submit: None, on_submit: None,
on_tab: None,
on_toggle_edit: None, on_toggle_edit: None,
leading_icon: None, leading_icon: None,
trailing_icon: None, trailing_icon: None,
@ -256,6 +262,7 @@ where
} }
} }
#[inline]
fn dnd_id(&self) -> u128 { fn dnd_id(&self) -> u128 {
match &self.id.0 { match &self.id.0 {
iced_core::id::Internal::Custom(id, _) | iced_core::id::Internal::Unique(id) => { iced_core::id::Internal::Custom(id, _) | iced_core::id::Internal::Unique(id) => {
@ -267,7 +274,8 @@ where
/// Sets the input to be always active. /// Sets the input to be always active.
/// This makes it behave as if it was always focused. /// This makes it behave as if it was always focused.
pub fn always_active(mut self) -> Self { #[inline]
pub const fn always_active(mut self) -> Self {
self.always_active = true; self.always_active = true;
self self
} }
@ -285,6 +293,7 @@ where
} }
/// Sets the [`Id`] of the [`TextInput`]. /// Sets the [`Id`] of the [`TextInput`].
#[inline]
pub fn id(mut self, id: Id) -> Self { pub fn id(mut self, id: Id) -> Self {
self.id = id; self.id = id;
self self
@ -303,66 +312,85 @@ where
} }
/// Converts the [`TextInput`] into a secure password input. /// Converts the [`TextInput`] into a secure password input.
pub fn password(mut self) -> Self { #[inline]
pub const fn password(mut self) -> Self {
self.is_secure = true; self.is_secure = true;
self self
} }
pub fn editable(mut self) -> Self { /// Applies behaviors unique to the `editable_input` variable.
self.is_editable = true; #[inline]
pub(crate) const fn editable(mut self) -> Self {
self.is_editable_variant = true;
self self
} }
pub fn editing(mut self, enable: bool) -> Self { #[inline]
pub const fn editing(mut self, enable: bool) -> Self {
self.is_read_only = !enable; self.is_read_only = !enable;
self self
} }
/// Selects all text when the text input is focused /// Selects all text when the text input is focused
pub fn select_on_focus(mut self, select_on_focus: bool) -> Self { #[inline]
pub const fn select_on_focus(mut self, select_on_focus: bool) -> Self {
self.select_on_focus = select_on_focus; self.select_on_focus = select_on_focus;
self self
} }
/// Emits a message when an unfocused text input has been focused by click.
///
/// This will not trigger if the input was focused externally by the application.
#[inline]
pub fn on_focus(mut self, on_focus: Message) -> Self {
self.on_focus = Some(on_focus);
self
}
/// Emits a message when a focused text input has been unfocused via the Tab or Esc key.
///
/// This will not trigger if the input was unfocused externally by the application.
#[inline]
pub fn on_unfocus(mut self, on_unfocus: Message) -> Self {
self.on_unfocus = Some(on_unfocus);
self
}
/// Sets the message that should be produced when some text is typed into /// Sets the message that should be produced when some text is typed into
/// the [`TextInput`]. /// the [`TextInput`].
/// ///
/// If this method is not called, the [`TextInput`] will be disabled. /// If this method is not called, the [`TextInput`] will be disabled.
pub fn on_input<F>(mut self, callback: F) -> Self pub fn on_input(mut self, callback: impl Fn(String) -> Message + 'a) -> Self {
where
F: 'a + Fn(String) -> Message,
{
self.on_input = Some(Box::new(callback)); self.on_input = Some(Box::new(callback));
self self
} }
/// Sets the message that should be produced when the [`TextInput`] is /// Emits a message when a focused text input receives the Enter/Return key.
/// focused and the enter key is pressed. pub fn on_submit(mut self, callback: impl Fn(String) -> Message + 'a) -> Self {
pub fn on_submit<F>(self, callback: F) -> Self self.on_submit = Some(Box::new(callback));
where
F: 'a + Fn(String) -> Message,
{
self.on_submit_maybe(Some(Box::new(callback)))
}
/// Maybe sets the message that should be produced when the [`TextInput`] is
/// focused and the enter key is pressed.
pub fn on_submit_maybe<F>(mut self, callback: Option<F>) -> Self
where
F: 'a + Fn(String) -> Message,
{
if let Some(callback) = callback {
self.on_submit = Some(Box::new(callback));
} else {
self.on_submit = None;
}
self self
} }
pub fn on_toggle_edit<F>(mut self, callback: F) -> Self /// Optionally emits a message when a focused text input receives the Enter/Return key.
where pub fn on_submit_maybe(self, callback: Option<impl Fn(String) -> Message + 'a>) -> Self {
F: 'a + Fn(bool) -> Message, if let Some(callback) = callback {
{ self.on_submit(callback)
} else {
self
}
}
/// Emits a message when the Tab key has been captured, which prevents focus from changing.
///
/// If you do no want to capture the Tab key, use [`TextInput::on_unfocus`] instead.
#[inline]
pub fn on_tab(mut self, on_tab: Message) -> Self {
self.on_tab = Some(on_tab);
self
}
/// Emits a message when the editable state of the input changes.
pub fn on_toggle_edit(mut self, callback: impl Fn(bool) -> Message + 'a) -> Self {
self.on_toggle_edit = Some(Box::new(callback)); self.on_toggle_edit = Some(Box::new(callback));
self self
} }
@ -377,12 +405,17 @@ where
/// Sets the [`Font`] of the [`TextInput`]. /// Sets the [`Font`] of the [`TextInput`].
/// ///
/// [`Font`]: text::Renderer::Font /// [`Font`]: text::Renderer::Font
pub fn font(mut self, font: <crate::Renderer as iced_core::text::Renderer>::Font) -> Self { #[inline]
pub const fn font(
mut self,
font: <crate::Renderer as iced_core::text::Renderer>::Font,
) -> Self {
self.font = Some(font); self.font = Some(font);
self self
} }
/// Sets the start [`Icon`] of the [`TextInput`]. /// Sets the start [`Icon`] of the [`TextInput`].
#[inline]
pub fn leading_icon( pub fn leading_icon(
mut self, mut self,
icon: Element<'a, Message, crate::Theme, crate::Renderer>, icon: Element<'a, Message, crate::Theme, crate::Renderer>,
@ -392,6 +425,7 @@ where
} }
/// Sets the end [`Icon`] of the [`TextInput`]. /// Sets the end [`Icon`] of the [`TextInput`].
#[inline]
pub fn trailing_icon( pub fn trailing_icon(
mut self, mut self,
icon: Element<'a, Message, crate::Theme, crate::Renderer>, icon: Element<'a, Message, crate::Theme, crate::Renderer>,
@ -407,7 +441,7 @@ where
} }
/// Sets the [`Padding`] of the [`TextInput`]. /// Sets the [`Padding`] of the [`TextInput`].
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
self.padding = padding.into(); self.padding = padding.into();
self self
} }
@ -425,8 +459,9 @@ where
} }
/// Sets the text input to manage its input value or not /// Sets the text input to manage its input value or not
pub fn manage_value(mut self, manage_value: bool) -> Self { #[inline]
self.manage_value = true; pub const fn manage_value(mut self, manage_value: bool) -> Self {
self.manage_value = manage_value;
self self
} }
@ -435,6 +470,7 @@ where
/// ///
/// [`Renderer`]: text::Renderer /// [`Renderer`]: text::Renderer
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[inline]
pub fn draw( pub fn draw(
&self, &self,
tree: &Tree, tree: &Tree,
@ -484,13 +520,15 @@ where
/// Sets the window id of the [`TextInput`] and the window id of the drag icon. /// Sets the window id of the [`TextInput`] and the window id of the drag icon.
/// Both ids are required to be unique. /// Both ids are required to be unique.
/// This is required for the dnd to work. /// This is required for the dnd to work.
pub fn surface_ids(mut self, window_id: (window::Id, window::Id)) -> Self { #[inline]
pub const fn surface_ids(mut self, window_id: (window::Id, window::Id)) -> Self {
self.surface_ids = Some(window_id); self.surface_ids = Some(window_id);
self self
} }
/// Sets the mode of this [`TextInput`] to be a drag and drop icon. /// Sets the mode of this [`TextInput`] to be a drag and drop icon.
pub fn dnd_icon(mut self, dnd_icon: bool) -> Self { #[inline]
pub const fn dnd_icon(mut self, dnd_icon: bool) -> Self {
self.dnd_icon = dnd_icon; self.dnd_icon = dnd_icon;
self self
} }
@ -508,6 +546,7 @@ where
} }
/// Get the layout node of the actual text input /// Get the layout node of the actual text input
fn text_layout<'b>(&'a self, layout: Layout<'b>) -> Layout<'b> { fn text_layout<'b>(&'a self, layout: Layout<'b>) -> Layout<'b> {
if self.dnd_icon { if self.dnd_icon {
layout layout
@ -525,10 +564,12 @@ impl<Message> Widget<Message, crate::Theme, crate::Renderer> for TextInput<'_, M
where where
Message: Clone + 'static, Message: Clone + 'static,
{ {
#[inline]
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>() tree::Tag::of::<State>()
} }
#[inline]
fn state(&self) -> tree::State { fn state(&self) -> tree::State {
tree::State::new(State::new( tree::State::new(State::new(
self.is_secure, self.is_secure,
@ -540,6 +581,7 @@ where
fn diff(&mut self, tree: &mut Tree) { fn diff(&mut self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<State>(); let state = tree.state.downcast_mut::<State>();
if !self.manage_value || !self.value.is_empty() && state.tracked_value != self.value { if !self.manage_value || !self.value.is_empty() && state.tracked_value != self.value {
state.tracked_value = self.value.clone(); state.tracked_value = self.value.clone();
} else if self.value.is_empty() { } else if self.value.is_empty() {
@ -586,8 +628,6 @@ where
state.dirty = true; state.dirty = true;
} }
self.is_read_only = state.is_read_only;
if self.always_active && state.is_focused.is_none() { if self.always_active && state.is_focused.is_none() {
let now = Instant::now(); let now = Instant::now();
LAST_FOCUS_UPDATE.with(|x| x.set(now)); LAST_FOCUS_UPDATE.with(|x| x.set(now));
@ -607,12 +647,18 @@ where
}; };
} }
if !state.is_focused.as_ref().map_or(false, |f| { if !state
f.updated_at == LAST_FOCUS_UPDATE.with(|f| f.get()) .is_focused
}) { .as_ref()
state.is_focused = None; .map(|f| f.updated_at == LAST_FOCUS_UPDATE.with(|f| f.get()))
.unwrap_or_default()
{
state.unfocus();
// state.is_read_only = true;
} }
self.is_read_only = state.is_read_only;
// Stop pasting if input becomes disabled // Stop pasting if input becomes disabled
if !self.manage_value && self.on_input.is_none() { if !self.manage_value && self.on_input.is_none() {
state.is_pasting = None; state.is_pasting = None;
@ -635,6 +681,7 @@ where
.collect() .collect()
} }
#[inline]
fn size(&self) -> Size<Length> { fn size(&self) -> Size<Length> {
Size { Size {
width: self.width, width: self.width,
@ -790,7 +837,8 @@ where
let size = self.size.unwrap_or_else(|| renderer.default_size().0); let size = self.size.unwrap_or_else(|| renderer.default_size().0);
let line_height = self.line_height; let line_height = self.line_height;
if self.is_editable { // Disables editing of the editable variant when clicking outside of it.
if self.is_editable_variant {
if let Some(ref on_edit) = self.on_toggle_edit { if let Some(ref on_edit) = self.on_toggle_edit {
let state = tree.state.downcast_mut::<State>(); let state = tree.state.downcast_mut::<State>();
if !state.is_read_only && state.is_focused.is_none() { if !state.is_read_only && state.is_focused.is_none() {
@ -800,34 +848,38 @@ where
} }
} }
// Calculates the layout of the trailing icon button element.
if !tree.children.is_empty() { if !tree.children.is_empty() {
let index = tree.children.len() - 1; let index = tree.children.len() - 1;
if let (Some(trailing_icon), Some(tree)) = if let (Some(trailing_icon), Some(tree)) =
(self.trailing_icon.as_mut(), tree.children.get_mut(index)) (self.trailing_icon.as_mut(), tree.children.get_mut(index))
{ {
let children = text_layout.children(); trailing_icon_layout = Some(text_layout.children().last().unwrap());
trailing_icon_layout = Some(children.last().unwrap());
if let Some(trailing_layout) = trailing_icon_layout { // Enable custom buttons defined on the trailing icon position to be handled.
if cursor_position.is_over(trailing_layout.bounds()) { if !self.is_editable_variant {
let res = trailing_icon.as_widget_mut().on_event( if let Some(trailing_layout) = trailing_icon_layout {
tree, if cursor_position.is_over(trailing_layout.bounds()) {
event.clone(), let res = trailing_icon.as_widget_mut().on_event(
trailing_layout, tree,
cursor_position, event.clone(),
renderer, trailing_layout,
clipboard, cursor_position,
shell, renderer,
viewport, clipboard,
); shell,
viewport,
);
if res == event::Status::Captured { if res == event::Status::Captured {
return res; return res;
}
} }
} }
} }
} }
} }
let dnd_id = self.dnd_id(); let dnd_id = self.dnd_id();
let id = Widget::id(self); let id = Widget::id(self);
update( update(
@ -841,11 +893,14 @@ where
&mut self.value, &mut self.value,
size, size,
font, font,
self.is_editable_variant,
self.is_secure, self.is_secure,
self.is_editable, self.on_focus.as_ref(),
self.on_unfocus.as_ref(),
self.on_input.as_deref(), self.on_input.as_deref(),
self.on_paste.as_deref(), self.on_paste.as_deref(),
self.on_submit.as_deref(), self.on_submit.as_deref(),
self.on_tab.as_ref(),
self.on_toggle_edit.as_deref(), self.on_toggle_edit.as_deref(),
|| tree.state.downcast_mut::<State>(), || tree.state.downcast_mut::<State>(),
self.on_create_dnd_source.as_deref(), self.on_create_dnd_source.as_deref(),
@ -856,6 +911,7 @@ where
) )
} }
#[inline]
fn draw( fn draw(
&self, &self,
tree: &Tree, tree: &Tree,
@ -908,9 +964,7 @@ where
if let (Some(leading_icon), Some(tree)) = if let (Some(leading_icon), Some(tree)) =
(self.leading_icon.as_ref(), state.children.get(index)) (self.leading_icon.as_ref(), state.children.get(index))
{ {
let mut children = layout.children(); let leading_icon_layout = layout.children().nth(1).unwrap();
children.next();
let leading_icon_layout = children.next().unwrap();
if cursor_position.is_over(leading_icon_layout.bounds()) { if cursor_position.is_over(leading_icon_layout.bounds()) {
return leading_icon.as_widget().mouse_interaction( return leading_icon.as_widget().mouse_interaction(
@ -954,19 +1008,21 @@ where
) )
} }
#[inline]
fn id(&self) -> Option<Id> { fn id(&self) -> Option<Id> {
Some(self.id.clone()) Some(self.id.clone())
} }
#[inline]
fn set_id(&mut self, id: Id) { fn set_id(&mut self, id: Id) {
self.id = id; self.id = id;
} }
fn drag_destinations( fn drag_destinations(
&self, &self,
state: &Tree, _state: &Tree,
layout: Layout<'_>, layout: Layout<'_>,
renderer: &crate::Renderer, _renderer: &crate::Renderer,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) { ) {
if let Some(input) = layout.children().last() { if let Some(input) = layout.children().last() {
@ -1251,18 +1307,21 @@ pub fn update<'a, Message: Clone + 'static>(
id: Option<Id>, id: Option<Id>,
event: Event, event: Event,
text_layout: Layout<'_>, text_layout: Layout<'_>,
trailing_icon_layout: Option<Layout<'_>>, edit_button_layout: Option<Layout<'_>>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
clipboard: &mut dyn Clipboard, clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
value: &mut Value, value: &mut Value,
size: f32, size: f32,
font: <crate::Renderer as iced_core::text::Renderer>::Font, font: <crate::Renderer as iced_core::text::Renderer>::Font,
is_editable_variant: bool,
is_secure: bool, is_secure: bool,
is_editable: bool, on_focus: Option<&Message>,
on_unfocus: Option<&Message>,
on_input: Option<&dyn Fn(String) -> Message>, on_input: Option<&dyn Fn(String) -> Message>,
on_paste: Option<&dyn Fn(String) -> Message>, on_paste: Option<&dyn Fn(String) -> Message>,
on_submit: Option<&dyn Fn(String) -> Message>, on_submit: Option<&dyn Fn(String) -> Message>,
on_tab: Option<&Message>,
on_toggle_edit: Option<&dyn Fn(bool) -> Message>, on_toggle_edit: Option<&dyn Fn(bool) -> Message>,
state: impl FnOnce() -> &'a mut State, state: impl FnOnce() -> &'a mut State,
#[allow(unused_variables)] on_start_dnd_source: Option<&dyn Fn(State) -> Message>, #[allow(unused_variables)] on_start_dnd_source: Option<&dyn Fn(State) -> Message>,
@ -1285,9 +1344,15 @@ pub fn update<'a, Message: Clone + 'static>(
// NOTE: Clicks must be captured to prevent mouse areas behind them handling the same clicks. // NOTE: Clicks must be captured to prevent mouse areas behind them handling the same clicks.
/// Mark a branch as cold
#[inline]
#[cold]
fn cold() {}
match event { match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => { | Event::Touch(touch::Event::FingerPressed { .. }) => {
cold();
let state = state(); let state = state();
let click_position = if on_input.is_some() || manage_value { let click_position = if on_input.is_some() || manage_value {
@ -1296,18 +1361,30 @@ pub fn update<'a, Message: Clone + 'static>(
None None
}; };
if click_position.is_some() {
state.is_focused = state.is_focused.or_else(|| {
let now = Instant::now();
LAST_FOCUS_UPDATE.with(|x| x.set(now));
Some(Focus {
updated_at: now,
now,
})
});
}
if let Some(cursor_position) = click_position { if let Some(cursor_position) = click_position {
// Check if the edit button was clicked.
if state.dragging_state == None
&& edit_button_layout.map_or(false, |l| cursor.is_over(l.bounds()))
{
if is_editable_variant {
state.is_read_only = !state.is_read_only;
state.move_cursor_to_end();
if let Some(on_toggle_edit) = on_toggle_edit {
shell.publish(on_toggle_edit(!state.is_read_only));
}
let now = Instant::now();
LAST_FOCUS_UPDATE.with(|x| x.set(now));
state.is_focused = Some(Focus {
updated_at: now,
now,
});
}
return event::Status::Captured;
}
let target = cursor_position.x - text_layout.bounds().x; let target = cursor_position.x - text_layout.bounds().x;
let click = let click =
@ -1324,7 +1401,7 @@ pub fn update<'a, Message: Clone + 'static>(
// single click that is on top of the selected text // single click that is on top of the selected text
// is the click on selected text? // is the click on selected text?
if manage_value || on_input.is_some() { if on_input.is_some() || manage_value {
let left = start.min(end); let left = start.min(end);
let right = end.max(start); let right = end.max(start);
@ -1393,40 +1470,14 @@ pub fn update<'a, Message: Clone + 'static>(
update_cache(state, &unsecured_value); update_cache(state, &unsecured_value);
} else { } else {
update_cache(state, value); update_cache(state, value);
// existing logic for setting the selection state.setting_selection(value, text_layout.bounds(), target);
let position = if target > 0.0 {
find_cursor_position(text_layout.bounds(), value, state, target)
} else {
None
};
state.cursor.move_to(position.unwrap_or(0));
state.dragging_state = Some(DraggingState::Selection);
} }
} else { } else {
// existing logic for setting the selection state.setting_selection(value, text_layout.bounds(), target);
let position = if target > 0.0 {
update_cache(state, value);
find_cursor_position(text_layout.bounds(), value, state, target)
} else {
None
};
state.cursor.move_to(position.unwrap_or(0));
state.dragging_state = Some(DraggingState::Selection);
} }
} }
(None, click::Kind::Single, _) => { (None, click::Kind::Single, _) => {
// existing logic for setting the selection state.setting_selection(value, text_layout.bounds(), target);
let position = if target > 0.0 {
update_cache(state, value);
find_cursor_position(text_layout.bounds(), value, state, target)
} else {
None
};
state.cursor.move_to(position.unwrap_or(0));
state.dragging_state = Some(DraggingState::Selection);
} }
(None | Some(DraggingState::Selection), click::Kind::Double, _) => { (None | Some(DraggingState::Selection), click::Kind::Double, _) => {
update_cache(state, value); update_cache(state, value);
@ -1455,93 +1506,41 @@ pub fn update<'a, Message: Clone + 'static>(
} }
} }
// Enable write mode when an editable input label is clicked // Focus on click of the text input, and ensure that the input is writable.
if is_editable if state.is_focused.is_none()
&& state.is_read_only
&& matches!(state.dragging_state, None | Some(DraggingState::Selection)) && matches!(state.dragging_state, None | Some(DraggingState::Selection))
{ {
state.is_read_only = false; if let Some(on_focus) = on_focus {
shell.publish(on_focus.clone());
}
if state.is_read_only {
state.is_read_only = false;
if let Some(on_toggle_edit) = on_toggle_edit {
let message = (on_toggle_edit)(true);
shell.publish(message);
}
}
let now = Instant::now(); let now = Instant::now();
LAST_FOCUS_UPDATE.with(|x| x.set(now)); LAST_FOCUS_UPDATE.with(|x| x.set(now));
state.is_focused = Some(Focus { state.is_focused = Some(Focus {
updated_at: now, updated_at: now,
now, now,
}); });
if let Some(on_toggle_edit) = on_toggle_edit {
let message = (on_toggle_edit)(!state.is_read_only);
shell.publish(message);
}
} }
state.last_click = Some(click); state.last_click = Some(click);
return event::Status::Captured; return event::Status::Captured;
} } else {
state.unfocus();
let mut is_trailing_clicked = false;
if is_editable {
if let Some(trailing_layout) = trailing_icon_layout {
is_trailing_clicked = cursor.is_over(trailing_layout.bounds());
if is_trailing_clicked {
if on_toggle_edit.is_some() {
let Some(pos) = cursor.position() else {
return event::Status::Ignored;
};
let click =
mouse::Click::new(pos, mouse::Button::Left, state.last_click);
match (
&state.dragging_state,
click.kind(),
state.cursor().state(value),
) {
(None, click::Kind::Single, _) => {
state.is_read_only = !state.is_read_only;
if let Some(on_toggle_edit) = on_toggle_edit {
let message = (on_toggle_edit)(!state.is_read_only);
shell.publish(message);
let now = Instant::now();
LAST_FOCUS_UPDATE.with(|x| x.set(now));
state.is_focused = Some(Focus {
updated_at: now,
now,
});
state.move_cursor_to_end();
}
}
_ => {
state.dragging_state = None;
}
}
}
return event::Status::Captured;
}
}
}
// Condition met when mouse click is completely outside of the widget.
if !is_trailing_clicked && click_position.is_none() {
state.is_focused = None;
state.dragging_state = None;
state.is_pasting = None;
state.keyboard_modifiers = keyboard::Modifiers::default();
state.is_read_only = true;
// Ensure clicks outside emit the toggle edit message.
if let Some(on_toggle_edit) = on_toggle_edit {
let message = (on_toggle_edit)(false);
shell.publish(message);
}
} }
} }
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => { | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => {
cold();
let state = state(); let state = state();
state.dragging_state = None; state.dragging_state = None;
@ -1573,14 +1572,10 @@ pub fn update<'a, Message: Clone + 'static>(
let state = state(); let state = state();
if let Some(focus) = &mut state.is_focused { if let Some(focus) = &mut state.is_focused {
if !manage_value && on_input.is_none() { if state.is_read_only || (!manage_value && on_input.is_none()) {
return event::Status::Ignored; return event::Status::Ignored;
}; };
if state.is_read_only {
return event::Status::Ignored;
}
let modifiers = state.keyboard_modifiers; let modifiers = state.keyboard_modifiers;
focus.updated_at = Instant::now(); focus.updated_at = Instant::now();
LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
@ -1750,6 +1745,7 @@ pub fn update<'a, Message: Clone + 'static>(
let contents = editor.contents(); let contents = editor.contents();
let unsecured_value = Value::new(&contents); let unsecured_value = Value::new(&contents);
state.tracked_value = unsecured_value.clone(); state.tracked_value = unsecured_value.clone();
if let Some(on_input) = on_input { if let Some(on_input) = on_input {
let message = if let Some(paste) = &on_paste { let message = if let Some(paste) = &on_paste {
(paste)(contents) (paste)(contents)
@ -1759,6 +1755,7 @@ pub fn update<'a, Message: Clone + 'static>(
shell.publish(message); shell.publish(message);
} }
state.is_pasting = Some(content); state.is_pasting = Some(content);
let value = if is_secure { let value = if is_secure {
@ -1775,17 +1772,34 @@ pub fn update<'a, Message: Clone + 'static>(
state.cursor.select_all(value); state.cursor.select_all(value);
} }
keyboard::Key::Named(keyboard::key::Named::Escape) => { keyboard::Key::Named(keyboard::key::Named::Escape) => {
state.is_focused = None; state.unfocus();
state.dragging_state = None; state.is_read_only = true;
state.is_pasting = None;
state.keyboard_modifiers = keyboard::Modifiers::default(); if let Some(on_unfocus) = on_unfocus {
shell.publish(on_unfocus.clone());
}
}
keyboard::Key::Named(keyboard::key::Named::Tab) => {
if let Some(on_tab) = on_tab {
// Allow the application to decide how the event is handled.
// This could be to connect the text input to another text input.
// Or to connect the text input to a button.
shell.publish(on_tab.clone());
} else {
state.unfocus();
state.is_read_only = true;
if let Some(on_unfocus) = on_unfocus {
shell.publish(on_unfocus.clone());
}
return event::Status::Ignored;
};
} }
keyboard::Key::Named( keyboard::Key::Named(
keyboard::key::Named::ArrowUp keyboard::key::Named::ArrowUp | keyboard::key::Named::ArrowDown,
| keyboard::key::Named::ArrowDown
| keyboard::key::Named::Tab,
) => { ) => {
return event::Status::Ignored; return event::Status::Ignored;
} }
@ -1819,8 +1833,6 @@ pub fn update<'a, Message: Clone + 'static>(
unsecured_value unsecured_value
}; };
update_cache(state, &value); update_cache(state, &value);
return event::Status::Captured;
} }
} }
_ => {} _ => {}
@ -1869,8 +1881,10 @@ pub fn update<'a, Message: Clone + 'static>(
} }
#[cfg(feature = "wayland")] #[cfg(feature = "wayland")]
Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => { Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => {
cold();
let state = state(); let state = state();
if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) { if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) {
// TODO: restore value in text input
state.dragging_state = None; state.dragging_state = None;
return event::Status::Captured; return event::Status::Captured;
} }
@ -1885,6 +1899,7 @@ pub fn update<'a, Message: Clone + 'static>(
surface, surface,
}, },
)) if rectangle == Some(dnd_id) => { )) if rectangle == Some(dnd_id) => {
cold();
let state = state(); let state = state();
let is_clicked = text_layout.bounds().contains(Point { let is_clicked = text_layout.bounds().contains(Point {
x: x as f32, x: x as f32,
@ -1934,6 +1949,7 @@ pub fn update<'a, Message: Clone + 'static>(
} }
#[cfg(feature = "wayland")] #[cfg(feature = "wayland")]
Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if rectangle == Some(dnd_id) => { Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if rectangle == Some(dnd_id) => {
cold();
let state = state(); let state = state();
if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() { if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() {
let Some(mime_type) = SUPPORTED_TEXT_MIME_TYPES let Some(mime_type) = SUPPORTED_TEXT_MIME_TYPES
@ -1953,6 +1969,7 @@ pub fn update<'a, Message: Clone + 'static>(
rectangle, rectangle,
OfferEvent::Leave | OfferEvent::LeaveDestination, OfferEvent::Leave | OfferEvent::LeaveDestination,
)) if rectangle == Some(dnd_id) => { )) if rectangle == Some(dnd_id) => {
cold();
let state = state(); let state = state();
// ASHLEY TODO we should be able to reset but for now we don't if we are handling a // ASHLEY TODO we should be able to reset but for now we don't if we are handling a
// drop // drop
@ -1968,6 +1985,7 @@ pub fn update<'a, Message: Clone + 'static>(
Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type })) Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type }))
if rectangle == Some(dnd_id) => if rectangle == Some(dnd_id) =>
{ {
cold();
let state = state(); let state = state();
if let DndOfferState::Dropped = state.dnd_offer.clone() { if let DndOfferState::Dropped = state.dnd_offer.clone() {
state.dnd_offer = DndOfferState::None; state.dnd_offer = DndOfferState::None;
@ -2560,18 +2578,21 @@ impl State {
} }
/// Returns whether the [`TextInput`] is currently focused or not. /// Returns whether the [`TextInput`] is currently focused or not.
#[inline]
#[must_use] #[must_use]
pub fn is_focused(&self) -> bool { pub fn is_focused(&self) -> bool {
self.is_focused.is_some() self.is_focused.is_some()
} }
/// Returns the [`Cursor`] of the [`TextInput`]. /// Returns the [`Cursor`] of the [`TextInput`].
#[inline]
#[must_use] #[must_use]
pub fn cursor(&self) -> Cursor { pub fn cursor(&self) -> Cursor {
self.cursor self.cursor
} }
/// Focuses the [`TextInput`]. /// Focuses the [`TextInput`].
#[cold]
pub fn focus(&mut self) { pub fn focus(&mut self) {
let now = Instant::now(); let now = Instant::now();
LAST_FOCUS_UPDATE.with(|x| x.set(now)); LAST_FOCUS_UPDATE.with(|x| x.set(now));
@ -2589,63 +2610,90 @@ impl State {
} }
/// Unfocuses the [`TextInput`]. /// Unfocuses the [`TextInput`].
pub fn unfocus(&mut self) { #[cold]
pub(super) fn unfocus(&mut self) {
self.is_focused = None; self.is_focused = None;
self.dragging_state = None;
self.is_pasting = None;
self.keyboard_modifiers = keyboard::Modifiers::default();
} }
/// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text. /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text.
#[inline]
pub fn move_cursor_to_front(&mut self) { pub fn move_cursor_to_front(&mut self) {
self.cursor.move_to(0); self.cursor.move_to(0);
} }
/// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text. /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text.
#[inline]
pub fn move_cursor_to_end(&mut self) { pub fn move_cursor_to_end(&mut self) {
self.cursor.move_to(usize::MAX); self.cursor.move_to(usize::MAX);
} }
/// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location. /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location.
#[inline]
pub fn move_cursor_to(&mut self, position: usize) { pub fn move_cursor_to(&mut self, position: usize) {
self.cursor.move_to(position); self.cursor.move_to(position);
} }
/// Selects all the content of the [`TextInput`]. /// Selects all the content of the [`TextInput`].
#[inline]
pub fn select_all(&mut self) { pub fn select_all(&mut self) {
self.cursor.select_range(0, usize::MAX); self.cursor.select_range(0, usize::MAX);
} }
pub(super) fn setting_selection(&mut self, value: &Value, bounds: Rectangle<f32>, target: f32) {
let position = if target > 0.0 {
find_cursor_position(bounds, value, self, target)
} else {
None
};
self.cursor.move_to(position.unwrap_or(0));
self.dragging_state = Some(DraggingState::Selection);
}
} }
impl operation::Focusable for State { impl operation::Focusable for State {
#[inline]
fn is_focused(&self) -> bool { fn is_focused(&self) -> bool {
Self::is_focused(self) Self::is_focused(self)
} }
#[inline]
fn focus(&mut self) { fn focus(&mut self) {
Self::focus(self); Self::focus(self);
} }
#[inline]
fn unfocus(&mut self) { fn unfocus(&mut self) {
Self::unfocus(self); Self::unfocus(self);
} }
} }
impl operation::TextInput for State { impl operation::TextInput for State {
#[inline]
fn move_cursor_to_front(&mut self) { fn move_cursor_to_front(&mut self) {
Self::move_cursor_to_front(self); Self::move_cursor_to_front(self);
} }
#[inline]
fn move_cursor_to_end(&mut self) { fn move_cursor_to_end(&mut self) {
Self::move_cursor_to_end(self); Self::move_cursor_to_end(self);
} }
#[inline]
fn move_cursor_to(&mut self, position: usize) { fn move_cursor_to(&mut self, position: usize) {
Self::move_cursor_to(self, position); Self::move_cursor_to(self, position);
} }
#[inline]
fn select_all(&mut self) { fn select_all(&mut self) {
Self::select_all(self); Self::select_all(self);
} }
} }
#[inline(never)]
fn measure_cursor_and_scroll_offset( fn measure_cursor_and_scroll_offset(
paragraph: &impl text::Paragraph, paragraph: &impl text::Paragraph,
text_bounds: Rectangle, text_bounds: Rectangle,
@ -2662,6 +2710,7 @@ fn measure_cursor_and_scroll_offset(
/// Computes the position of the text cursor at the given X coordinate of /// Computes the position of the text cursor at the given X coordinate of
/// a [`TextInput`]. /// a [`TextInput`].
#[inline(never)]
fn find_cursor_position( fn find_cursor_position(
text_bounds: Rectangle, text_bounds: Rectangle,
value: &Value, value: &Value,
@ -2686,6 +2735,7 @@ fn find_cursor_position(
) )
} }
#[inline(never)]
fn replace_paragraph( fn replace_paragraph(
state: &mut State, state: &mut State,
layout: Layout<'_>, layout: Layout<'_>,
@ -2715,6 +2765,7 @@ const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
mod platform { mod platform {
use iced_core::keyboard; use iced_core::keyboard;
#[inline]
pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool {
if cfg!(target_os = "macos") { if cfg!(target_os = "macos") {
modifiers.alt() modifiers.alt()
@ -2724,6 +2775,7 @@ mod platform {
} }
} }
#[inline(never)]
fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 { fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 {
if state.is_focused() { if state.is_focused() {
let cursor = state.cursor(); let cursor = state.cursor();

View file

@ -27,12 +27,14 @@ impl Value {
/// ///
/// A [`Value`] is empty when it contains no graphemes. /// A [`Value`] is empty when it contains no graphemes.
#[must_use] #[must_use]
#[inline]
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.len() == 0 self.len() == 0
} }
/// Returns the total amount of graphemes in the [`Value`]. /// Returns the total amount of graphemes in the [`Value`].
#[must_use] #[must_use]
#[inline]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.graphemes.len() self.graphemes.len()
} }
@ -75,6 +77,7 @@ impl Value {
/// Returns a new [`Value`] containing the graphemes from `start` until the /// Returns a new [`Value`] containing the graphemes from `start` until the
/// given `end`. /// given `end`.
#[must_use] #[must_use]
#[inline]
pub fn select(&self, start: usize, end: usize) -> Self { pub fn select(&self, start: usize, end: usize) -> Self {
let graphemes = self.graphemes[start.min(self.len())..end.min(self.len())].to_vec(); let graphemes = self.graphemes[start.min(self.len())..end.min(self.len())].to_vec();
@ -84,6 +87,7 @@ impl Value {
/// Returns a new [`Value`] containing the graphemes until the given /// Returns a new [`Value`] containing the graphemes until the given
/// `index`. /// `index`.
#[must_use] #[must_use]
#[inline]
pub fn until(&self, index: usize) -> Self { pub fn until(&self, index: usize) -> Self {
let graphemes = self.graphemes[..index.min(self.len())].to_vec(); let graphemes = self.graphemes[..index.min(self.len())].to_vec();
@ -91,6 +95,7 @@ impl Value {
} }
/// Inserts a new `char` at the given grapheme `index`. /// Inserts a new `char` at the given grapheme `index`.
#[inline]
pub fn insert(&mut self, index: usize, c: char) { pub fn insert(&mut self, index: usize, c: char) {
self.graphemes.insert(index, c.to_string()); self.graphemes.insert(index, c.to_string());
@ -100,6 +105,7 @@ impl Value {
} }
/// Inserts a bunch of graphemes at the given grapheme `index`. /// Inserts a bunch of graphemes at the given grapheme `index`.
#[inline]
pub fn insert_many(&mut self, index: usize, mut value: Value) { pub fn insert_many(&mut self, index: usize, mut value: Value) {
let _ = self let _ = self
.graphemes .graphemes
@ -107,11 +113,13 @@ impl Value {
} }
/// Removes the grapheme at the given `index`. /// Removes the grapheme at the given `index`.
#[inline]
pub fn remove(&mut self, index: usize) { pub fn remove(&mut self, index: usize) {
let _ = self.graphemes.remove(index); let _ = self.graphemes.remove(index);
} }
/// Removes the graphemes from `start` to `end`. /// Removes the graphemes from `start` to `end`.
#[inline]
pub fn remove_many(&mut self, start: usize, end: usize) { pub fn remove_many(&mut self, start: usize, end: usize) {
let _ = self.graphemes.splice(start..end, std::iter::empty()); let _ = self.graphemes.splice(start..end, std::iter::empty());
} }
@ -129,6 +137,7 @@ impl Value {
} }
impl ToString for Value { impl ToString for Value {
#[inline]
fn to_string(&self) -> String { fn to_string(&self) -> String {
self.graphemes.concat() self.graphemes.concat()
} }