tooltip.rs

  1use super::{
  2    ContainerStyle, Drawable, Element, 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, SceneBuilder, SizeConstraint, Task, View, ViewContext,
 10};
 11use serde::Deserialize;
 12use std::{
 13    cell::{Cell, RefCell},
 14    ops::Range,
 15    rc::Rc,
 16    time::Duration,
 17};
 18
 19const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
 20
 21pub struct Tooltip<V: View> {
 22    child: Element<V>,
 23    tooltip: Option<Element<V>>,
 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    pub container: ContainerStyle,
 38    pub text: TextStyle,
 39    keystroke: KeystrokeStyle,
 40    pub max_text_width: Option<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<V: View> Tooltip<V> {
 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: Element<V>,
 58        cx: &mut ViewContext<V>,
 59    ) -> Self {
 60        struct ElementState<Tag>(Tag);
 61        struct MouseEventHandlerState<Tag>(Tag);
 62        let focused_view_id = cx.focused_view_id();
 63
 64        let state_handle = cx.default_element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
 65        let state = state_handle.read(cx).clone();
 66        let tooltip = if state.visible.get() {
 67            let mut collapsed_tooltip = Self::render_tooltip(
 68                focused_view_id,
 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(focused_view_id, text, style, action, false)
 78                        .constrained()
 79                        .dynamically(move |constraint, view, cx| {
 80                            SizeConstraint::strict_along(
 81                                Axis::Vertical,
 82                                collapsed_tooltip.layout(constraint, view, cx).y(),
 83                            )
 84                        })
 85                        .boxed(),
 86                )
 87                .with_fit_mode(OverlayFitMode::SwitchAnchor)
 88                .with_anchor_position(state.position.get())
 89                .boxed(),
 90            )
 91        } else {
 92            None
 93        };
 94        let child = MouseEventHandler::<MouseEventHandlerState<Tag>, _>::new(id, cx, |_, _| child)
 95            .on_hover(move |e, _, cx| {
 96                let position = e.position;
 97                let window_id = cx.window_id();
 98                let view_id = cx.view_id();
 99                if e.started {
100                    if !state.visible.get() {
101                        state.position.set(position);
102
103                        let mut debounce = state.debounce.borrow_mut();
104                        if debounce.is_none() {
105                            *debounce = Some(cx.spawn({
106                                let state = state.clone();
107                                |_, mut cx| async move {
108                                    cx.background().timer(DEBOUNCE_TIMEOUT).await;
109                                    state.visible.set(true);
110                                    cx.update(|cx| cx.notify_view(window_id, view_id));
111                                }
112                            }));
113                        }
114                    }
115                } else {
116                    state.visible.set(false);
117                    state.debounce.take();
118                    cx.notify();
119                }
120            })
121            .boxed();
122        Self {
123            child,
124            tooltip,
125            _state: state_handle,
126        }
127    }
128
129    pub fn render_tooltip(
130        focused_view_id: Option<usize>,
131        text: String,
132        style: TooltipStyle,
133        action: Option<Box<dyn Action>>,
134        measure: bool,
135    ) -> impl Drawable<V> {
136        Flex::row()
137            .with_child({
138                let text = if let Some(max_text_width) = style.max_text_width {
139                    Text::new(text, style.text)
140                        .constrained()
141                        .with_max_width(max_text_width)
142                } else {
143                    Text::new(text, style.text).constrained()
144                };
145
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                    focused_view_id?,
155                    action,
156                    style.keystroke.container,
157                    style.keystroke.text,
158                );
159                if measure {
160                    Some(keystroke_label.boxed())
161                } else {
162                    Some(keystroke_label.aligned().boxed())
163                }
164            }))
165            .contained()
166            .with_style(style.container)
167    }
168}
169
170impl<V: View> Drawable<V> for Tooltip<V> {
171    type LayoutState = ();
172    type PaintState = ();
173
174    fn layout(
175        &mut self,
176        constraint: SizeConstraint,
177        view: &mut V,
178        cx: &mut ViewContext<V>,
179    ) -> (Vector2F, Self::LayoutState) {
180        let size = self.child.layout(constraint, view, cx);
181        if let Some(tooltip) = self.tooltip.as_mut() {
182            tooltip.layout(
183                SizeConstraint::new(Vector2F::zero(), cx.window_size()),
184                view,
185                cx,
186            );
187        }
188        (size, ())
189    }
190
191    fn paint(
192        &mut self,
193        scene: &mut SceneBuilder,
194        bounds: RectF,
195        visible_bounds: RectF,
196        _: &mut Self::LayoutState,
197        view: &mut V,
198        cx: &mut ViewContext<V>,
199    ) {
200        self.child
201            .paint(scene, bounds.origin(), visible_bounds, view, cx);
202        if let Some(tooltip) = self.tooltip.as_mut() {
203            tooltip.paint(scene, bounds.origin(), visible_bounds, view, cx);
204        }
205    }
206
207    fn rect_for_text_range(
208        &self,
209        range: Range<usize>,
210        _: RectF,
211        _: RectF,
212        _: &Self::LayoutState,
213        _: &Self::PaintState,
214        view: &V,
215        cx: &ViewContext<V>,
216    ) -> Option<RectF> {
217        self.child.rect_for_text_range(range, view, cx)
218    }
219
220    fn debug(
221        &self,
222        _: RectF,
223        _: &Self::LayoutState,
224        _: &Self::PaintState,
225        view: &V,
226        cx: &ViewContext<V>,
227    ) -> serde_json::Value {
228        json!({
229            "child": self.child.debug(view, cx),
230            "tooltip": self.tooltip.as_ref().map(|t| t.debug(view, cx)),
231        })
232    }
233}