tooltip.rs

  1use super::{
  2    ContainerStyle, Element, ElementBox, Flex, KeystrokeLabel, MouseEventHandler, Overlay,
  3    OverlayFitMode, ParentElement, 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<Tag: 'static, 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        struct ElementState<Tag>(Tag);
 61        struct MouseEventHandlerState<Tag>(Tag);
 62
 63        let state_handle = cx.element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
 64        let state = state_handle.read(cx).clone();
 65        let tooltip = if state.visible.get() {
 66            let mut collapsed_tooltip = Self::render_tooltip(
 67                text.clone(),
 68                style.clone(),
 69                action.as_ref().map(|a| a.boxed_clone()),
 70                true,
 71            )
 72            .boxed();
 73            Some(
 74                Overlay::new(
 75                    Self::render_tooltip(text, style, action, false)
 76                        .constrained()
 77                        .dynamically(move |constraint, cx| {
 78                            SizeConstraint::strict_along(
 79                                Axis::Vertical,
 80                                collapsed_tooltip.layout(constraint, cx).y(),
 81                            )
 82                        })
 83                        .boxed(),
 84                )
 85                .fit_mode(OverlayFitMode::FlipAlignment)
 86                .with_abs_position(state.position.get())
 87                .boxed(),
 88            )
 89        } else {
 90            None
 91        };
 92        let child =
 93            MouseEventHandler::new::<MouseEventHandlerState<Tag>, _, _>(id, cx, |_, _| child)
 94                .on_hover(move |position, hover, cx| {
 95                    let window_id = cx.window_id();
 96                    if let Some(view_id) = cx.view_id() {
 97                        if hover {
 98                            if !state.visible.get() {
 99                                state.position.set(position);
100
101                                let mut debounce = state.debounce.borrow_mut();
102                                if debounce.is_none() {
103                                    *debounce = Some(cx.spawn({
104                                        let state = state.clone();
105                                        |mut cx| async move {
106                                            cx.background().timer(DEBOUNCE_TIMEOUT).await;
107                                            state.visible.set(true);
108                                            cx.update(|cx| cx.notify_view(window_id, view_id));
109                                        }
110                                    }));
111                                }
112                            }
113                        } else {
114                            state.visible.set(false);
115                            state.debounce.take();
116                        }
117                    }
118                })
119                .boxed();
120        Self {
121            child,
122            tooltip,
123            _state: state_handle,
124        }
125    }
126
127    fn render_tooltip(
128        text: String,
129        style: TooltipStyle,
130        action: Option<Box<dyn Action>>,
131        measure: bool,
132    ) -> impl Element {
133        Flex::row()
134            .with_child({
135                let text = Text::new(text, style.text)
136                    .constrained()
137                    .with_max_width(style.max_text_width);
138                if measure {
139                    text.flex(1., false).boxed()
140                } else {
141                    text.flex(1., false).aligned().boxed()
142                }
143            })
144            .with_children(action.map(|action| {
145                let keystroke_label =
146                    KeystrokeLabel::new(action, style.keystroke.container, style.keystroke.text);
147                if measure {
148                    keystroke_label.boxed()
149                } else {
150                    keystroke_label.aligned().boxed()
151                }
152            }))
153            .contained()
154            .with_style(style.container)
155    }
156}
157
158impl Element for Tooltip {
159    type LayoutState = ();
160    type PaintState = ();
161
162    fn layout(
163        &mut self,
164        constraint: SizeConstraint,
165        cx: &mut LayoutContext,
166    ) -> (Vector2F, Self::LayoutState) {
167        let size = self.child.layout(constraint, cx);
168        if let Some(tooltip) = self.tooltip.as_mut() {
169            tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
170        }
171        (size, ())
172    }
173
174    fn paint(
175        &mut self,
176        bounds: RectF,
177        visible_bounds: RectF,
178        _: &mut Self::LayoutState,
179        cx: &mut PaintContext,
180    ) {
181        self.child.paint(bounds.origin(), visible_bounds, cx);
182        if let Some(tooltip) = self.tooltip.as_mut() {
183            tooltip.paint(bounds.origin(), visible_bounds, cx);
184        }
185    }
186
187    fn dispatch_event(
188        &mut self,
189        event: &crate::Event,
190        _: RectF,
191        _: RectF,
192        _: &mut Self::LayoutState,
193        _: &mut Self::PaintState,
194        cx: &mut crate::EventContext,
195    ) -> bool {
196        self.child.dispatch_event(event, cx)
197    }
198
199    fn debug(
200        &self,
201        _: RectF,
202        _: &Self::LayoutState,
203        _: &Self::PaintState,
204        cx: &crate::DebugContext,
205    ) -> serde_json::Value {
206        json!({
207            "child": self.child.debug(cx),
208            "tooltip": self.tooltip.as_ref().map(|t| t.debug(cx)),
209        })
210    }
211}