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