Skip to main content

azul_layout/widgets/
tooltip.rs

1//! Tooltip widget — wraps an arbitrary anchor [`Dom`] and shows a small text
2//! popup near it while the pointer hovers, hiding it again on leave.
3//!
4//! ## Implementation note (CSS-based, see `TODO2` below)
5//!
6//! The drop-down popup path (`open_menu_for_hit_node` / `MenuPopupPosition`) is
7//! built for *menus* — a list of clickable `MenuItem`s — not arbitrary text
8//! shown next to an anchor, and it would also require a live window/hit-test to
9//! verify. This widget therefore takes the simpler, fully-compilable and
10//! self-contained CSS route the recipe allows: the tip is an absolutely-
11//! positioned child of a `position: relative` wrapper, hidden by default
12//! (`opacity: 0`) and revealed on `MouseEnter` / hidden on `MouseLeave` via
13//! `set_css_property`. No user callbacks are needed — the show/hide handlers are
14//! internal.
15//!
16//! TODO2: this is a CSS simplification of a "real" floating popover. The tip is
17//! placed at a fixed offset below the anchor (it does not measure the anchor's
18//! height, flip when near a screen edge, or escape an `overflow: hidden`
19//! ancestor). A future revision could route through the window-popup / menu
20//! popup path for true screen-anchored positioning once that is runtime-
21//! verifiable.
22//!
23//! Key types: [`Tooltip`].
24
25use azul_core::{
26    callbacks::{CoreCallback, CoreCallbackData, Update},
27    dom::{Dom, EventFilter, HoverEventFilter, IdOrClass, IdOrClass::Class, IdOrClassVec},
28    refany::{OptionRefAny, RefAny},
29};
30use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
31use azul_css::{
32    props::{
33        basic::{color::ColorU, StyleFontSize},
34        layout::{LayoutDisplay, LayoutPosition, LayoutFlexGrow, LayoutTop, LayoutLeft, LayoutPaddingLeft, LayoutPaddingRight, LayoutPaddingTop, LayoutPaddingBottom},
35        property::{CssProperty, StyleWhiteSpaceValue},
36        style::{StyleBackgroundContent, StyleBackgroundContentVec, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleTextColor, StyleWhiteSpace, StyleOpacity},
37    },
38    AzString,
39};
40
41use crate::callbacks::CallbackInfo;
42
43static TOOLTIP_WRAPPER_CLASS: &[IdOrClass] =
44    &[Class(AzString::from_const_str("__azul-native-tooltip"))];
45static TOOLTIP_TIP_CLASS: &[IdOrClass] =
46    &[Class(AzString::from_const_str("__azul-native-tooltip-tip"))];
47
48// ---- layout (logical px) ----
49/// Fixed vertical offset of the tip below the wrapper's top edge. A
50/// simplification — see the module-level `TODO2`.
51const TIP_OFFSET_Y: isize = 22;
52const TIP_RADIUS: isize = 4;
53
54// ---- colours ----
55/// Tip background (#333333, dark).
56const TIP_BG_COLOR: ColorU = ColorU {
57    r: 51,
58    g: 51,
59    b: 51,
60    a: 240,
61};
62/// Tip text colour (white).
63const TIP_TEXT_COLOR: ColorU = ColorU {
64    r: 255,
65    g: 255,
66    b: 255,
67    a: 255,
68};
69
70const TIP_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(TIP_BG_COLOR)];
71const TIP_BG: StyleBackgroundContentVec = StyleBackgroundContentVec::from_const_slice(TIP_BG_ITEMS);
72
73/// Wrapper around the anchor: an inline-block positioning context so the
74/// absolutely-positioned tip is placed relative to it.
75static TOOLTIP_WRAPPER_STYLE: &[CssPropertyWithConditions] = &[
76    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::InlineBlock)),
77    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Relative)),
78    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
79];
80
81/// The tip itself: absolutely positioned, hidden by default (`opacity: 0`).
82static TOOLTIP_TIP_STYLE: &[CssPropertyWithConditions] = &[
83    CssPropertyWithConditions::simple(CssProperty::const_position(LayoutPosition::Absolute)),
84    CssPropertyWithConditions::simple(CssProperty::const_top(LayoutTop::const_px(TIP_OFFSET_Y))),
85    CssPropertyWithConditions::simple(CssProperty::const_left(LayoutLeft::const_px(0))),
86    CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
87        8,
88    ))),
89    CssPropertyWithConditions::simple(CssProperty::const_padding_right(
90        LayoutPaddingRight::const_px(8),
91    )),
92    CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(4))),
93    CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
94        LayoutPaddingBottom::const_px(4),
95    )),
96    CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
97        StyleBorderTopLeftRadius::const_px(TIP_RADIUS),
98    )),
99    CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
100        StyleBorderTopRightRadius::const_px(TIP_RADIUS),
101    )),
102    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
103        StyleBorderBottomLeftRadius::const_px(TIP_RADIUS),
104    )),
105    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
106        StyleBorderBottomRightRadius::const_px(TIP_RADIUS),
107    )),
108    CssPropertyWithConditions::simple(CssProperty::const_background_content(TIP_BG)),
109    CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
110        inner: TIP_TEXT_COLOR,
111    })),
112    CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(12))),
113    // Preserve the tip on one line so it does not wrap into the anchor's width.
114    CssPropertyWithConditions::simple(CssProperty::WhiteSpace(StyleWhiteSpaceValue::Exact(
115        StyleWhiteSpace::Nowrap,
116    ))),
117    // Hidden until hovered.
118    CssPropertyWithConditions::simple(CssProperty::const_opacity(StyleOpacity::const_new(0))),
119];
120
121/// A tooltip: an anchor [`Dom`] plus the text shown on hover.
122#[derive(Debug, Clone, PartialEq, Eq)]
123#[repr(C)]
124pub struct Tooltip {
125    /// The element the tooltip is attached to.
126    pub anchor: Dom,
127    /// The text shown in the tip popup.
128    pub text: AzString,
129    /// Style of the positioning wrapper around the anchor.
130    pub wrapper_style: CssPropertyWithConditionsVec,
131    /// Style of the tip popup.
132    pub tip_style: CssPropertyWithConditionsVec,
133}
134
135impl Default for Tooltip {
136    fn default() -> Self {
137        Self::new(Dom::default(), AzString::from_const_str(""))
138    }
139}
140
141impl Tooltip {
142    /// Creates a tooltip wrapping `anchor` that shows `text` on hover.
143    #[must_use] pub fn new(anchor: Dom, text: AzString) -> Self {
144        Self {
145            anchor,
146            text,
147            wrapper_style: CssPropertyWithConditionsVec::from_const_slice(TOOLTIP_WRAPPER_STYLE),
148            tip_style: CssPropertyWithConditionsVec::from_const_slice(TOOLTIP_TIP_STYLE),
149        }
150    }
151
152    /// Sets the tip text.
153    #[inline]
154    pub fn set_text(&mut self, text: AzString) {
155        self.text = text;
156    }
157
158    /// Builder-style setter for the tip text.
159    #[inline]
160    #[must_use] pub fn with_text(mut self, text: AzString) -> Self {
161        self.set_text(text);
162        self
163    }
164
165    /// Overrides the tip popup style.
166    #[inline]
167    pub fn set_tip_style(&mut self, style: CssPropertyWithConditionsVec) {
168        self.tip_style = style;
169    }
170
171    /// Builder-style setter for the tip popup style.
172    #[inline]
173    #[must_use] pub fn with_tip_style(mut self, style: CssPropertyWithConditionsVec) -> Self {
174        self.set_tip_style(style);
175        self
176    }
177
178    #[inline]
179    #[must_use] pub fn swap_with_default(&mut self) -> Self {
180        let mut s = Self::default();
181        core::mem::swap(&mut s, self);
182        s
183    }
184
185    #[must_use] pub fn dom(self) -> Dom {
186        // The hover handlers only navigate the DOM (the tip is found relative to
187        // the hovered wrapper), so no per-tooltip state is needed.
188        let marker = RefAny::new(());
189
190        let tip = Dom::create_text(self.text)
191            .with_ids_and_classes(IdOrClassVec::from_const_slice(TOOLTIP_TIP_CLASS))
192            .with_css_props(self.tip_style);
193
194        Dom::create_div()
195            .with_ids_and_classes(IdOrClassVec::from_const_slice(TOOLTIP_WRAPPER_CLASS))
196            .with_css_props(self.wrapper_style)
197            .with_callbacks(
198                vec![
199                    CoreCallbackData {
200                        event: EventFilter::Hover(HoverEventFilter::MouseEnter),
201                        callback: CoreCallback {
202                            cb: on_tooltip_enter as usize,
203                            ctx: OptionRefAny::None,
204                        },
205                        refany: marker.clone(),
206                    },
207                    CoreCallbackData {
208                        event: EventFilter::Hover(HoverEventFilter::MouseLeave),
209                        callback: CoreCallback {
210                            cb: on_tooltip_leave as usize,
211                            ctx: OptionRefAny::None,
212                        },
213                        refany: marker,
214                    },
215                ]
216                .into(),
217            )
218            // children: [anchor, tip] — the tip is the anchor's next sibling.
219            .with_children(vec![self.anchor, tip].into())
220    }
221}
222
223/// Returns the tip node (the second child) of the hovered wrapper.
224fn tip_of_wrapper(info: &CallbackInfo) -> Option<azul_core::dom::DomNodeId> {
225    let wrapper = info.get_hit_node();
226    let anchor = info.get_first_child(wrapper)?;
227    info.get_next_sibling(anchor)
228}
229
230/// Pointer entered the wrapper → reveal the tip.
231extern "C" fn on_tooltip_enter(_data: RefAny, mut info: CallbackInfo) -> Update {
232    if let Some(tip) = tip_of_wrapper(&info) {
233        info.set_css_property(tip, CssProperty::const_opacity(StyleOpacity::const_new(100)));
234    }
235    Update::DoNothing
236}
237
238/// Pointer left the wrapper → hide the tip.
239extern "C" fn on_tooltip_leave(_data: RefAny, mut info: CallbackInfo) -> Update {
240    if let Some(tip) = tip_of_wrapper(&info) {
241        info.set_css_property(tip, CssProperty::const_opacity(StyleOpacity::const_new(0)));
242    }
243    Update::DoNothing
244}
245
246impl From<Tooltip> for Dom {
247    fn from(t: Tooltip) -> Self {
248        t.dom()
249    }
250}