tooltip.rs

  1use super::{
  2    ContainerStyle, Element, ElementBox, Flex, KeystrokeLabel, MouseEventHandler, Overlay,
  3    ParentElement, Text,
  4};
  5use crate::{
  6    fonts::TextStyle,
  7    geometry::{rect::RectF, vector::Vector2F},
  8    json::json,
  9    Action, Axis, ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint,
 10    Task, View,
 11};
 12use serde::Deserialize;
 13use std::{
 14    cell::{Cell, RefCell},
 15    rc::Rc,
 16    time::Duration,
 17};
 18
 19const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
 20
 21pub struct Tooltip {
 22    child: ElementBox,
 23    tooltip: Option<ElementBox>,
 24    _state: ElementStateHandle<Rc<TooltipState>>,
 25}
 26
 27#[derive(Default)]
 28struct TooltipState {
 29    visible: Cell<bool>,
 30    position: Cell<Vector2F>,
 31    debounce: RefCell<Option<Task<()>>>,
 32}
 33
 34#[derive(Clone, Deserialize, Default)]
 35pub struct TooltipStyle {
 36    #[serde(flatten)]
 37    container: ContainerStyle,
 38    text: TextStyle,
 39    keystroke: KeystrokeStyle,
 40    max_text_width: f32,
 41}
 42
 43#[derive(Clone, Deserialize, Default)]
 44pub struct KeystrokeStyle {
 45    #[serde(flatten)]
 46    container: ContainerStyle,
 47    #[serde(flatten)]
 48    text: TextStyle,
 49}
 50
 51impl Tooltip {
 52    pub fn new<T: View>(
 53        id: usize,
 54        text: String,
 55        action: Option<Box<dyn Action>>,
 56        style: TooltipStyle,
 57        child: ElementBox,
 58        cx: &mut RenderContext<T>,
 59    ) -> Self {
 60        let state_handle = cx.element_state::<TooltipState, Rc<TooltipState>>(id);
 61        let state = state_handle.read(cx).clone();
 62        let tooltip = if state.visible.get() {
 63            let mut collapsed_tooltip = Self::render_tooltip(
 64                text.clone(),
 65                style.clone(),
 66                action.as_ref().map(|a| a.boxed_clone()),
 67                true,
 68            )
 69            .boxed();
 70            Some(
 71                Overlay::new(
 72                    Self::render_tooltip(text, style, action, false)
 73                        .constrained()
 74                        .dynamically(move |constraint, cx| {
 75                            SizeConstraint::strict_along(
 76                                Axis::Vertical,
 77                                collapsed_tooltip.layout(constraint, cx).y(),
 78                            )
 79                        })
 80                        .boxed(),
 81                )
 82                .move_to_fit(true)
 83                .with_abs_position(state.position.get())
 84                .boxed(),
 85            )
 86        } else {
 87            None
 88        };
 89        let child = MouseEventHandler::new::<Self, _, _>(id, cx, |_, _| child)
 90            .on_hover(move |position, hover, cx| {
 91                let window_id = cx.window_id();
 92                if let Some(view_id) = cx.view_id() {
 93                    if hover {
 94                        if !state.visible.get() {
 95                            state.position.set(position);
 96
 97                            let mut debounce = state.debounce.borrow_mut();
 98                            if debounce.is_none() {
 99                                *debounce = Some(cx.spawn({
100                                    let state = state.clone();
101                                    |mut cx| async move {
102                                        cx.background().timer(DEBOUNCE_TIMEOUT).await;
103                                        state.visible.set(true);
104                                        cx.update(|cx| cx.notify_view(window_id, view_id));
105                                    }
106                                }));
107                            }
108                        }
109                    } else {
110                        state.visible.set(false);
111                        state.debounce.take();
112                    }
113                }
114            })
115            .boxed();
116        Self {
117            child,
118            tooltip,
119            _state: state_handle,
120        }
121    }
122
123    fn render_tooltip(
124        text: String,
125        style: TooltipStyle,
126        action: Option<Box<dyn Action>>,
127        measure: bool,
128    ) -> impl Element {
129        Flex::row()
130            .with_child({
131                let text = Text::new(text, style.text)
132                    .constrained()
133                    .with_max_width(style.max_text_width);
134                if measure {
135                    text.flex(1., false).boxed()
136                } else {
137                    text.flex(1., false).aligned().boxed()
138                }
139            })
140            .with_children(action.map(|action| {
141                let keystroke_label =
142                    KeystrokeLabel::new(action, style.keystroke.container, style.keystroke.text);
143                if measure {
144                    keystroke_label.boxed()
145                } else {
146                    keystroke_label.aligned().boxed()
147                }
148            }))
149            .contained()
150            .with_style(style.container)
151    }
152}
153
154impl Element for Tooltip {
155    type LayoutState = ();
156    type PaintState = ();
157
158    fn layout(
159        &mut self,
160        constraint: SizeConstraint,
161        cx: &mut LayoutContext,
162    ) -> (Vector2F, Self::LayoutState) {
163        let size = self.child.layout(constraint, cx);
164        if let Some(tooltip) = self.tooltip.as_mut() {
165            tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
166        }
167        (size, ())
168    }
169
170    fn paint(
171        &mut self,
172        bounds: RectF,
173        visible_bounds: RectF,
174        _: &mut Self::LayoutState,
175        cx: &mut PaintContext,
176    ) {
177        self.child.paint(bounds.origin(), visible_bounds, cx);
178        if let Some(tooltip) = self.tooltip.as_mut() {
179            tooltip.paint(bounds.origin(), visible_bounds, cx);
180        }
181    }
182
183    fn dispatch_event(
184        &mut self,
185        event: &crate::Event,
186        _: RectF,
187        _: RectF,
188        _: &mut Self::LayoutState,
189        _: &mut Self::PaintState,
190        cx: &mut crate::EventContext,
191    ) -> bool {
192        self.child.dispatch_event(event, cx)
193    }
194
195    fn debug(
196        &self,
197        _: RectF,
198        _: &Self::LayoutState,
199        _: &Self::PaintState,
200        cx: &crate::DebugContext,
201    ) -> serde_json::Value {
202        json!({
203            "child": self.child.debug(cx),
204            "tooltip": self.tooltip.as_ref().map(|t| t.debug(cx)),
205        })
206    }
207}