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}