Skip to main content

repose_material/material3/
tooltip.rs

1#![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/// Configuration for tooltip.
14#[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
49/// State controlling tooltip visibility.
50pub 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
74/// Wraps `content` with a tooltip label shown above it when `state` is visible.
75///
76/// When [`TooltipConfig::enable_user_input`] is true (default), the tooltip is
77/// shown on pointer hover and dismissed on leave.
78///
79/// Usage:
80/// ```ignore
81/// let tip = TooltipState::new();
82/// TooltipBox("I'm a tooltip", tip.clone(), Modifier::new(), Button("Hover me", {
83///     let tip = tip.clone();
84///     move || tip.show()
85/// }));
86/// ```
87pub 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}