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