Skip to main content

egui_material3/
tooltip.rs

1use crate::theme::get_global_color;
2use egui::{
3    pos2, Area, FontId, Id, Order, Rect, Response, Sense, Stroke, Ui, Vec2,
4};
5
6/// Tooltip position relative to target element
7#[derive(Clone, Copy, PartialEq, Debug)]
8pub enum TooltipPosition {
9    /// Above the target element
10    Top,
11    /// Below the target element
12    Bottom,
13    /// To the left of the target element
14    Left,
15    /// To the right of the target element
16    Right,
17    /// Automatically choose best position
18    Auto,
19}
20
21/// Material Design tooltip component
22///
23/// Tooltips display informative text when users hover over an element.
24/// They follow Material Design 3 specifications.
25///
26/// # Example
27/// ```rust
28/// # egui::__run_test_ui(|ui| {
29/// use egui_material3::{MaterialButton, show_tooltip_on_hover, TooltipPosition};
30///
31/// let button_response = ui.add(MaterialButton::filled("Hover me"));
32/// show_tooltip_on_hover(ui, &button_response, "This is a tooltip", TooltipPosition::Top);
33/// # });
34/// ```
35pub struct MaterialTooltip {
36    text: String,
37    position: TooltipPosition,
38    max_width: f32,
39    padding: Vec2,
40    font_size: f32,
41}
42
43impl MaterialTooltip {
44    /// Create a new tooltip
45    pub fn new(text: impl Into<String>) -> Self {
46        Self {
47            text: text.into(),
48            position: TooltipPosition::Auto,
49            max_width: 200.0,
50            padding: Vec2::new(8.0, 6.0),
51            font_size: 12.0,
52        }
53    }
54
55    /// Set the tooltip position
56    pub fn position(mut self, position: TooltipPosition) -> Self {
57        self.position = position;
58        self
59    }
60
61    /// Set the maximum width
62    pub fn max_width(mut self, width: f32) -> Self {
63        self.max_width = width;
64        self
65    }
66
67    /// Set the padding
68    pub fn padding(mut self, padding: Vec2) -> Self {
69        self.padding = padding;
70        self
71    }
72
73    /// Set the font size
74    pub fn font_size(mut self, size: f32) -> Self {
75        self.font_size = size;
76        self
77    }
78
79    /// Show the tooltip at a specific position
80    pub fn show(&self, ui: &mut Ui, target_rect: Rect) {
81        let inverse_surface = get_global_color("inverseSurface");
82        let inverse_on_surface = get_global_color("inverseOnSurface");
83
84        // Calculate text size
85        let text_galley = ui.painter().layout(
86            self.text.clone(),
87            FontId::proportional(self.font_size),
88            inverse_on_surface,
89            self.max_width - self.padding.x * 2.0,
90        );
91
92        let tooltip_size = Vec2::new(
93            text_galley.size().x + self.padding.x * 2.0,
94            text_galley.size().y + self.padding.y * 2.0,
95        );
96
97        // Calculate tooltip position based on preferred position
98        let screen_rect = ui.ctx().screen_rect();
99        let tooltip_pos = self.calculate_position(target_rect, tooltip_size, screen_rect);
100
101        // Create unique ID for this tooltip
102        let tooltip_id = Id::new("tooltip").with(&self.text);
103
104        // Show tooltip as an overlay
105        Area::new(tooltip_id)
106            .fixed_pos(tooltip_pos)
107            .order(Order::Tooltip)
108            .interactable(false)
109            .show(ui.ctx(), |ui| {
110                let (rect, _) = ui.allocate_exact_size(tooltip_size, Sense::hover());
111
112                // Draw background
113                ui.painter().rect_filled(rect, 4.0, inverse_surface);
114
115                // Draw border for better visibility
116                ui.painter().rect_stroke(
117                    rect,
118                    4.0,
119                    Stroke::new(1.0, inverse_on_surface.linear_multiply(0.2)),
120                    egui::epaint::StrokeKind::Outside,
121                );
122
123                // Draw text
124                let text_pos = pos2(
125                    rect.min.x + self.padding.x,
126                    rect.min.y + self.padding.y,
127                );
128                ui.painter().galley(text_pos, text_galley, inverse_on_surface);
129            });
130    }
131
132    /// Calculate the position for the tooltip based on the preferred position
133    fn calculate_position(
134        &self,
135        target_rect: Rect,
136        tooltip_size: Vec2,
137        screen_rect: Rect,
138    ) -> egui::Pos2 {
139        let spacing = 8.0; // Gap between target and tooltip
140
141        let position = match self.position {
142            TooltipPosition::Auto => {
143                // Auto-select best position based on available space
144                self.auto_position(target_rect, tooltip_size, screen_rect)
145            }
146            pos => pos,
147        };
148
149        match position {
150            TooltipPosition::Top => pos2(
151                target_rect.center().x - tooltip_size.x / 2.0,
152                target_rect.min.y - tooltip_size.y - spacing,
153            ),
154            TooltipPosition::Bottom => pos2(
155                target_rect.center().x - tooltip_size.x / 2.0,
156                target_rect.max.y + spacing,
157            ),
158            TooltipPosition::Left => pos2(
159                target_rect.min.x - tooltip_size.x - spacing,
160                target_rect.center().y - tooltip_size.y / 2.0,
161            ),
162            TooltipPosition::Right => pos2(
163                target_rect.max.x + spacing,
164                target_rect.center().y - tooltip_size.y / 2.0,
165            ),
166            TooltipPosition::Auto => {
167                // This shouldn't happen as Auto is converted above
168                pos2(target_rect.max.x + spacing, target_rect.min.y)
169            }
170        }
171    }
172
173    /// Automatically determine the best position
174    fn auto_position(
175        &self,
176        target_rect: Rect,
177        tooltip_size: Vec2,
178        screen_rect: Rect,
179    ) -> TooltipPosition {
180        let spacing = 8.0;
181
182        // Check available space in each direction
183        let space_above = target_rect.min.y - screen_rect.min.y;
184        let space_below = screen_rect.max.y - target_rect.max.y;
185        let space_left = target_rect.min.x - screen_rect.min.x;
186        let space_right = screen_rect.max.x - target_rect.max.x;
187
188        // Prefer bottom, then top, then right, then left
189        if space_below >= tooltip_size.y + spacing {
190            TooltipPosition::Bottom
191        } else if space_above >= tooltip_size.y + spacing {
192            TooltipPosition::Top
193        } else if space_right >= tooltip_size.x + spacing {
194            TooltipPosition::Right
195        } else if space_left >= tooltip_size.x + spacing {
196            TooltipPosition::Left
197        } else {
198            // Default to bottom if no space is sufficient
199            TooltipPosition::Bottom
200        }
201    }
202}
203
204/// Convenience function to show a tooltip on hover
205///
206/// This function shows a tooltip when the user hovers over the target element.
207///
208/// # Example
209/// ```rust
210/// # egui::__run_test_ui(|ui| {
211/// use egui_material3::{MaterialButton, show_tooltip_on_hover, TooltipPosition};
212///
213/// let response = ui.add(MaterialButton::filled("Hover me"));
214/// show_tooltip_on_hover(ui, &response, "Tooltip text", TooltipPosition::Top);
215/// # });
216/// ```
217pub fn show_tooltip_on_hover(
218    ui: &mut Ui,
219    target_response: &Response,
220    text: impl Into<String>,
221    position: TooltipPosition,
222) {
223    if target_response.hovered() {
224        MaterialTooltip::new(text).position(position).show(ui, target_response.rect);
225    }
226}
227
228/// Convenience function to show a tooltip with custom styling on hover
229pub fn show_tooltip_on_hover_custom(
230    ui: &mut Ui,
231    target_response: &Response,
232    tooltip: MaterialTooltip,
233) {
234    if target_response.hovered() {
235        tooltip.show(ui, target_response.rect);
236    }
237}
238
239/// Add a tooltip to any widget (builder pattern style)
240///
241/// This is a convenience wrapper that captures the response and shows a tooltip.
242///
243/// # Example
244/// ```rust
245/// # egui::__run_test_ui(|ui| {
246/// use egui_material3::{MaterialButton, with_tooltip, TooltipPosition};
247///
248/// let response = with_tooltip(ui, "Tooltip text", TooltipPosition::Top, |ui| {
249///     ui.add(MaterialButton::filled("Button with tooltip"))
250/// });
251/// # });
252/// ```
253pub fn with_tooltip<R>(
254    ui: &mut Ui,
255    _text: impl Into<String>,
256    _position: TooltipPosition,
257    add_contents: impl FnOnce(&mut Ui) -> R,
258) -> R {
259    // Note: This is a simplified helper. For proper tooltip support,
260    // use show_tooltip_on_hover with the response directly.
261    // This function is provided for API compatibility.
262    add_contents(ui)
263}
264
265/// Create a tooltip component
266pub fn tooltip(text: impl Into<String>) -> MaterialTooltip {
267    MaterialTooltip::new(text)
268}