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    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: ElementBox<V>,
 23    tooltip: Option<ElementBox<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: ElementBox<V>,
 58        view: &mut 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                cx.window_id,
 70                focused_view_id,
 71                text.clone(),
 72                style.clone(),
 73                action.as_ref().map(|a| a.boxed_clone()),
 74                true,
 75            )
 76            .boxed();
 77            Some(
 78                Overlay::new(
 79                    Self::render_tooltip(cx.window_id, focused_view_id, text, style, action, false)
 80                        .constrained()
 81                        .dynamically(move |constraint, cx| {
 82                            SizeConstraint::strict_along(
 83                                Axis::Vertical,
 84                                collapsed_tooltip.layout(constraint, cx).y(),
 85                            )
 86                        })
 87                        .boxed(),
 88                )
 89                .with_fit_mode(OverlayFitMode::SwitchAnchor)
 90                .with_anchor_position(state.position.get())
 91                .boxed(),
 92            )
 93        } else {
 94            None
 95        };
 96        let child =
 97            MouseEventHandler::<MouseEventHandlerState<Tag>>::new(id, view, 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<V: View> Element<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}