feat: Tooltips and Better Surface Management

This commit is contained in:
Ashley Wulber 2025-03-14 11:56:21 -04:00 committed by GitHub
parent c7edd37b03
commit 337b80d4ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
90 changed files with 3651 additions and 977 deletions

View file

@ -18,7 +18,6 @@ use super::style::StyleSheet;
pub use super::value::Value;
use apply::Apply;
use cosmic_theme::Theme;
use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent};
use iced::clipboard::mime::AsMimeTypes;
use iced::Limits;
@ -40,10 +39,6 @@ use iced_core::{
Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size,
Vector, Widget,
};
#[cfg(feature = "wayland")]
use iced_renderer::core::event::{wayland, PlatformSpecific};
#[cfg(feature = "wayland")]
use iced_runtime::platform_specific;
use iced_runtime::{task, Action, Task};
thread_local! {
@ -200,7 +195,7 @@ pub struct TextInput<'a, Message> {
error: Option<Cow<'a, str>>,
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_submit: 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>>,
trailing_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>,
@ -211,6 +206,8 @@ pub struct TextInput<'a, Message> {
line_height: text::LineHeight,
helper_line_height: text::LineHeight,
always_active: bool,
/// The text input tracks and manages the input value in its state.
manage_value: bool,
}
impl<'a, Message> TextInput<'a, Message>
@ -255,6 +252,7 @@ where
label: None,
helper_text: None,
always_active: false,
manage_value: false,
}
}
@ -340,14 +338,24 @@ where
/// Sets the message that should be produced when the [`TextInput`] is
/// focused and the enter key is pressed.
pub fn on_submit(self, message: Message) -> Self {
self.on_submit_maybe(Some(message))
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(mut self, message: Option<Message>) -> Self {
self.on_submit = message;
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
}
@ -416,6 +424,12 @@ where
self
}
/// 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;
self
}
/// Draws the [`TextInput`] with the given [`Renderer`], overriding its
/// [`Value`] if provided.
///
@ -507,7 +521,7 @@ where
}
}
impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for TextInput<'a, Message>
impl<Message> Widget<Message, crate::Theme, crate::Renderer> for TextInput<'_, Message>
where
Message: Clone + 'static,
{
@ -526,9 +540,14 @@ 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() {
self.value = state.tracked_value.clone();
// std::mem::swap(&mut state.tracked_value, &mut self.value);
}
// Unfocus text input if it becomes disabled
if self.on_input.is_none() {
if self.on_input.is_none() && !self.manage_value {
state.last_click = None;
state.is_focused = None;
state.is_pasting = None;
@ -581,13 +600,10 @@ where
// if the previous state was at the end of the text, keep it there
let old_value = Value::new(&old_value);
if state.is_focused.is_some() {
match state.cursor.state(&old_value) {
cursor::State::Index(index) => {
if index == old_value.len() {
state.cursor.move_to(self.value.len());
}
if let cursor::State::Index(index) = state.cursor.state(&old_value) {
if index == old_value.len() {
state.cursor.move_to(self.value.len());
}
_ => {}
};
}
@ -597,6 +613,11 @@ where
state.is_focused = None;
}
// Stop pasting if input becomes disabled
if !self.manage_value && self.on_input.is_none() {
state.is_pasting = None;
}
let mut children: Vec<_> = self
.leading_icon
.iter_mut()
@ -779,7 +800,7 @@ where
}
}
if tree.children.len() > 0 {
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))
@ -824,13 +845,14 @@ where
self.is_editable,
self.on_input.as_deref(),
self.on_paste.as_deref(),
&self.on_submit,
self.on_submit.as_deref(),
self.on_toggle_edit.as_deref(),
|| tree.state.downcast_mut::<State>(),
self.on_create_dnd_source.as_deref(),
dnd_id,
line_height,
layout,
self.manage_value,
)
}
@ -856,7 +878,7 @@ where
&self.placeholder,
self.size,
self.font,
self.on_input.is_none(),
self.on_input.is_none() && !self.manage_value,
self.is_secure,
self.leading_icon.as_ref(),
self.trailing_icon.as_ref(),
@ -925,7 +947,11 @@ where
}
let mut children = layout.children();
let layout = children.next().unwrap();
mouse_interaction(layout, cursor_position, self.on_input.is_none())
mouse_interaction(
layout,
cursor_position,
self.on_input.is_none() && !self.manage_value,
)
}
fn id(&self) -> Option<Id> {
@ -1236,13 +1262,14 @@ pub fn update<'a, Message: 'static>(
is_editable: bool,
on_input: Option<&dyn Fn(String) -> Message>,
on_paste: Option<&dyn Fn(String) -> Message>,
on_submit: &Option<Message>,
on_submit: Option<&dyn Fn(String) -> 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>,
#[allow(unused_variables)] dnd_id: u128,
line_height: text::LineHeight,
layout: Layout<'_>,
manage_value: bool,
) -> event::Status
where
Message: Clone,
@ -1264,7 +1291,7 @@ where
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let state = state();
let click_position = if on_input.is_some() {
let click_position = if on_input.is_some() || manage_value {
cursor.position_over(layout.bounds())
} else {
None
@ -1299,7 +1326,7 @@ where
// single click that is on top of the selected text
// is the click on selected text?
if let Some(on_input) = on_input {
if manage_value || on_input.is_some() {
let left = start.min(end);
let right = end.max(start);
@ -1339,8 +1366,11 @@ where
let contents = editor.contents();
let unsecured_value = Value::new(&contents);
let message = (on_input)(contents);
shell.publish(message);
state.tracked_value = unsecured_value.clone();
if let Some(on_input) = on_input {
let message = (on_input)(contents);
shell.publish(message);
}
if let Some(on_start_dnd) = on_start_dnd_source {
shell.publish(on_start_dnd(state.clone()));
}
@ -1349,7 +1379,7 @@ where
iced_core::clipboard::start_dnd(
clipboard,
false,
id.map(|id| iced_core::clipboard::DndSource::Widget(id)),
id.map(iced_core::clipboard::DndSource::Widget),
Some(iced_core::clipboard::IconSurface::new(
Element::from(
TextInput::<'static, ()>::new("", input_text.clone())
@ -1531,7 +1561,7 @@ where
let state = state();
if let Some(focus) = &mut state.is_focused {
let Some(on_input) = on_input else {
if !manage_value && on_input.is_none() {
return event::Status::Ignored;
};
@ -1545,8 +1575,8 @@ where
match key {
keyboard::Key::Named(keyboard::key::Named::Enter) => {
if let Some(on_submit) = on_submit.clone() {
shell.publish(on_submit);
if let Some(on_submit) = on_submit {
shell.publish((on_submit)(unsecured_value.to_string()));
}
}
keyboard::Key::Named(keyboard::key::Named::Backspace) => {
@ -1566,9 +1596,11 @@ where
let contents = editor.contents();
let unsecured_value = Value::new(&contents);
let message = (on_input)(editor.contents());
shell.publish(message);
state.tracked_value = unsecured_value.clone();
if let Some(on_input) = on_input {
let message = (on_input)(editor.contents());
shell.publish(message);
}
let value = if is_secure {
unsecured_value.secure()
} else {
@ -1592,8 +1624,12 @@ where
editor.delete();
let contents = editor.contents();
let unsecured_value = Value::new(&contents);
let message = (on_input)(contents);
shell.publish(message);
if let Some(on_input) = on_input {
let message = (on_input)(contents);
state.tracked_value = unsecured_value.clone();
shell.publish(message);
}
let value = if is_secure {
unsecured_value.secure()
} else {
@ -1671,10 +1707,12 @@ where
let mut editor = Editor::new(value, &mut state.cursor);
editor.delete();
let message = (on_input)(editor.contents());
shell.publish(message);
let content = editor.contents();
state.tracked_value = Value::new(&content);
if let Some(on_input) = on_input {
let message = (on_input)(content);
shell.publish(message);
}
}
}
keyboard::Key::Character(c)
@ -1699,13 +1737,16 @@ where
let contents = editor.contents();
let unsecured_value = Value::new(&contents);
let message = if let Some(paste) = &on_paste {
(paste)(contents)
} else {
(on_input)(contents)
};
shell.publish(message);
state.tracked_value = unsecured_value.clone();
if let Some(on_input) = on_input {
let message = if let Some(paste) = &on_paste {
(paste)(contents)
} else {
(on_input)(contents)
};
shell.publish(message);
}
state.is_pasting = Some(content);
let value = if is_secure {
@ -1750,8 +1791,11 @@ where
}
let contents = editor.contents();
let unsecured_value = Value::new(&contents);
let message = (on_input)(contents);
shell.publish(message);
state.tracked_value = unsecured_value.clone();
if let Some(on_input) = on_input {
let message = (on_input)(contents);
shell.publish(message);
}
focus.updated_at = Instant::now();
LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
@ -1926,7 +1970,7 @@ where
editor.paste(Value::new(content.as_str()));
let contents = editor.contents();
let unsecured_value = Value::new(&contents);
state.tracked_value = unsecured_value.clone();
if let Some(on_paste) = on_paste.as_ref() {
let message = (on_paste)(contents);
shell.publish(message);
@ -2408,6 +2452,7 @@ pub(crate) struct DndOfferState;
#[derive(Debug, Default, Clone)]
#[must_use]
pub struct State {
pub tracked_value: Value,
pub value: crate::Plain,
pub placeholder: crate::Plain,
pub label: crate::Plain,
@ -2482,6 +2527,7 @@ impl State {
/// Creates a new [`State`], representing a focused [`TextInput`].
pub fn focused(is_secure: bool, is_read_only: bool) -> Self {
Self {
tracked_value: Value::default(),
is_secure,
value: crate::Plain::default(),
placeholder: crate::Plain::default(),

View file

@ -8,7 +8,7 @@ use unicode_segmentation::UnicodeSegmentation;
///
/// [`TextInput`]: crate::widget::TextInput
// TODO: Reduce allocations, cache results (?)
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Value {
graphemes: Vec<String>,
}