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