tooltip.rs

  1use std::{
  2    cell::{Cell, RefCell},
  3    rc::Rc,
  4    time::Duration,
  5};
  6
  7use super::{Element, ElementBox, MouseEventHandler};
  8use crate::{
  9    geometry::{rect::RectF, vector::Vector2F},
 10    json::json,
 11    ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint, Task, View,
 12};
 13
 14const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
 15
 16pub struct Tooltip {
 17    child: ElementBox,
 18    tooltip: Option<ElementBox>,
 19    state: ElementStateHandle<Rc<TooltipState>>,
 20}
 21
 22#[derive(Default)]
 23struct TooltipState {
 24    visible: Cell<bool>,
 25    position: Cell<Vector2F>,
 26    debounce: RefCell<Option<Task<()>>>,
 27}
 28
 29impl Tooltip {
 30    pub fn new<T: View>(
 31        id: usize,
 32        child: ElementBox,
 33        tooltip: ElementBox,
 34        cx: &mut RenderContext<T>,
 35    ) -> Self {
 36        let state_handle = cx.element_state::<TooltipState, Rc<TooltipState>>(id);
 37        let state = state_handle.read(cx).clone();
 38        let tooltip = if state.visible.get() {
 39            Some(tooltip)
 40        } else {
 41            None
 42        };
 43        let child = MouseEventHandler::new::<Self, _, _>(id, cx, |_, _| child)
 44            .on_hover(move |position, hover, cx| {
 45                let window_id = cx.window_id();
 46                if let Some(view_id) = cx.view_id() {
 47                    if hover {
 48                        if !state.visible.get() {
 49                            state.position.set(position);
 50
 51                            let mut debounce = state.debounce.borrow_mut();
 52                            if debounce.is_none() {
 53                                *debounce = Some(cx.spawn({
 54                                    let state = state.clone();
 55                                    |mut cx| async move {
 56                                        cx.background().timer(DEBOUNCE_TIMEOUT).await;
 57                                        state.visible.set(true);
 58                                        cx.update(|cx| cx.notify_view(window_id, view_id));
 59                                    }
 60                                }));
 61                            }
 62                        }
 63                    } else {
 64                        state.visible.set(false);
 65                        state.debounce.take();
 66                    }
 67                }
 68            })
 69            .boxed();
 70        Self {
 71            child,
 72            tooltip,
 73            state: state_handle,
 74        }
 75    }
 76}
 77
 78impl Element for Tooltip {
 79    type LayoutState = ();
 80    type PaintState = ();
 81
 82    fn layout(
 83        &mut self,
 84        constraint: SizeConstraint,
 85        cx: &mut LayoutContext,
 86    ) -> (Vector2F, Self::LayoutState) {
 87        let size = self.child.layout(constraint, cx);
 88        if let Some(tooltip) = self.tooltip.as_mut() {
 89            tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
 90        }
 91        (size, ())
 92    }
 93
 94    fn paint(
 95        &mut self,
 96        bounds: RectF,
 97        visible_bounds: RectF,
 98        _: &mut Self::LayoutState,
 99        cx: &mut PaintContext,
100    ) {
101        self.child.paint(bounds.origin(), visible_bounds, cx);
102        if let Some(tooltip) = self.tooltip.as_mut() {
103            let origin = self.state.read(cx).position.get();
104            let mut bounds = RectF::new(origin, tooltip.size());
105
106            // Align tooltip to the left if its bounds overflow the window width.
107            if bounds.lower_right().x() > cx.window_size.x() {
108                bounds.set_origin_x(bounds.origin_x() - bounds.width());
109            }
110
111            // Align tooltip to the top if its bounds overflow the window height.
112            if bounds.lower_right().y() > cx.window_size.y() {
113                bounds.set_origin_y(bounds.origin_y() - bounds.height());
114            }
115
116            cx.scene.push_stacking_context(None);
117            tooltip.paint(bounds.origin(), bounds, cx);
118            cx.scene.pop_stacking_context();
119        }
120    }
121
122    fn dispatch_event(
123        &mut self,
124        event: &crate::Event,
125        _: RectF,
126        _: RectF,
127        _: &mut Self::LayoutState,
128        _: &mut Self::PaintState,
129        cx: &mut crate::EventContext,
130    ) -> bool {
131        self.child.dispatch_event(event, cx)
132    }
133
134    fn debug(
135        &self,
136        _: RectF,
137        _: &Self::LayoutState,
138        _: &Self::PaintState,
139        cx: &crate::DebugContext,
140    ) -> serde_json::Value {
141        json!({
142            "child": self.child.debug(cx),
143            "tooltip": self.tooltip.as_ref().map(|t| t.debug(cx)),
144        })
145    }
146}