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