Skip to main content

tui_lipan/widgets/
tooltip.rs

1//! Tooltip widget.
2
3use std::sync::Arc;
4use std::sync::atomic::{AtomicUsize, Ordering};
5
6use crate::core::component::{Component, Context, Update};
7use crate::core::element::{Element, IntoElement, Key};
8use crate::style::{BorderStyle, Padding, Style};
9use crate::widgets::{Frame, Popover, PopoverOffset, PopoverPlacement, Text};
10
11static TOOLTIP_ID: AtomicUsize = AtomicUsize::new(1);
12
13/// A tooltip widget that displays help text on hover or focus.
14#[derive(Clone)]
15pub struct Tooltip {
16    child: Element,
17    text: Arc<str>,
18    open: Option<bool>,
19    auto: bool,
20    show_on_focus: bool,
21    text_style: Style,
22    container_style: Style,
23    border: bool,
24    border_style: BorderStyle,
25    padding: Padding,
26    placement: PopoverPlacement,
27    offset: PopoverOffset,
28    clamp: bool,
29    auto_flip: bool,
30}
31
32impl Tooltip {
33    /// Create a new tooltip wrapping a child element.
34    pub fn new(text: impl Into<Arc<str>>) -> Self {
35        Self {
36            child: crate::widgets::Spacer::new().into(),
37            text: text.into(),
38            open: None,
39            auto: true,
40            show_on_focus: true,
41            text_style: Style::default(),
42            container_style: Style::default(),
43            border: false,
44            border_style: BorderStyle::Plain,
45            padding: 0.into(),
46            placement: PopoverPlacement::BelowStart,
47            offset: PopoverOffset::ZERO,
48            clamp: true,
49            auto_flip: true,
50        }
51    }
52
53    /// Set the child element.
54    pub fn child(mut self, child: impl IntoElement) -> Self {
55        self.child = child.into();
56        self
57    }
58
59    /// Set open state.
60    pub fn open(mut self, open: bool) -> Self {
61        self.open = Some(open);
62        self
63    }
64
65    /// Enable or disable auto-open on hover/focus.
66    pub fn auto(mut self, auto: bool) -> Self {
67        self.auto = auto;
68        self
69    }
70
71    /// Enable or disable automatic display while the trigger has focus.
72    pub fn show_on_focus(mut self, show_on_focus: bool) -> Self {
73        self.show_on_focus = show_on_focus;
74        self
75    }
76
77    /// Set text style.
78    pub fn text_style(mut self, style: Style) -> Self {
79        self.text_style = style;
80        self
81    }
82
83    /// Set container style.
84    pub fn container_style(mut self, style: Style) -> Self {
85        self.container_style = style;
86        self
87    }
88
89    /// Draw a border around the tooltip.
90    pub fn border(mut self, border: bool) -> Self {
91        self.border = border;
92        self
93    }
94
95    /// Set border style.
96    pub fn border_style(mut self, border_style: BorderStyle) -> Self {
97        self.border_style = border_style;
98        self
99    }
100
101    /// Set padding.
102    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
103        self.padding = padding.into();
104        self
105    }
106
107    /// Set popover placement.
108    pub fn placement(mut self, placement: PopoverPlacement) -> Self {
109        self.placement = placement;
110        self
111    }
112
113    /// Set popover offset.
114    pub fn offset(mut self, offset: impl Into<PopoverOffset>) -> Self {
115        self.offset = offset.into();
116        self
117    }
118
119    /// Clamp popover to the viewport bounds.
120    pub fn clamp(mut self, clamp: bool) -> Self {
121        self.clamp = clamp;
122        self
123    }
124
125    /// Automatically flip placement when it overflows the viewport.
126    pub fn auto_flip(mut self, auto_flip: bool) -> Self {
127        self.auto_flip = auto_flip;
128        self
129    }
130}
131
132#[derive(Clone)]
133struct TooltipProps {
134    child: Element,
135    text: Arc<str>,
136    open: Option<bool>,
137    auto: bool,
138    show_on_focus: bool,
139    text_style: Style,
140    container_style: Style,
141    border: bool,
142    border_style: BorderStyle,
143    padding: Padding,
144    placement: PopoverPlacement,
145    offset: PopoverOffset,
146    clamp: bool,
147    auto_flip: bool,
148}
149
150impl PartialEq for TooltipProps {
151    fn eq(&self, other: &Self) -> bool {
152        if self.text != other.text
153            || self.open != other.open
154            || self.auto != other.auto
155            || self.show_on_focus != other.show_on_focus
156            || self.text_style != other.text_style
157            || self.container_style != other.container_style
158            || self.border != other.border
159            || self.border_style != other.border_style
160            || self.padding != other.padding
161            || self.placement != other.placement
162            || self.offset != other.offset
163            || self.clamp != other.clamp
164            || self.auto_flip != other.auto_flip
165        {
166            return false;
167        }
168
169        let left = crate::layout::hash::element_layout_hash(&self.child);
170        let right = crate::layout::hash::element_layout_hash(&other.child);
171        match (left, right) {
172            (Some(left), Some(right)) => left == right,
173            _ => false,
174        }
175    }
176}
177
178impl From<Tooltip> for TooltipProps {
179    fn from(tooltip: Tooltip) -> Self {
180        Self {
181            child: tooltip.child,
182            text: tooltip.text,
183            open: tooltip.open,
184            auto: tooltip.auto,
185            show_on_focus: tooltip.show_on_focus,
186            text_style: tooltip.text_style,
187            container_style: tooltip.container_style,
188            border: tooltip.border,
189            border_style: tooltip.border_style,
190            padding: tooltip.padding,
191            placement: tooltip.placement,
192            offset: tooltip.offset,
193            clamp: tooltip.clamp,
194            auto_flip: tooltip.auto_flip,
195        }
196    }
197}
198
199#[derive(Clone, Debug)]
200struct TooltipState {
201    trigger_key: Key,
202}
203
204struct TooltipComponent;
205
206impl TooltipComponent {
207    fn new() -> Self {
208        Self
209    }
210}
211
212impl Component for TooltipComponent {
213    type Message = ();
214    type Properties = TooltipProps;
215    type State = TooltipState;
216
217    fn create_state(&self, _props: &Self::Properties) -> Self::State {
218        let id = TOOLTIP_ID.fetch_add(1, Ordering::Relaxed);
219        TooltipState {
220            trigger_key: Key::from(format!("tooltip-trigger-{}", id)),
221        }
222    }
223
224    fn update(&mut self, _msg: Self::Message, _ctx: &mut Context<Self>) -> Update {
225        Update::none()
226    }
227
228    fn view(&self, ctx: &Context<Self>) -> Element {
229        let trigger = ctx.props.child.clone().key(ctx.state.trigger_key.clone());
230
231        let auto_open = ctx.props.auto
232            && (ctx.has_hover_within_key(ctx.state.trigger_key.clone())
233                || (ctx.props.show_on_focus
234                    && ctx.has_focus_within_key(ctx.state.trigger_key.clone())));
235        let open = ctx.props.open.unwrap_or(auto_open);
236
237        let content = Frame::new()
238            .border(ctx.props.border)
239            .border_style(ctx.props.border_style)
240            .padding(ctx.props.padding)
241            .style(ctx.props.container_style)
242            .child(Text::new(ctx.props.text.clone()).style(ctx.props.text_style));
243
244        Popover::new()
245            .trigger(trigger)
246            .content(content)
247            .open(open)
248            .capture_focus(false)
249            .auto_focus(false)
250            .min_trigger_width(false)
251            .placement(ctx.props.placement)
252            .offset(ctx.props.offset)
253            .clamp(ctx.props.clamp)
254            .auto_flip(ctx.props.auto_flip)
255            .into()
256    }
257}
258
259impl From<Tooltip> for Element {
260    fn from(tooltip: Tooltip) -> Self {
261        crate::child(TooltipComponent::new, TooltipProps::from(tooltip))
262    }
263}