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};
 18use util::ResultExt;
 19
 20const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
 21
 22pub struct Tooltip<V: View> {
 23    child: Element<V>,
 24    tooltip: Option<Element<V>>,
 25    _state: ElementStateHandle<Rc<TooltipState>>,
 26}
 27
 28#[derive(Default)]
 29struct TooltipState {
 30    visible: Cell<bool>,
 31    position: Cell<Vector2F>,
 32    debounce: RefCell<Option<Task<()>>>,
 33}
 34
 35#[derive(Clone, Deserialize, Default)]
 36pub struct TooltipStyle {
 37    #[serde(flatten)]
 38    pub container: ContainerStyle,
 39    pub text: TextStyle,
 40    keystroke: KeystrokeStyle,
 41    pub max_text_width: Option<f32>,
 42}
 43
 44#[derive(Clone, Deserialize, Default)]
 45pub struct KeystrokeStyle {
 46    #[serde(flatten)]
 47    container: ContainerStyle,
 48    #[serde(flatten)]
 49    text: TextStyle,
 50}
 51
 52impl<V: View> Tooltip<V> {
 53    pub fn new<Tag: 'static, T: View>(
 54        id: usize,
 55        text: String,
 56        action: Option<Box<dyn Action>>,
 57        style: TooltipStyle,
 58        child: Element<V>,
 59        cx: &mut ViewContext<V>,
 60    ) -> Self {
 61        struct ElementState<Tag>(Tag);
 62        struct MouseEventHandlerState<Tag>(Tag);
 63        let focused_view_id = cx.focused_view_id();
 64
 65        let state_handle = cx.default_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                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(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                if e.started {
 99                    if !state.visible.get() {
100                        state.position.set(position);
101
102                        let mut debounce = state.debounce.borrow_mut();
103                        if debounce.is_none() {
104                            *debounce = Some(cx.spawn_weak({
105                                let state = state.clone();
106                                |view, mut cx| async move {
107                                    cx.background().timer(DEBOUNCE_TIMEOUT).await;
108                                    state.visible.set(true);
109                                    if let Some(view) = view.upgrade(&cx) {
110                                        view.update(&mut cx, |_, cx| cx.notify()).log_err();
111                                    }
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        focused_view_id: Option<usize>,
132        text: String,
133        style: TooltipStyle,
134        action: Option<Box<dyn Action>>,
135        measure: bool,
136    ) -> impl Drawable<V> {
137        Flex::row()
138            .with_child({
139                let text = if let Some(max_text_width) = style.max_text_width {
140                    Text::new(text, style.text)
141                        .constrained()
142                        .with_max_width(max_text_width)
143                } else {
144                    Text::new(text, style.text).constrained()
145                };
146
147                if measure {
148                    text.flex(1., false).boxed()
149                } else {
150                    text.flex(1., false).aligned().boxed()
151                }
152            })
153            .with_children(action.and_then(|action| {
154                let keystroke_label = KeystrokeLabel::new(
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<V: View> Drawable<V> for Tooltip<V> {
172    type LayoutState = ();
173    type PaintState = ();
174
175    fn layout(
176        &mut self,
177        constraint: SizeConstraint,
178        view: &mut V,
179        cx: &mut ViewContext<V>,
180    ) -> (Vector2F, Self::LayoutState) {
181        let size = self.child.layout(constraint, view, cx);
182        if let Some(tooltip) = self.tooltip.as_mut() {
183            tooltip.layout(
184                SizeConstraint::new(Vector2F::zero(), cx.window_size()),
185                view,
186                cx,
187            );
188        }
189        (size, ())
190    }
191
192    fn paint(
193        &mut self,
194        scene: &mut SceneBuilder,
195        bounds: RectF,
196        visible_bounds: RectF,
197        _: &mut Self::LayoutState,
198        view: &mut V,
199        cx: &mut ViewContext<V>,
200    ) {
201        self.child
202            .paint(scene, bounds.origin(), visible_bounds, view, cx);
203        if let Some(tooltip) = self.tooltip.as_mut() {
204            tooltip.paint(scene, bounds.origin(), visible_bounds, view, cx);
205        }
206    }
207
208    fn rect_for_text_range(
209        &self,
210        range: Range<usize>,
211        _: RectF,
212        _: RectF,
213        _: &Self::LayoutState,
214        _: &Self::PaintState,
215        view: &V,
216        cx: &ViewContext<V>,
217    ) -> Option<RectF> {
218        self.child.rect_for_text_range(range, view, cx)
219    }
220
221    fn debug(
222        &self,
223        _: RectF,
224        _: &Self::LayoutState,
225        _: &Self::PaintState,
226        view: &V,
227        cx: &ViewContext<V>,
228    ) -> serde_json::Value {
229        json!({
230            "child": self.child.debug(view, cx),
231            "tooltip": self.tooltip.as_ref().map(|t| t.debug(view, cx)),
232        })
233    }
234}