Add delay suppport for tooltip widget

This commit is contained in:
Kiëd Llaentenn 2025-05-25 18:30:27 -04:00 • committed by Héctor Ramón Jiménez
parent ac511f5f68
commit 5ec23c1243
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
2 changed files with 102 additions and 30 deletions

View file

@ -1,6 +1,7 @@
use iced::Element; use iced::Element;
use iced::widget::tooltip::Position; use iced::widget::tooltip::Position;
use iced::widget::{button, center, container, tooltip}; use iced::widget::{button, center, checkbox, column, container, tooltip};
use iced::{alignment, time::Duration};
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
iced::run(Tooltip::update, Tooltip::view) iced::run(Tooltip::update, Tooltip::view)
@ -9,11 +10,13 @@ pub fn main() -> iced::Result {
#[derive(Default)] #[derive(Default)]
struct Tooltip { struct Tooltip {
position: Position, position: Position,
is_immediate: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Message { enum Message {
ChangePosition, ChangePosition,
SetImmediate(bool),
} }
impl Tooltip { impl Tooltip {
@ -30,10 +33,17 @@ impl Tooltip {
self.position = position; self.position = position;
} }
Message::SetImmediate(is_immediate) => {
self.is_immediate = is_immediate;
}
} }
} }
fn view(&self) -> Element<'_, Message> { fn view(&self) -> Element<'_, Message> {
let delay =
Duration::from_millis(if self.is_immediate { 0 } else { 2000 });
let tooltip = tooltip( let tooltip = tooltip(
button("Press to change position") button("Press to change position")
.on_press(Message::ChangePosition), .on_press(Message::ChangePosition),
@ -41,9 +51,19 @@ impl Tooltip {
self.position, self.position,
) )
.gap(10) .gap(10)
.delay(delay)
.style(container::rounded_box); .style(container::rounded_box);
center(tooltip).into() let checkbox = checkbox(self.is_immediate)
.label("Show immediately")
.on_toggle(Message::SetImmediate);
center(
column![tooltip, checkbox]
.align_x(alignment::Horizontal::Center)
.spacing(7),
)
.into()
} }
} }

View file

