// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! Tooltip — short hint anchored below a widget. //! //! Like [`Toast`](super::toast::Toast), `Tooltip` is **not** an //! [`Element`]; it is a builder that produces an [`OverlaySpec`] for //! the application to return from [`App::overlays`](crate::App::overlays). //! The overlay is rendered as an `xdg-popup` anchored to the widget //! tagged with [`Tooltip::anchor_id`] in the previous frame's layout, //! so the hint appears flush below the trigger and follows it across //! resizes and re-layouts. //! //! Hover detection and the show / hide delay are the **application's //! responsibility**. ltk does not currently expose pointer-hover //! callbacks at the widget level, so the typical pattern is: //! //! 1. Track which widget the pointer is over via `App::on_tap` or //! a custom hover bookkeeping you maintain in `update`. //! 2. Start a timer (via [`App::set_channel_sender`](crate::App::set_channel_sender)) //! that fires a "show tooltip" message after the desired delay. //! 3. Return the [`Tooltip::overlay`] from `overlays()` only while the //! tooltip should be visible. //! //! ```rust,no_run //! # use ltk::{ tooltip, OverlaySpec, WidgetId }; //! # struct App { tooltip_for: Option } //! # impl App { //! fn overlays( &self ) -> Vec> //! { //! match self.tooltip_for //! { //! Some( id ) => vec![ tooltip( "Save the document", id ).overlay() ], //! None => vec![], //! } //! } //! # } //! ``` use std::collections::hash_map::DefaultHasher; use std::hash::{ Hash, Hasher }; use crate::app::{ Anchor, Layer, OverlayId, OverlaySpec }; use crate::types::{ Color, Length, WidgetId }; use super::Element; mod theme; #[ cfg( test ) ] mod tests; /// Builder for an `xdg-popup`-backed hint anchored to a widget. pub struct Tooltip { /// Body text rendered inside the tooltip. pub( crate ) message: String, /// Stable identifier of the widget the popup anchors to. The widget /// must have been built with `.id( anchor_id )` so the runtime can /// look its rect up in the previous frame's layout snapshot. pub( crate ) anchor_id: WidgetId, /// Maximum width (logical pixels). The tooltip is single-line; long /// strings are clipped at the surface boundary. pub( crate ) max_width: u32, /// Optional override for the tooltip background colour. pub( crate ) bg: Option, /// Optional override for the body text colour. pub( crate ) fg: Option, /// Optional message fired when the user taps outside the popup. pub( crate ) on_dismiss: Option, } impl Tooltip { /// Create a tooltip with the given message anchored to `anchor_id`. pub fn new( message: impl Into, anchor_id: WidgetId ) -> Self { Self { message: message.into(), anchor_id, max_width: theme::MAX_W, bg: None, fg: None, on_dismiss: None, } } /// Override the maximum tooltip width. Defaults to 320 logical px. pub fn max_width( mut self, w: u32 ) -> Self { self.max_width = w; self } /// Override the background colour. pub fn background( mut self, c: Color ) -> Self { self.bg = Some( c ); self } /// Override the body text colour. pub fn color( mut self, c: Color ) -> Self { self.fg = Some( c ); self } /// Set the dismiss message fired on a tap outside the popup. pub fn on_dismiss( mut self, msg: Msg ) -> Self { self.on_dismiss = Some( msg ); self } /// Build the [`OverlaySpec`] the application returns from /// [`App::overlays`](crate::App::overlays). pub fn overlay( self ) -> OverlaySpec { use super::{ container, text }; let mut hasher = DefaultHasher::new(); "tooltip".hash( &mut hasher ); self.anchor_id.hash( &mut hasher ); let overlay_id = OverlayId( hasher.finish() as u32 ); let bg = self.bg.unwrap_or_else( theme::bg ); let fg = self.fg.unwrap_or_else( theme::text ); let body: Element = container( text( self.message.clone() ) .size( Length::widget( theme::FONT_SIZE ) ) .color( fg ) ) .background( bg ) .padding_h( Length::widget( theme::PAD_H ) ) .padding_v( Length::widget( theme::PAD_V ) ) .radius( theme::RADIUS ) .into(); OverlaySpec { id: overlay_id, layer: Layer::Overlay, anchor: Anchor::TOP, // Height scales with the display so the fluid text always fits; the // caller-set max width stays fixed so wrapping is predictable. size: ( Length::px( self.max_width as f32 ), Length::widget( theme::HEIGHT as f32 ) ), exclusive_zone: 0, keyboard_exclusive: false, input_region: None, view: body, on_dismiss: self.on_dismiss, anchor_widget_id: Some( self.anchor_id ), } } } /// Create a [`Tooltip`] anchored to the widget tagged with `anchor_id`. /// /// ```rust,no_run /// # use ltk::{ tooltip, OverlaySpec, WidgetId }; /// # fn _ex() -> OverlaySpec<()> { /// tooltip( "Click to save", WidgetId( "btn/save" ) ) /// .max_width( 240 ) /// .overlay() /// # } /// ``` pub fn tooltip( message: impl Into, anchor_id: WidgetId, ) -> Tooltip { Tooltip::new( message, anchor_id ) }