repose_material/material3/
tooltip.rs1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use repose_core::*;
7use repose_ui::{
8 Box, Text, TextStyle,
9 ViewExt,
10 anim::animate_f32,
11};
12
13use super::*;
14
15static TOOLTIP_COUNTER: AtomicU64 = AtomicU64::new(0);
16
17#[derive(Clone, Debug)]
19pub struct TooltipConfig {
20 pub modifier: Modifier,
21 pub container_color: Color,
22 pub content_color: Color,
23 pub offset_y: f32,
24 pub horizontal_padding: f32,
25 pub vertical_padding: f32,
26 pub has_action: bool,
27 pub enable_user_input: bool,
28 pub focusable: bool,
29 pub max_width: f32,
30 pub tonal_elevation: f32,
31 pub shadow_elevation: f32,
32}
33
34impl Default for TooltipConfig {
35 fn default() -> Self {
36 Self {
37 modifier: Modifier::new(),
38 container_color: TooltipDefaults::container_color(),
39 content_color: TooltipDefaults::content_color(),
40 offset_y: TooltipDefaults::OFFSET_Y,
41 horizontal_padding: TooltipDefaults::HORIZONTAL_PADDING,
42 vertical_padding: TooltipDefaults::VERTICAL_PADDING,
43 has_action: false,
44 enable_user_input: true,
45 focusable: false,
46 max_width: TooltipDefaults::MAX_WIDTH,
47 tonal_elevation: 0.0,
48 shadow_elevation: 0.0,
49 }
50 }
51}
52
53pub struct TooltipState {
55 visible: Signal<bool>,
56}
57
58impl TooltipState {
59 pub fn new() -> Rc<Self> {
60 Rc::new(Self {
61 visible: signal(false),
62 })
63 }
64
65 pub fn is_visible(&self) -> bool {
66 self.visible.get()
67 }
68
69 pub fn show(&self) {
70 self.visible.set(true);
71 }
72
73 pub fn dismiss(&self) {
74 self.visible.set(false);
75 }
76}
77
78pub fn TooltipBox(
92 text: impl Into<String>,
93 state: Rc<TooltipState>,
94 content: View,
95 config: TooltipConfig,
96) -> View {
97 let text: Rc<str> = Rc::from(text.into());
98 let th = theme();
99 let spec = th.motion.overlay;
100 let id = remember(|| TOOLTIP_COUNTER.fetch_add(1, Ordering::Relaxed));
101
102 let alpha = animate_f32(
103 format!("tooltip_alpha_{id}"),
104 if state.is_visible() { 1.0 } else { 0.0 },
105 spec,
106 );
107
108 let tooltip_visible = state.is_visible() || alpha > 0.01;
109 let scale = 0.92 + 0.08 * alpha;
110
111 let mut host = config
112 .modifier
113 .align_self(AlignSelf::FLEX_START)
114 .flex_shrink(0.0);
115
116 if config.enable_user_input {
117 let enter = state.clone();
118 let leave = state.clone();
119 host = host.hoverable(
120 move || enter.show(),
121 move || leave.dismiss(),
122 );
123 }
124
125 Box(host).child((
126 content,
127 if tooltip_visible {
128 Box(Modifier::new()
129 .absolute()
130 .offset(Some(0.0), Some(config.offset_y), Some(0.0), None)
131 .justify_content(JustifyContent::CENTER)
132 .align_items(AlignItems::CENTER)
133 .hit_passthrough()
134 .render_z_index(10_000.0)
135 .alpha(alpha))
136 .child(
137 Box(Modifier::new()
138 .background(config.container_color)
139 .clip_rounded(th.shapes.extra_small)
140 .padding_values(PaddingValues {
141 left: config.horizontal_padding,
142 right: config.horizontal_padding,
143 top: config.vertical_padding,
144 bottom: config.vertical_padding,
145 })
146 .max_width(config.max_width)
147 .flex_shrink(0.0)
148 .scale(scale)
149 .hit_passthrough()
150 .then({
151 let mut m = Modifier::new();
152 if config.shadow_elevation > 0.0 {
153 m = m.shadow(config.shadow_elevation, 0.0);
154 }
155 if config.tonal_elevation > 0.0 {
156 m = m.state_elevation(StateElevation {
157 default: config.tonal_elevation,
158 hovered: config.tonal_elevation,
159 pressed: config.tonal_elevation,
160 dragged: config.tonal_elevation,
161 disabled: 0.0,
162 });
163 }
164 m
165 }))
166 .child(
167 Text((*text).to_string())
168 .color(config.content_color)
169 .size(th.typography.label_medium),
170 ),
171 )
172 } else {
173 Box(Modifier::new())
174 },
175 ))
176}