//! Tooltips display a hint of information over some element when hovered. //! //! # Example //! ```no_run //! # mod iced { pub mod widget { pub use iced_widget::*; } } //! # pub type State = (); //! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; //! use iced::widget::{container, tooltip}; //! //! enum Message { //! // ... //! } //! //! fn view(_state: &State) -> Element<'_, Message> { //! tooltip( //! "Hover me to display the tooltip!", //! container("This is the tooltip contents!") //! .padding(10) //! .style(container::rounded_box), //! tooltip::Position::Bottom, //! ).into() //! } //! ``` use crate::container; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::text; use crate::core::widget::{self, Widget}; use crate::core::{ Clipboard, Element, Event, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Vector, }; /// An element to display a widget over another. /// /// # Example /// ```no_run /// # mod iced { pub mod widget { pub use iced_widget::*; } } /// # pub type State = (); /// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// use iced::widget::{container, tooltip}; /// /// enum Message { /// // ... /// } /// /// fn view(_state: &State) -> Element<'_, Message> { /// tooltip( /// "Hover me to display the tooltip!", /// container("This is the tooltip contents!") /// .padding(10) /// .style(container::rounded_box), /// tooltip::Position::Bottom, /// ).into() /// } /// ``` #[allow(missing_debug_implementations)] pub struct Tooltip< 'a, Message, Theme = crate::Theme, Renderer = crate::Renderer, > where Theme: container::Catalog, Renderer: text::Renderer, { content: Element<'a, Message, Theme, Renderer>, tooltip: Element<'a, Message, Theme, Renderer>, position: Position, gap: f32, padding: f32, snap_within_viewport: bool, class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Tooltip<'a, Message, Theme, Renderer> where Theme: container::Catalog, Renderer: text::Renderer, { /// The default padding of a [`Tooltip`] drawn by this renderer. const DEFAULT_PADDING: f32 = 5.0; /// Creates a new [`Tooltip`]. /// /// [`Tooltip`]: struct.Tooltip.html pub fn new( content: impl Into>, tooltip: impl Into>, position: Position, ) -> Self { Tooltip { content: content.into(), tooltip: tooltip.into(), position, gap: 0.0, padding: Self::DEFAULT_PADDING, snap_within_viewport: true, class: Theme::default(), } } /// Sets the gap between the content and its [`Tooltip`]. pub fn gap(mut self, gap: impl Into) -> Self { self.gap = gap.into().0; self } /// Sets the padding of the [`Tooltip`]. pub fn padding(mut self, padding: impl Into) -> Self { self.padding = padding.into().0; self } /// Sets whether the [`Tooltip`] is snapped within the viewport. pub fn snap_within_viewport(mut self, snap: bool) -> Self { self.snap_within_viewport = snap; self } /// Sets the style of the [`Tooltip`]. #[must_use] pub fn style( mut self, style: impl Fn(&Theme) -> container::Style + 'a, ) -> Self where Theme::Class<'a>: From>, { self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into(); self } /// Sets the style class of the [`Tooltip`]. #[cfg(feature = "advanced")] #[must_use] pub fn class(mut self, class: impl Into>) -> Self { self.class = class.into(); self } } impl Widget for Tooltip<'_, Message, Theme, Renderer> where Theme: container::Catalog, Renderer: text::Renderer, { fn children(&self) -> Vec { vec![ widget::Tree::new(&self.content), widget::Tree::new(&self.tooltip), ] } fn diff(&self, tree: &mut widget::Tree) { tree.diff_children(&[ self.content.as_widget(), self.tooltip.as_widget(), ]); } fn state(&self) -> widget::tree::State { widget::tree::State::new(State::default()) } fn tag(&self) -> widget::tree::Tag { widget::tree::Tag::of::() } fn size(&self) -> Size { self.content.as_widget().size() } fn size_hint(&self) -> Size { self.content.as_widget().size_hint() } fn layout( &self, tree: &mut widget::Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.content .as_widget() .layout(&mut tree.children[0], renderer, limits) } fn update( &mut self, tree: &mut widget::Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { let state = tree.state.downcast_mut::(); let was_idle = *state == State::Idle; *state = cursor .position_over(layout.bounds()) .map(|cursor_position| State::Hovered { cursor_position }) .unwrap_or_default(); let is_idle = *state == State::Idle; if was_idle != is_idle { shell.invalidate_layout(); shell.request_redraw(); } else if !is_idle && self.position == Position::FollowCursor { shell.request_redraw(); } self.content.as_widget_mut().update( &mut tree.children[0], event, layout, cursor, renderer, clipboard, shell, viewport, ); } fn mouse_interaction( &self, tree: &widget::Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( &tree.children[0], layout, cursor, viewport, renderer, ) } fn draw( &self, tree: &widget::Tree, renderer: &mut Renderer, theme: &Theme, inherited_style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { self.content.as_widget().draw( &tree.children[0], renderer, theme, inherited_style, layout, cursor, viewport, ); } fn overlay<'b>( &'b mut self, tree: &'b mut widget::Tree, layout: Layout<'_>, renderer: &Renderer, translation: Vector, ) -> Option> { let state = tree.state.downcast_ref::(); let mut children = tree.children.iter_mut(); let content = self.content.as_widget_mut().overlay( children.next().unwrap(), layout, renderer, translation, ); let tooltip = if let State::Hovered { cursor_position } = *state { Some(overlay::Element::new(Box::new(Overlay { position: layout.position() + translation, tooltip: &self.tooltip, state: children.next().unwrap(), cursor_position, content_bounds: layout.bounds(), snap_within_viewport: self.snap_within_viewport, positioning: self.position, gap: self.gap, padding: self.padding, class: &self.class, }))) } else { None }; if content.is_some() || tooltip.is_some() { Some( overlay::Group::with_children( content.into_iter().chain(tooltip).collect(), ) .overlay(), ) } else { None } } } impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where Message: 'a, Theme: container::Catalog + 'a, Renderer: text::Renderer + 'a, { fn from( tooltip: Tooltip<'a, Message, Theme, Renderer>, ) -> Element<'a, Message, Theme, Renderer> { Element::new(tooltip) } } /// The position of the tooltip. Defaults to following the cursor. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Position { /// The tooltip will appear on the top of the widget. #[default] Top, /// The tooltip will appear on the bottom of the widget. Bottom, /// The tooltip will appear on the left of the widget. Left, /// The tooltip will appear on the right of the widget. Right, /// The tooltip will follow the cursor. FollowCursor, } #[derive(Debug, Clone, Copy, PartialEq, Default)] enum State { #[default] Idle, Hovered { cursor_position: Point, }, } struct Overlay<'a, 'b, Message, Theme, Renderer> where Theme: container::Catalog, Renderer: text::Renderer, { position: Point, tooltip: &'b Element<'a, Message, Theme, Renderer>, state: &'b mut widget::Tree, cursor_position: Point, content_bounds: Rectangle, snap_within_viewport: bool, positioning: Position, gap: f32, padding: f32, class: &'b Theme::Class<'a>, } impl overlay::Overlay for Overlay<'_, '_, Message, Theme, Renderer> where Theme: container::Catalog, Renderer: text::Renderer, { fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { let viewport = Rectangle::with_size(bounds); let tooltip_layout = self.tooltip.as_widget().layout( self.state, renderer, &layout::Limits::new( Size::ZERO, self.snap_within_viewport .then(|| viewport.size()) .unwrap_or(Size::INFINITY), ) .shrink(Padding::new(self.padding)), ); let text_bounds = tooltip_layout.bounds(); let x_center = self.position.x + (self.content_bounds.width - text_bounds.width) / 2.0; let y_center = self.position.y + (self.content_bounds.height - text_bounds.height) / 2.0; let mut tooltip_bounds = { let offset = match self.positioning { Position::Top => Vector::new( x_center, self.position.y - text_bounds.height - self.gap - self.padding, ), Position::Bottom => Vector::new( x_center, self.position.y + self.content_bounds.height + self.gap + self.padding, ), Position::Left => Vector::new( self.position.x - text_bounds.width - self.gap - self.padding, y_center, ), Position::Right => Vector::new( self.position.x + self.content_bounds.width + self.gap + self.padding, y_center, ), Position::FollowCursor => { let translation = self.position - self.content_bounds.position(); Vector::new( self.cursor_position.x, self.cursor_position.y - text_bounds.height, ) + translation } }; Rectangle { x: offset.x - self.padding, y: offset.y - self.padding, width: text_bounds.width + self.padding * 2.0, height: text_bounds.height + self.padding * 2.0, } }; if self.snap_within_viewport { if tooltip_bounds.x < viewport.x { tooltip_bounds.x = viewport.x; } else if viewport.x + viewport.width < tooltip_bounds.x + tooltip_bounds.width { tooltip_bounds.x = viewport.x + viewport.width - tooltip_bounds.width; } if tooltip_bounds.y < viewport.y { tooltip_bounds.y = viewport.y; } else if viewport.y + viewport.height < tooltip_bounds.y + tooltip_bounds.height { tooltip_bounds.y = viewport.y + viewport.height - tooltip_bounds.height; } } layout::Node::with_children( tooltip_bounds.size(), vec![ tooltip_layout .translate(Vector::new(self.padding, self.padding)), ], ) .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y)) } fn draw( &self, renderer: &mut Renderer, theme: &Theme, inherited_style: &renderer::Style, layout: Layout<'_>, cursor_position: mouse::Cursor, ) { let style = theme.style(self.class); container::draw_background(renderer, &style, layout.bounds()); let defaults = renderer::Style { text_color: style.text_color.unwrap_or(inherited_style.text_color), }; self.tooltip.as_widget().draw( self.state, renderer, theme, &defaults, layout.children().next().unwrap(), cursor_position, &Rectangle::with_size(Size::INFINITY), ); } fn is_over( &self, _layout: Layout<'_>, _renderer: &Renderer, _cursor_position: Point, ) -> bool { false } }