@ -1,5 +1,8 @@
//! Tooltips display a hint of information over some element when hovered. //! Tooltips display a hint of information over some element when hovered.
//! //!
//! By default, the tooltip is only displayed after a short duration. (This
//! delay can be customized.)
//!
//! # Example //! # Example
//! ```no_run //! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } } //! # mod iced { pub mod widget { pub use iced_widget::*; } }
@ -27,6 +30,7 @@ use crate::core::mouse;
use crate::core::overlay; use crate::core::overlay;
use crate::core::renderer; use crate::core::renderer;
use crate::core::text; use crate::core::text;
use crate::core::time::{Duration, Instant};
use crate::core::widget::{self, Widget}; use crate::core::widget::{self, Widget};
use crate::core::window; use crate::core::window;
use crate::core::{ use crate::core::{
@ -34,6 +38,9 @@ use crate::core::{
Shell, Size, Vector, Shell, Size, Vector,
}; };
// Default tooltip delay, 2 seconds. Chosen because it's the default on macOS.
const DEFAULT_TOOLTIP_DELAY: Duration = Duration::from_millis(2000);
/// An element to display a widget over another. /// An element to display a widget over another.
/// ///
/// # Example /// # Example
@ -72,6 +79,7 @@ pub struct Tooltip<
gap: f32, gap: f32,
padding: f32, padding: f32,
snap_within_viewport: bool, snap_within_viewport: bool,
delay: Duration,
class: Theme::Class<'a>, class: Theme::Class<'a>,
} }
@ -98,6 +106,7 @@ where
gap: 0.0, gap: 0.0,
padding: Self::DEFAULT_PADDING, padding: Self::DEFAULT_PADDING,
snap_within_viewport: true, snap_within_viewport: true,
delay: DEFAULT_TOOLTIP_DELAY,
class: Theme::default(), class: Theme::default(),
} }
} }
@ -114,6 +123,13 @@ where
self self
} }
/// Sets the delay before the [`Tooltip`] is shown. Set to 0 milliseconds
/// to be shown immediately.
pub fn delay(mut self, delay: Duration) -> Self {
self.delay = delay;
self
}
/// Sets whether the [`Tooltip`] is snapped within the viewport. /// Sets whether the [`Tooltip`] is snapped within the viewport.
pub fn snap_within_viewport(mut self, snap: bool) -> Self { pub fn snap_within_viewport(mut self, snap: bool) -> Self {
self.snap_within_viewport = snap; self.snap_within_viewport = snap;
@ -206,23 +222,54 @@ where
| Event::Window(window::Event::RedrawRequested(_)) = event | Event::Window(window::Event::RedrawRequested(_)) = event
{ {
let state = tree.state.downcast_mut::<State>(); let state = tree.state.downcast_mut::<State>();
let previous_state = *state; let now = Instant::now();
let was_idle = *state == State::Idle; let cursor_position = cursor.position_over(layout.bounds());
*state = cursor match (&state, cursor_position) {
.position_over(layout.bounds()) // Tooltip was idle, but is now hovered.
.map(|cursor_position| State::Hovered { cursor_position }) (State::Idle, Some(cursor_position)) => {
.unwrap_or_default(); shell.invalidate_layout();
shell.request_redraw_at(now + self.delay);
*state = State::Hovered {
cursor_position,
at: now,
};
}
let is_idle = *state == State::Idle; // Tooltip was active and isn't hovered anymore.
(State::Hovered { .. }, None) => {
shell.invalidate_layout();
shell.request_redraw();
*state = State::Idle;
}
if was_idle != is_idle { // Tooltip is active, but not for long enough.
shell.invalidate_layout(); (State::Hovered { at, .. }, Some(cursor_position))
shell.request_redraw(); if at.elapsed() < self.delay =>
} else if self.position == Position::FollowCursor {
&& *state != previous_state let when = now + self.delay - at.elapsed();
{ shell.request_redraw_at(when);
shell.request_redraw(); *state = State::Hovered {
at: *at,
cursor_position,
};
}
// Tooltip has been active long enough, and is following the cursor
// (thus requiring a redraw)
(State::Hovered { at, .. }, Some(cursor_position))
if self.position == Position::FollowCursor =>
{
shell.request_redraw();
*state = State::Hovered {
at: *at,
cursor_position,
};
}
// No change in state.
(State::Hovered { .. }, Some(_)) => (),
(State::Idle, None) => (),
} }
} }
@ -296,21 +343,25 @@ where
translation, translation,
); );
let tooltip = if let State::Hovered { cursor_position } = *state { let tooltip = match *state {
Some(overlay::Element::new(Box::new(Overlay { State::Hovered {
position: layout.position() + translation,
tooltip: &mut self.tooltip,
tree: children.next().unwrap(),
cursor_position, cursor_position,
content_bounds: layout.bounds(), at,
snap_within_viewport: self.snap_within_viewport, } if at.elapsed() > self.delay => {
positioning: self.position, Some(overlay::Element::new(Box::new(Overlay {
gap: self.gap, position: layout.position() + translation,
padding: self.padding, tooltip: &mut self.tooltip,
class: &self.class, tree: children.next().unwrap(),
}))) cursor_position,
} else { content_bounds: layout.bounds(),
None snap_within_viewport: self.snap_within_viewport,
positioning: self.position,
gap: self.gap,
padding: self.padding,
class: &self.class,
})))
}
_ => None,
}; };
if content.is_some() || tooltip.is_some() { if content.is_some() || tooltip.is_some() {
@ -362,6 +413,7 @@ enum State {
Idle, Idle,
Hovered { Hovered {
cursor_position: Point, cursor_position: Point,
at: Instant,
}, },
} }