tooltip.rs

  1use super::{
  2    ContainerStyle, Element, ElementBox, Flex, KeystrokeLabel, MouseEventHandler, ParentElement,
  3    Text,
  4};
  5use crate::{
  6    fonts::TextStyle,
  7    geometry::{rect::RectF, vector::Vector2F},
  8    json::json,
  9    Action, Axis, ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint,
 10    Task, View,
 11};
 12use serde::Deserialize;
 13use std::{
 14    cell::{Cell, RefCell},
 15    rc::Rc,
 16    time::Duration,
 17};
 18
 19const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
 20
 21pub struct Tooltip {
 22    child: ElementBox,
 23    tooltip: Option<ElementBox>,
 24    state: ElementStateHandle<Rc<TooltipState>>,
 25}
 26
 27#[derive(Default)]
 28struct TooltipState {
 29    visible: Cell<bool>,
 30    position: Cell<Vector2F>,
 31    debounce: RefCell<Option<Task<()>>>,
 32}
 33
 34#[derive(Clone, Deserialize, Default)]
 35pub struct TooltipStyle {
 36    #[serde(flatten)]
 37    container: ContainerStyle,
 38    text: TextStyle,
 39    keystroke: KeystrokeStyle,
 40    max_text_width: f32,
 41}
 42
 43#[derive(Clone, Deserialize, Default)]
 44pub struct KeystrokeStyle {
 45    #[serde(flatten)]
 46    container: ContainerStyle,
 47    #[serde(flatten)]
 48    text: TextStyle,
 49}
 50
 51impl Tooltip {
 52    pub fn new<T: View>(
 53        id: usize,
 54        text: String,
 55        action: Option<Box<dyn Action>>,
 56        style: TooltipStyle,
 57        child: ElementBox,
 58        cx: &mut RenderContext<T>,
 59    ) -> Self {
 60        let state_handle = cx.element_state::<TooltipState, Rc<TooltipState>>(id);
 61        let state = state_handle.read(cx).clone();
 62        let tooltip = if state.visible.get() {
 63            let mut collapsed_tooltip = Self::render_tooltip(
 64                text.clone(),
 65                style.clone(),
 66                action.as_ref().map(|a| a.boxed_clone()),
 67                true,
 68            )
 69            .boxed();
 70            Some(
 71                Self::render_tooltip(text, style, action, false)
 72                    .constrained()
 73                    .dynamically(move |constraint, cx| {
 74                        SizeConstraint::strict_along(
 75                            Axis::Vertical,
 76                            collapsed_tooltip.layout(constraint, cx).y(),
 77                        )
 78                    })
 79                    .boxed(),
 80            )
 81        } else {
 82            None
 83        };
 84        let child = MouseEventHandler::new::<Self, _, _>(id, cx, |_, _| child)
 85            .on_hover(move |position, hover, cx| {
 86                let window_id = cx.window_id();
 87                if let Some(view_id) = cx.view_id() {
 88                    if hover {
 89                        if !state.visible.get() {
 90                            state.position.set(position);
 91
 92                            let mut debounce = state.debounce.borrow_mut();
 93                            if debounce.is_none() {
 94                                *debounce = Some(cx.spawn({
 95                                    let state = state.clone();
 96                                    |mut cx| async move {
 97                                        cx.background().timer(DEBOUNCE_TIMEOUT).await;
 98                                        state.visible.set(true);
 99                                        cx.update(|cx| cx.notify_view(window_id, view_id));
100                                    }
101                                }));
102                            }
103                        }
104                    } else {
105                        state.visible.set(false);
106                        state.debounce.take();
107                    }
108                }
109            })
110            .boxed();
111        Self {
112            child,
113            tooltip,
114            state: state_handle,
115        }
116    }
117
118    fn render_tooltip(
119        text: String,
120        style: TooltipStyle,
121        action: Option<Box<dyn Action>>,
122        measure: bool,
123    ) -> impl Element {
124        Flex::row()
125            .with_child({
126                let text = Text::new(text, style.text)
127                    .constrained()
128                    .with_max_width(style.max_text_width);
129                if measure {
130                    text.flex(1., false).boxed()
131                } else {
132                    text.flex(1., false).aligned().boxed()
133                }
134            })
135            .with_children(action.map(|action| {
136                let keystroke_label =
137                    KeystrokeLabel::new(action, style.keystroke.container, style.keystroke.text);
138                if measure {
139                    keystroke_label.boxed()
140                } else {
141                    keystroke_label.aligned().boxed()
142                }
143            }))
144            .contained()
145            .with_style(style.container)
146    }
147}
148
149impl Element for Tooltip {
150    type LayoutState = ();
151    type PaintState = ();
152
153    fn layout(
154        &mut self,
155        constraint: SizeConstraint,
156        cx: &mut LayoutContext,
157    ) -> (Vector2F, Self::LayoutState) {
158        let size = self.child.layout(constraint, cx);
159        if let Some(tooltip) = self.tooltip.as_mut() {
160            tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
161        }
162        (size, ())
163    }
164
165    fn paint(
166        &mut self,
167        bounds: RectF,
168        visible_bounds: RectF,
169        _: &mut Self::LayoutState,
170        cx: &mut PaintContext,
171    ) {
172        self.child.paint(bounds.origin(), visible_bounds, cx);
173        if let Some(tooltip) = self.tooltip.as_mut() {
174            let origin = self.state.read(cx).position.get();
175            let mut bounds = RectF::new(origin, tooltip.size());
176
177            // Align tooltip to the left if its bounds overflow the window width.
178            if bounds.lower_right().x() > cx.window_size.x() {
179                bounds.set_origin_x(bounds.origin_x() - bounds.width());
180            }
181
182            // Align tooltip to the top if its bounds overflow the window height.
183            if bounds.lower_right().y() > cx.window_size.y() {
184                bounds.set_origin_y(bounds.origin_y() - bounds.height());
185            }
186
187            cx.scene.push_stacking_context(None);
188            tooltip.paint(bounds.origin(), bounds, cx);
189            cx.scene.pop_stacking_context();
190        }
191    }
192
193    fn dispatch_event(
194        &mut self,
195        event: &crate::Event,
196        _: RectF,
197        _: RectF,
198        _: &mut Self::LayoutState,
199        _: &mut Self::PaintState,
200        cx: &mut crate::EventContext,
201    ) -> bool {
202        self.child.dispatch_event(event, cx)
203    }
204
205    fn debug(
206        &self,
207        _: RectF,
208        _: &Self::LayoutState,
209        _: &Self::PaintState,
210        cx: &crate::DebugContext,
211    ) -> serde_json::Value {
212        json!({
213            "child": self.child.debug(cx),
214            "tooltip": self.tooltip.as_ref().map(|t| t.debug(cx)),
215        })
216    }
217}