tooltip.rs

  1use super::{
  2    AnyElement, ContainerStyle, 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, LayoutContext, SceneBuilder, SizeConstraint, Task, View,
 10    ViewContext,
 11};
 12use serde::Deserialize;
 13use std::{
 14    cell::{Cell, RefCell},
 15    ops::Range,
 16    rc::Rc,
 17    time::Duration,
 18};
 19use util::ResultExt;
 20
 21const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
 22
 23pub struct Tooltip<V: View> {
 24    child: AnyElement<V>,
 25    tooltip: Option<AnyElement<V>>,
 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<V: View> Tooltip<V> {
 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: AnyElement<V>,
 60        cx: &mut ViewContext<V>,
 61    ) -> Self {
 62        struct ElementState<Tag>(Tag);
 63        struct MouseEventHandlerState<Tag>(Tag);
 64        let focused_view_id = cx.focused_view_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                focused_view_id,
 71                text.clone(),
 72                style.clone(),
 73                action.as_ref().map(|a| a.boxed_clone()),
 74                true,
 75            );
 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).0.y(),
 84                            )
 85                        }),
 86                )
 87                .with_fit_mode(OverlayFitMode::SwitchAnchor)
 88                .with_anchor_position(state.position.get())
 89                .into_any(),
 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                if e.started {
 98                    if !state.visible.get() {
 99                        state.position.set(position);
100
101                        let mut debounce = state.debounce.borrow_mut();
102                        if debounce.is_none() {
103                            *debounce = Some(cx.spawn({
104                                let state = state.clone();
105                                |view, mut cx| async move {
106                                    cx.background().timer(DEBOUNCE_TIMEOUT).await;
107                                    state.visible.set(true);
108                                    view.update(&mut cx, |_, cx| cx.notify()).log_err();
109                                }
110                            }));
111                        }
112                    }
113                } else {
114                    state.visible.set(false);
115                    state.debounce.take();
116                    cx.notify();
117                }
118            })
119            .into_any();
120        Self {
121            child,
122            tooltip,
123            _state: state_handle,
124        }
125    }
126
127    pub fn render_tooltip(
128        focused_view_id: Option<usize>,
129        text: String,
130        style: TooltipStyle,
131        action: Option<Box<dyn Action>>,
132        measure: bool,
133    ) -> impl Element<V> {
134        Flex::row()
135            .with_child({
136                let text = if let Some(max_text_width) = style.max_text_width {
137                    Text::new(text, style.text)
138                        .constrained()
139                        .with_max_width(max_text_width)
140                } else {
141                    Text::new(text, style.text).constrained()
142                };
143
144                if measure {
145                    text.flex(1., false).into_any()
146                } else {
147                    text.flex(1., false).aligned().into_any()
148                }
149            })
150            .with_children(action.and_then(|action| {
151                let keystroke_label = KeystrokeLabel::new(
152                    focused_view_id?,
153                    action,
154                    style.keystroke.container,
155                    style.keystroke.text,
156                );
157                if measure {
158                    Some(keystroke_label.into_any())
159                } else {
160                    Some(keystroke_label.aligned().into_any())
161                }
162            }))
163            .contained()
164            .with_style(style.container)
165    }
166}
167
168impl<V: View> Element<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 LayoutContext<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}