Skip to main content

sequoia_elements/
tooltip.rs

1use sequoia_core::{Color, Edges, Size};
2use sequoia_layout::{Axis, ContainerStyle, LayoutCtx, LayoutStyle};
3use sequoia_style::TooltipStyle;
4use sequoia_view::{
5    Anchor, AnyView, InputMode, OverlayEntry, OverlayLayer, PaintCtx, Placement, Unit, View,
6};
7
8const PAD_X: f32 = 8.0;
9const PAD_Y: f32 = 5.0;
10
11pub struct Tooltip {
12    child: Option<AnyView>,
13    tip: String,
14    font_size: Option<f32>,
15    style: Option<TooltipStyle>,
16    background: Option<Color>,
17    border: Option<Color>,
18    text_color: Option<Color>,
19    radius: Option<f32>,
20    padding: Option<(f32, f32)>,
21    margin: Edges,
22}
23
24impl Tooltip {
25    pub fn wrap<V: View + 'static>(child: V, tip: impl Into<String>) -> Self {
26        Self::new(child, tip)
27    }
28
29    pub fn new<V: View + 'static>(child: V, tip: impl Into<String>) -> Self {
30        Self {
31            child: Some(AnyView::new(child)),
32            tip: tip.into(),
33            font_size: None,
34            style: None,
35            background: None,
36            border: None,
37            text_color: None,
38            radius: None,
39            padding: None,
40            margin: Edges::ZERO,
41        }
42    }
43
44    pub fn font_size(mut self, size: f32) -> Self {
45        self.font_size = Some(size);
46        self
47    }
48
49    pub fn style(mut self, style: TooltipStyle) -> Self {
50        self.style = Some(style);
51        self
52    }
53
54    pub fn background(mut self, color: Color) -> Self {
55        self.background = Some(color);
56        self
57    }
58
59    pub fn border(mut self, color: Color) -> Self {
60        self.border = Some(color);
61        self
62    }
63
64    pub fn text_color(mut self, color: Color) -> Self {
65        self.text_color = Some(color);
66        self
67    }
68
69    pub fn radius(mut self, radius: f32) -> Self {
70        self.radius = Some(radius);
71        self
72    }
73
74    pub fn padding_xy(mut self, pad_x: f32, pad_y: f32) -> Self {
75        self.padding = Some((pad_x, pad_y));
76        self
77    }
78
79    pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
80        let m = margin.into();
81        self.margin = Edges::only(
82            sanitize_margin(m.left),
83            sanitize_margin(m.top),
84            sanitize_margin(m.right),
85            sanitize_margin(m.bottom),
86        );
87        self
88    }
89
90    pub fn margin_xy(self, x: f32, y: f32) -> Self {
91        self.margin(Edges::xy(x, y))
92    }
93
94    pub fn margin_x(mut self, x: f32) -> Self {
95        let x = sanitize_margin(x);
96        self.margin.left = x;
97        self.margin.right = x;
98        self
99    }
100
101    pub fn margin_y(mut self, y: f32) -> Self {
102        let y = sanitize_margin(y);
103        self.margin.top = y;
104        self.margin.bottom = y;
105        self
106    }
107
108    pub fn margin_left(mut self, v: f32) -> Self {
109        self.margin.left = sanitize_margin(v);
110        self
111    }
112
113    pub fn margin_right(mut self, v: f32) -> Self {
114        self.margin.right = sanitize_margin(v);
115        self
116    }
117
118    pub fn margin_top(mut self, v: f32) -> Self {
119        self.margin.top = sanitize_margin(v);
120        self
121    }
122
123    pub fn margin_bottom(mut self, v: f32) -> Self {
124        self.margin.bottom = sanitize_margin(v);
125        self
126    }
127}
128
129impl View for Tooltip {
130    fn body(&self) -> impl View + 'static {
131        Unit
132    }
133    fn children_override(&self) -> Option<Vec<AnyView>> {
134        self.child.clone().map(|c| vec![c])
135    }
136    fn layout_style(&self) -> LayoutStyle {
137        LayoutStyle {
138            width: sequoia_core::Length::Fit,
139            height: sequoia_core::Length::Fit,
140            margin: self.margin,
141            ..LayoutStyle::default()
142        }
143    }
144    fn container(&self) -> Option<ContainerStyle> {
145        Some(ContainerStyle::new(Axis::Vertical, 0.0))
146    }
147    fn has_visual_state(&self) -> bool {
148        true
149    }
150    fn paint_self(&self, ctx: &mut PaintCtx) {
151        if !ctx.hovered_subtree || self.tip.is_empty() {
152            return;
153        }
154        let theme = ctx.theme();
155        let style = self.style.unwrap_or_default();
156        let size = self
157            .font_size
158            .or(style.font_size)
159            .unwrap_or(theme.font_size);
160        let background = self
161            .background
162            .or(style.background)
163            .unwrap_or(Color::rgb8(0x2B, 0x2B, 0x2B));
164        let border = self.border.or(style.border).unwrap_or(theme.border);
165        let text = self.text_color.or(style.text).unwrap_or(theme.text);
166        let radius = self.radius.or(style.radius).unwrap_or(3.0);
167        let padding = self.padding.or(style.padding).unwrap_or((PAD_X, PAD_Y));
168        ctx.show_overlay(
169            OverlayEntry::new(TooltipBubble {
170                tip: self.tip.clone(),
171                font_size: size,
172                background,
173                border,
174                text,
175                radius,
176                padding,
177            })
178            .anchor(Anchor::Rect(ctx.rect))
179            .placement(Placement::Bottom)
180            .layer(OverlayLayer::Tooltip)
181            .input_mode(InputMode::Passthrough)
182            .transient(true)
183            .offset(0.0, 4.0),
184        );
185    }
186}
187
188struct TooltipBubble {
189    tip: String,
190    font_size: f32,
191    background: Color,
192    border: Color,
193    text: Color,
194    radius: f32,
195    padding: (f32, f32),
196}
197
198impl View for TooltipBubble {
199    fn body(&self) -> impl View + 'static {
200        Unit
201    }
202    fn is_leaf(&self) -> bool {
203        true
204    }
205    fn layout_style(&self) -> LayoutStyle {
206        LayoutStyle {
207            width: sequoia_core::Length::Fit,
208            height: sequoia_core::Length::Fit,
209            ..LayoutStyle::default()
210        }
211    }
212    fn intrinsic_size(&self, ctx: &mut LayoutCtx) -> Option<Size> {
213        let w = ctx.text_width(&self.tip, self.font_size);
214        let (pad_x, pad_y) = self.padding;
215        Some(Size::new(
216            w + pad_x * 2.0,
217            LayoutCtx::text_line_height(self.font_size) + pad_y * 2.0,
218        ))
219    }
220    fn paint_self(&self, ctx: &mut PaintCtx) {
221        let rect = ctx.rect;
222        if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
223            return;
224        }
225        ctx.quad(rect, self.background, self.radius);
226        ctx.quad(
227            sequoia_core::Rect::from_xywh(
228                rect.left() + 1.0,
229                rect.top() + 1.0,
230                (rect.size.width - 2.0).max(0.0),
231                (rect.size.height - 2.0).max(0.0),
232            ),
233            self.border,
234            (self.radius - 1.0).max(0.0),
235        );
236        let (pad_x, _) = self.padding;
237        let x = rect.left() + pad_x;
238        let y = rect.top() + rect.size.height * 0.5 + self.font_size * 0.35;
239        ctx.draw_text(&self.tip, (x, y), self.font_size, self.text);
240    }
241}
242
243fn sanitize_margin(v: f32) -> f32 {
244    if v.is_finite() { v.max(0.0) } else { 0.0 }
245}