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: Option<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 = if let Some(max_text_width) = style.max_text_width {
144                    Text::new(text, style.text)
145                        .constrained()
146                        .with_max_width(max_text_width)
147                } else {
148                    Text::new(text, style.text).constrained()
149                };
150
151                if measure {
152                    text.flex(1., false).boxed()
153                } else {
154                    text.flex(1., false).aligned().boxed()
155                }
156            })
157            .with_children(action.and_then(|action| {
158                let keystroke_label = KeystrokeLabel::new(
159                    window_id,
160                    focused_view_id?,
161                    action,
162                    style.keystroke.container,
163                    style.keystroke.text,
164                );
165                if measure {
166                    Some(keystroke_label.boxed())
167                } else {
168                    Some(keystroke_label.aligned().boxed())
169                }
170            }))
171            .contained()
172            .with_style(style.container)
173    }
174}
175
176impl Element for Tooltip {
177    type LayoutState = ();
178    type PaintState = ();
179
180    fn layout(
181        &mut self,
182        constraint: SizeConstraint,
183        cx: &mut LayoutContext,
184    ) -> (Vector2F, Self::LayoutState) {
185        let size = self.child.layout(constraint, cx);
186        if let Some(tooltip) = self.tooltip.as_mut() {
187            tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
188        }
189        (size, ())
190    }
191
192    fn paint(
193        &mut self,
194        bounds: RectF,
195        visible_bounds: RectF,
196        _: &mut Self::LayoutState,
197        cx: &mut PaintContext,
198    ) {
199        self.child.paint(bounds.origin(), visible_bounds, cx);
200        if let Some(tooltip) = self.tooltip.as_mut() {
201            tooltip.paint(bounds.origin(), visible_bounds, cx);
202        }
203    }
204
205    fn rect_for_text_range(
206        &self,
207        range: Range<usize>,
208        _: RectF,
209        _: RectF,
210        _: &Self::LayoutState,
211        _: &Self::PaintState,
212        cx: &MeasurementContext,
213    ) -> Option<RectF> {
214        self.child.rect_for_text_range(range, cx)
215    }
216
217    fn debug(
218        &self,
219        _: RectF,
220        _: &Self::LayoutState,
221        _: &Self::PaintState,
222        cx: &crate::DebugContext,
223    ) -> serde_json::Value {
224        json!({
225            "child": self.child.debug(cx),
226            "tooltip": self.tooltip.as_ref().map(|t| t.debug(cx)),
227        })
228    }
229}