tooltip.rs

  1use super::{
  2    ContainerStyle, Element, ElementBox, Flex, KeystrokeLabel, MouseEventHandler, Overlay,
  3    OverlayFitMode, ParentElement, Text,
  4};
  5use crate::{
  6    fonts::TextStyle,
  7    geometry::{rect::RectF, vector::Vector2F},
  8    json::json,
  9    presenter::MeasurementContext,
 10    Action, Axis, ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint,
 11    Task, View,
 12};
 13use serde::Deserialize;
 14use std::{
 15    cell::{Cell, RefCell},
 16    ops::Range,
 17    rc::Rc,
 18    time::Duration,
 19};
 20
 21const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
 22
 23pub struct Tooltip {
 24    child: ElementBox,
 25    tooltip: Option<ElementBox>,
 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: 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 Tooltip {
 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: ElementBox,
 60        cx: &mut RenderContext<T>,
 61    ) -> Self {
 62        struct ElementState<Tag>(Tag);
 63        struct MouseEventHandlerState<Tag>(Tag);
 64
 65        let state_handle = cx.default_element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
 66        let state = state_handle.read(cx).clone();
 67        let tooltip = if state.visible.get() {
 68            let mut collapsed_tooltip = Self::render_tooltip(
 69                text.clone(),
 70                style.clone(),
 71                action.as_ref().map(|a| a.boxed_clone()),
 72                true,
 73            )
 74            .boxed();
 75            Some(
 76                Overlay::new(
 77                    Self::render_tooltip(text, style, action, false)
 78                        .constrained()
 79                        .dynamically(move |constraint, cx| {
 80                            SizeConstraint::strict_along(
 81                                Axis::Vertical,
 82                                collapsed_tooltip.layout(constraint, cx).y(),
 83                            )
 84                        })
 85                        .boxed(),
 86                )
 87                .with_fit_mode(OverlayFitMode::SwitchAnchor)
 88                .with_anchor_position(state.position.get())
 89                .boxed(),
 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                let window_id = cx.window_id();
 98                if let Some(view_id) = cx.view_id() {
 99                    if e.started {
100                        if !state.visible.get() {
101                            state.position.set(position);
102
103                            let mut debounce = state.debounce.borrow_mut();
104                            if debounce.is_none() {
105                                *debounce = Some(cx.spawn({
106                                    let state = state.clone();
107                                    |mut cx| async move {
108                                        cx.background().timer(DEBOUNCE_TIMEOUT).await;
109                                        state.visible.set(true);
110                                        cx.update(|cx| cx.notify_view(window_id, view_id));
111                                    }
112                                }));
113                            }
114                        }
115                    } else {
116                        state.visible.set(false);
117                        state.debounce.take();
118                    }
119                }
120            })
121            .boxed();
122        Self {
123            child,
124            tooltip,
125            _state: state_handle,
126        }
127    }
128
129    pub fn render_tooltip(
130        text: String,
131        style: TooltipStyle,
132        action: Option<Box<dyn Action>>,
133        measure: bool,
134    ) -> impl Element {
135        Flex::row()
136            .with_child({
137                let text = Text::new(text, style.text)
138                    .constrained()
139                    .with_max_width(style.max_text_width);
140                if measure {
141                    text.flex(1., false).boxed()
142                } else {
143                    text.flex(1., false).aligned().boxed()
144                }
145            })
146            .with_children(action.map(|action| {
147                let keystroke_label =
148                    KeystrokeLabel::new(action, style.keystroke.container, style.keystroke.text);
149                if measure {
150                    keystroke_label.boxed()
151                } else {
152                    keystroke_label.aligned().boxed()
153                }
154            }))
155            .contained()
156            .with_style(style.container)
157    }
158}
159
160impl Element for Tooltip {
161    type LayoutState = ();
162    type PaintState = ();
163
164    fn layout(
165        &mut self,
166        constraint: SizeConstraint,
167        cx: &mut LayoutContext,
168    ) -> (Vector2F, Self::LayoutState) {
169        let size = self.child.layout(constraint, cx);
170        if let Some(tooltip) = self.tooltip.as_mut() {
171            tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
172        }
173        (size, ())
174    }
175
176    fn paint(
177        &mut self,
178        bounds: RectF,
179        visible_bounds: RectF,
180        _: &mut Self::LayoutState,
181        cx: &mut PaintContext,
182    ) {
183        self.child.paint(bounds.origin(), visible_bounds, cx);
184        if let Some(tooltip) = self.tooltip.as_mut() {
185            tooltip.paint(bounds.origin(), visible_bounds, cx);
186        }
187    }
188
189    fn rect_for_text_range(
190        &self,
191        range: Range<usize>,
192        _: RectF,
193        _: RectF,
194        _: &Self::LayoutState,
195        _: &Self::PaintState,
196        cx: &MeasurementContext,
197    ) -> Option<RectF> {
198        self.child.rect_for_text_range(range, cx)
199    }
200
201    fn debug(
202        &self,
203        _: RectF,
204        _: &Self::LayoutState,
205        _: &Self::PaintState,
206        cx: &crate::DebugContext,
207    ) -> serde_json::Value {
208        json!({
209            "child": self.child.debug(cx),
210            "tooltip": self.tooltip.as_ref().map(|t| t.debug(cx)),
211        })
212    }
213}