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: 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                cx.window_id,
 69                focused_view_id,
 70                text.clone(),
 71                style.clone(),
 72                action.as_ref().map(|a| a.boxed_clone()),
 73                true,
 74            )
 75            .boxed();
 76            Some(
 77                Overlay::new(
 78                    Self::render_tooltip(cx.window_id, focused_view_id, text, style, action, false)
 79                        .constrained()
 80                        .dynamically(move |constraint, view, cx| {
 81                            SizeConstraint::strict_along(
 82                                Axis::Vertical,
 83                                collapsed_tooltip.layout(constraint, view, cx).y(),
 84                            )
 85                        })
 86                        .boxed(),
 87                )
 88                .with_fit_mode(OverlayFitMode::SwitchAnchor)
 89                .with_anchor_position(state.position.get())
 90                .boxed(),
 91            )
 92        } else {
 93            None
 94        };
 95        let child = MouseEventHandler::<MouseEventHandlerState<Tag>, _>::new(id, cx, |_, _| child)
 96            .on_hover(move |e, _, cx| {
 97                let position = e.position;
 98                let window_id = cx.window_id();
 99                let 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                    cx.notify();
120                }
121            })
122            .boxed();
123        Self {
124            child,
125            tooltip,
126            _state: state_handle,
127        }
128    }
129
130    pub fn render_tooltip(
131        window_id: usize,
132        focused_view_id: Option<usize>,
133        text: String,
134        style: TooltipStyle,
135        action: Option<Box<dyn Action>>,
136        measure: bool,
137    ) -> impl Drawable<V> {
138        Flex::row()
139            .with_child({
140                let text = Text::new(text, style.text)
141                    .constrained()
142                    .with_max_width(style.max_text_width);
143                if measure {
144                    text.flex(1., false).boxed()
145                } else {
146                    text.flex(1., false).aligned().boxed()
147                }
148            })
149            .with_children(action.and_then(|action| {
150                let keystroke_label = KeystrokeLabel::new(
151                    window_id,
152                    focused_view_id?,
153                    action,
154                    style.keystroke.container,
155                    style.keystroke.text,
156                );
157                if measure {
158                    Some(keystroke_label.boxed())
159                } else {
160                    Some(keystroke_label.aligned().boxed())
161                }
162            }))
163            .contained()
164            .with_style(style.container)
165    }
166}
167
168impl<V: View> Drawable<V> for Tooltip<V> {
169    type LayoutState = ();
170    type PaintState = ();
171
172    fn layout(
173        &mut self,
174        constraint: SizeConstraint,
175        view: &mut V,
176        cx: &mut ViewContext<V>,
177    ) -> (Vector2F, Self::LayoutState) {
178        let size = self.child.layout(constraint, view, cx);
179        if let Some(tooltip) = self.tooltip.as_mut() {
180            tooltip.layout(
181                SizeConstraint::new(Vector2F::zero(), cx.window_size()),
182                view,
183                cx,
184            );
185        }
186        (size, ())
187    }
188
189    fn paint(
190        &mut self,
191        scene: &mut SceneBuilder,
192        bounds: RectF,
193        visible_bounds: RectF,
194        _: &mut Self::LayoutState,
195        view: &mut V,
196        cx: &mut ViewContext<V>,
197    ) {
198        self.child
199            .paint(scene, bounds.origin(), visible_bounds, view, cx);
200        if let Some(tooltip) = self.tooltip.as_mut() {
201            tooltip.paint(scene, bounds.origin(), visible_bounds, view, 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        view: &V,
213        cx: &ViewContext<V>,
214    ) -> Option<RectF> {
215        self.child.rect_for_text_range(range, view, cx)
216    }
217
218    fn debug(
219        &self,
220        _: RectF,
221        _: &Self::LayoutState,
222        _: &Self::PaintState,
223        view: &V,
224        cx: &ViewContext<V>,
225    ) -> serde_json::Value {
226        json!({
227            "child": self.child.debug(view, cx),
228            "tooltip": self.tooltip.as_ref().map(|t| t.debug(view, cx)),
229        })
230    }
231}