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