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                        cx.notify();
119                    }
120                }
121            })
122            .boxed();
123        Self {
124            child,
125            tooltip,
126            _state: state_handle,
127        }
128    }
129
130    pub fn render_tooltip(
131        text: String,
132        style: TooltipStyle,
133        action: Option<Box<dyn Action>>,
134        measure: bool,
135    ) -> impl Element {
136        Flex::row()
137            .with_child({
138                let text = Text::new(text, style.text)
139                    .constrained()
140                    .with_max_width(style.max_text_width);
141                if measure {
142                    text.flex(1., false).boxed()
143                } else {
144                    text.flex(1., false).aligned().boxed()
145                }
146            })
147            .with_children(action.map(|action| {
148                let keystroke_label =
149                    KeystrokeLabel::new(action, style.keystroke.container, style.keystroke.text);
150                if measure {
151                    keystroke_label.boxed()
152                } else {
153                    keystroke_label.aligned().boxed()
154                }
155            }))
156            .contained()
157            .with_style(style.container)
158    }
159}
160
161impl Element for Tooltip {
162    type LayoutState = ();
163    type PaintState = ();
164
165    fn layout(
166        &mut self,
167        constraint: SizeConstraint,
168        cx: &mut LayoutContext,
169    ) -> (Vector2F, Self::LayoutState) {
170        let size = self.child.layout(constraint, cx);
171        if let Some(tooltip) = self.tooltip.as_mut() {
172            tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
173        }
174        (size, ())
175    }
176
177    fn paint(
178        &mut self,
179        bounds: RectF,
180        visible_bounds: RectF,
181        _: &mut Self::LayoutState,
182        cx: &mut PaintContext,
183    ) {
184        self.child.paint(bounds.origin(), visible_bounds, cx);
185        if let Some(tooltip) = self.tooltip.as_mut() {
186            tooltip.paint(bounds.origin(), visible_bounds, cx);
187        }
188    }
189
190    fn rect_for_text_range(
191        &self,
192        range: Range<usize>,
193        _: RectF,
194        _: RectF,
195        _: &Self::LayoutState,
196        _: &Self::PaintState,
197        cx: &MeasurementContext,
198    ) -> Option<RectF> {
199        self.child.rect_for_text_range(range, cx)
200    }
201
202    fn debug(
203        &self,
204        _: RectF,
205        _: &Self::LayoutState,
206        _: &Self::PaintState,
207        cx: &crate::DebugContext,
208    ) -> serde_json::Value {
209        json!({
210            "child": self.child.debug(cx),
211            "tooltip": self.tooltip.as_ref().map(|t| t.debug(cx)),
212        })
213    }
214}