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 schemars::JsonSchema;
 13use serde::Deserialize;
 14use std::{
 15    cell::{Cell, RefCell},
 16    ops::Range,
 17    rc::Rc,
 18    time::Duration,
 19};
 20use util::ResultExt;
 21
 22const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
 23
 24pub struct Tooltip<V: View> {
 25    child: AnyElement<V>,
 26    tooltip: Option<AnyElement<V>>,
 27    _state: ElementStateHandle<Rc<TooltipState>>,
 28}
 29
 30#[derive(Default)]
 31struct TooltipState {
 32    visible: Cell<bool>,
 33    position: Cell<Vector2F>,
 34    debounce: RefCell<Option<Task<()>>>,
 35}
 36
 37#[derive(Clone, Deserialize, Default, JsonSchema)]
 38pub struct TooltipStyle {
 39    #[serde(flatten)]
 40    pub container: ContainerStyle,
 41    pub text: TextStyle,
 42    keystroke: KeystrokeStyle,
 43    pub max_text_width: Option<f32>,
 44}
 45
 46#[derive(Clone, Deserialize, Default, JsonSchema)]
 47pub struct KeystrokeStyle {
 48    #[serde(flatten)]
 49    container: ContainerStyle,
 50    #[serde(flatten)]
 51    text: TextStyle,
 52}
 53
 54impl<V: View> Tooltip<V> {
 55    pub fn new<Tag: 'static, T: View>(
 56        id: usize,
 57        text: String,
 58        action: Option<Box<dyn Action>>,
 59        style: TooltipStyle,
 60        child: AnyElement<V>,
 61        cx: &mut ViewContext<V>,
 62    ) -> Self {
 63        struct ElementState<Tag>(Tag);
 64        struct MouseEventHandlerState<Tag>(Tag);
 65        let focused_view_id = cx.focused_view_id();
 66
 67        let state_handle = cx.default_element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
 68        let state = state_handle.read(cx).clone();
 69        let tooltip = if state.visible.get() {
 70            let mut collapsed_tooltip = Self::render_tooltip(
 71                focused_view_id,
 72                text.clone(),
 73                style.clone(),
 74                action.as_ref().map(|a| a.boxed_clone()),
 75                true,
 76            );
 77            Some(
 78                Overlay::new(
 79                    Self::render_tooltip(focused_view_id, text, style, action, false)
 80                        .constrained()
 81                        .dynamically(move |constraint, view, cx| {
 82                            SizeConstraint::strict_along(
 83                                Axis::Vertical,
 84                                collapsed_tooltip.layout(constraint, view, cx).0.y(),
 85                            )
 86                        }),
 87                )
 88                .with_fit_mode(OverlayFitMode::SwitchAnchor)
 89                .with_anchor_position(state.position.get())
 90                .into_any(),
 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({
105                                let state = state.clone();
106                                |view, mut cx| async move {
107                                    cx.background().timer(DEBOUNCE_TIMEOUT).await;
108                                    state.visible.set(true);
109                                    view.update(&mut cx, |_, cx| cx.notify()).log_err();
110                                }
111                            }));
112                        }
113                    }
114                } else {
115                    state.visible.set(false);
116                    state.debounce.take();
117                    cx.notify();
118                }
119            })
120            .into_any();
121        Self {
122            child,
123            tooltip,
124            _state: state_handle,
125        }
126    }
127
128    pub fn render_tooltip(
129        focused_view_id: Option<usize>,
130        text: String,
131        style: TooltipStyle,
132        action: Option<Box<dyn Action>>,
133        measure: bool,
134    ) -> impl Element<V> {
135        Flex::row()
136            .with_child({
137                let text = if let Some(max_text_width) = style.max_text_width {
138                    Text::new(text, style.text)
139                        .constrained()
140                        .with_max_width(max_text_width)
141                } else {
142                    Text::new(text, style.text).constrained()
143                };
144
145                if measure {
146                    text.flex(1., false).into_any()
147                } else {
148                    text.flex(1., false).aligned().into_any()
149                }
150            })
151            .with_children(action.and_then(|action| {
152                let keystroke_label = KeystrokeLabel::new(
153                    focused_view_id?,
154                    action,
155                    style.keystroke.container,
156                    style.keystroke.text,
157                );
158                if measure {
159                    Some(keystroke_label.into_any())
160                } else {
161                    Some(keystroke_label.aligned().into_any())
162                }
163            }))
164            .contained()
165            .with_style(style.container)
166    }
167}
168
169impl<V: View> Element<V> for Tooltip<V> {
170    type LayoutState = ();
171    type PaintState = ();
172
173    fn layout(
174        &mut self,
175        constraint: SizeConstraint,
176        view: &mut V,
177        cx: &mut LayoutContext<V>,
178    ) -> (Vector2F, Self::LayoutState) {
179        let size = self.child.layout(constraint, view, cx);
180        if let Some(tooltip) = self.tooltip.as_mut() {
181            tooltip.layout(
182                SizeConstraint::new(Vector2F::zero(), cx.window_size()),
183                view,
184                cx,
185            );
186        }
187        (size, ())
188    }
189
190    fn paint(
191        &mut self,
192        scene: &mut SceneBuilder,
193        bounds: RectF,
194        visible_bounds: RectF,
195        _: &mut Self::LayoutState,
196        view: &mut V,
197        cx: &mut ViewContext<V>,
198    ) {
199        self.child
200            .paint(scene, bounds.origin(), visible_bounds, view, cx);
201        if let Some(tooltip) = self.tooltip.as_mut() {
202            tooltip.paint(scene, bounds.origin(), visible_bounds, view, cx);
203        }
204    }
205
206    fn rect_for_text_range(
207        &self,
208        range: Range<usize>,
209        _: RectF,
210        _: RectF,
211        _: &Self::LayoutState,
212        _: &Self::PaintState,
213        view: &V,
214        cx: &ViewContext<V>,
215    ) -> Option<RectF> {
216        self.child.rect_for_text_range(range, view, cx)
217    }
218
219    fn debug(
220        &self,
221        _: RectF,
222        _: &Self::LayoutState,
223        _: &Self::PaintState,
224        view: &V,
225        cx: &ViewContext<V>,
226    ) -> serde_json::Value {
227        json!({
228            "child": self.child.debug(view, cx),
229            "tooltip": self.tooltip.as_ref().map(|t| t.debug(view, cx)),
230        })
231    }
232}