1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
use rosace_core::types::{Point, Size};
use rosace_render::Color;
use super::overlay::{FocusBehavior, InputBehavior, LayerPosition, OverlayEntry};
use super::{BoxedWidget, Children, LayoutCtx, PaintCtx, Widget};
/// Theme-driven tooltip appearance (D115/Phase 32). Set once on the theme
/// via [`rosace_theme::ThemeData::with_ext`] and every `.tooltip(..)` /
/// [`Tooltip`] in the app picks it up — shape, colors, font, padding all
/// come from here, with a sensible default when the theme sets none. Same
/// type-keyed extension mechanism `CursorStyle` uses (D105).
#[derive(Debug, Clone, Copy)]
pub struct TooltipStyle {
pub background: Color,
pub text_color: Color,
pub radius: f32,
pub font_size: f32,
/// Horizontal padding inside the bubble; vertical padding is derived
/// from the font size so the label always sits centered.
pub pad_h: f32,
/// Drop-shadow strength; `0.0` disables it.
pub elevation: f32,
}
impl Default for TooltipStyle {
fn default() -> Self {
Self {
background: Color::rgba(40, 42, 58, 245),
text_color: Color::rgb(228, 230, 244),
radius: 6.0,
font_size: 12.0,
pad_h: 10.0,
elevation: 1.0,
}
}
}
impl TooltipStyle {
/// Resolve from the active theme's extension, else the default.
fn resolve(theme: &rosace_theme::ThemeData) -> Self {
theme.ext::<TooltipStyle>().copied().unwrap_or_default()
}
}
/// Wraps a child and shows a floating label while the pointer hovers it.
///
/// Prefer the ergonomic `widget.tooltip("text")` ([`super::WidgetExt`])
/// over constructing this directly — same result, no wrapping. Styling is
/// theme-driven ([`TooltipStyle`]); an explicit `.style(..)` overrides it
/// per-tooltip.
pub struct Tooltip {
label: String,
style: Option<TooltipStyle>,
child: BoxedWidget,
}
impl Tooltip {
pub fn new(label: impl Into<String>, child: impl Widget + 'static) -> Self {
Self { label: label.into(), style: None, child: Box::new(child) }
}
/// Per-tooltip style override (otherwise the theme's `TooltipStyle`).
pub fn style(mut self, style: TooltipStyle) -> Self {
self.style = Some(style);
self
}
/// Convenience: override just the font size on the resolved style.
pub fn font_size(mut self, s: f32) -> Self {
let mut st = self.style.unwrap_or_default();
st.font_size = s;
self.style = Some(st);
self
}
}
impl Widget for Tooltip {
fn children(&self) -> Children<'_> { Children::One(&*self.child) }
fn paint(&self, ctx: &mut PaintCtx) {
let r = ctx.rect;
self.child.paint(&mut ctx.child(r));
ctx.hoverable();
if ctx.hovered() {
let style = self.style.unwrap_or_else(|| TooltipStyle::resolve(&ctx.theme));
let w = ctx.font.measure_text(&self.label, style.font_size) + style.pad_h * 2.0;
let h = style.font_size * 1.7;
let label = self.label.clone();
// TREE-ATTACHED so the engine's `AboveCentered` handling maps
// the anchor through `content_to_screen` — a tooltip on a
// widget inside a GPU scroll layer is remapped to window space
// and centred over its anchor (the legacy Absolute push path
// skipped that remap and dropped the label off-screen).
ctx.attach_overlay(
OverlayEntry::new(
LayerPosition::AboveCentered(r),
TooltipLabel { label, w, h, style },
)
.input(InputBehavior::PassThrough)
.focus(FocusBehavior::Inert),
);
}
}
}
struct TooltipLabel {
label: String,
w: f32,
h: f32,
style: TooltipStyle,
}
impl Widget for TooltipLabel {
fn layout(&self, ctx: &LayoutCtx) -> Size {
ctx.constraints.constrain(Size { width: self.w, height: self.h })
}
fn paint(&self, ctx: &mut PaintCtx) {
let r = ctx.rect;
if self.style.elevation > 0.0 {
ctx.fill_shadow_rrect(r, self.style.radius, Color::rgba(0, 0, 0, 90), 8.0);
}
ctx.fill_rrect(r, self.style.radius, self.style.background);
let ty = r.origin.y + (self.h - ctx.font.line_height(self.style.font_size)) / 2.0;
ctx.draw_text_at(
&self.label,
Point { x: r.origin.x + self.style.pad_h, y: ty },
self.style.text_color,
self.style.font_size,
);
}
}
/// Ergonomic extension available on EVERY widget (D115/Phase 32): attach a
/// tooltip as a PROPERTY instead of wrapping — `Button::new("Save")
/// .tooltip("Saves changes")`. Desktop shows it on hover; mobile has no
/// hover, so it's naturally inert there (a long-press variant can hook the
/// same path later). Styling comes from the theme's [`TooltipStyle`].
pub trait WidgetExt: Widget + Sized + 'static {
/// Show `label` while the pointer hovers this widget.
fn tooltip(self, label: impl Into<String>) -> Tooltip {
Tooltip::new(label, self)
}
}
impl<W: Widget + Sized + 'static> WidgetExt for W {}