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