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 {
#[inline]
fn default() -> Self {
Self {
state: State::Index(0),
@ -37,6 +38,7 @@ impl Default for Cursor {
impl Cursor {
/// Returns the [`State`] of the [`Cursor`].
#[must_use]
#[inline(never)]
pub fn state(&self, value: &Value) -> State {
match self.state {
State::Index(index) => State::Index(index.min(value.len())),
@ -57,6 +59,7 @@ impl Cursor {
///
/// `start` is guaranteed to be <= than `end`.
#[must_use]
#[inline]
pub fn selection(&self, value: &Value) -> Option<(usize, usize)> {
match self.state(value) {
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) {
self.state = State::Index(position);
}
#[inline]
pub(crate) fn move_right(&mut self, value: &Value) {
self.move_right_by_amount(value, 1);
}
#[inline]
pub(crate) fn move_right_by_words(&mut self, value: &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) {
match self.state(value) {
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) {
match self.state(value) {
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) {
self.move_to(value.previous_start_of_word(self.left(value)));
}
#[inline]
pub(crate) fn select_range(&mut self, start: usize, end: usize) {
if start == end {
self.state = State::Index(start);
self.state = if start == end {
State::Index(start)
} else {
self.state = State::Selection { start, end };
}
State::Selection { start, end }
};
}
#[inline]
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),
@ -111,6 +122,7 @@ impl Cursor {
}
}
#[inline]
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),
@ -121,6 +133,7 @@ impl Cursor {
}
}
#[inline]
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)),
@ -130,6 +143,7 @@ impl Cursor {
}
}
#[inline]
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)),
@ -139,10 +153,12 @@ impl Cursor {
}
}
#[inline]
pub(crate) fn select_all(&mut self, value: &Value) {
self.select_range(0, value.len());
}
#[inline]
pub(crate) fn start(&self, value: &Value) -> usize {
let start = match self.state {
State::Index(index) => index,
@ -152,6 +168,7 @@ impl Cursor {
start.min(value.len())
}
#[inline]
pub(crate) fn end(&self, value: &Value) -> usize {
let end = match self.state {
State::Index(index) => index,
@ -161,6 +178,7 @@ impl Cursor {
end.min(value.len())
}
#[inline]
fn left(&self, value: &Value) -> usize {
match self.state(value) {
State::Index(index) => index,
@ -168,6 +186,7 @@ impl Cursor {
}
}
#[inline]
fn right(&self, value: &Value) -> usize {
match self.state(value) {
State::Index(index) => index,

View file

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

View file

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

View file

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