Skip to main content

scribl_widget/
modal.rs

1use std::sync::Arc;
2use std::time::{Duration, Instant};
3
4use druid::piet::FontFamily;
5use druid::widget::prelude::*;
6use druid::widget::LabelText;
7use druid::{
8    ArcStr, Color, Data, FontDescriptor, Point, Rect, Selector, SingleUse, TextLayout, TimerToken,
9    Vec2, WidgetPod,
10};
11
12pub struct ModalHost<T, W> {
13    mouse_pos: Point,
14    inner: W,
15    marker: std::marker::PhantomData<T>,
16    cur_tooltip: Option<(Point, ArcStr)>,
17    tooltip_layout: TextLayout<ArcStr>,
18    modal: Option<WidgetPod<T, Box<dyn Widget<T>>>>,
19}
20
21pub struct TooltipHost<T, W> {
22    text: LabelText<T>,
23    timer: TimerToken,
24    // If we are considering showing a tooltip, this will be the time of the last
25    // mouse move event.
26    last_mouse_move: Option<Instant>,
27    inner: WidgetPod<T, W>,
28}
29
30pub trait TooltipExt<T: Data, W: Widget<T>> {
31    fn tooltip<LT: Into<LabelText<T>>>(self, text: LT) -> TooltipHost<T, W>;
32}
33
34impl<T: Data, W: Widget<T> + 'static> TooltipExt<T, W> for W {
35    fn tooltip<LT: Into<LabelText<T>>>(self, text: LT) -> TooltipHost<T, W> {
36        TooltipHost {
37            text: text.into(),
38            timer: TimerToken::INVALID,
39            last_mouse_move: None,
40            inner: WidgetPod::new(self),
41        }
42    }
43}
44
45const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
46const TOOLTIP_DELAY_CHECK: Duration = Duration::from_millis(480);
47const TOOLTIP_COLOR: Color = crate::UI_LIGHT_YELLOW;
48const TOOLTIP_STROKE_WIDTH: f64 = 1.0;
49const TOOLTIP_STROKE_COLOR: Color = Color::rgb8(0, 0, 0);
50const TOOLTIP_TEXT_COLOR: Color = Color::rgb8(0, 0, 0);
51// It looks better if we don't put the tooltip *right* on the tip of the mouse,
52// because the mouse obstructs it.
53const TOOLTIP_OFFSET: Vec2 = Vec2::new(5.0, 5.0);
54
55const FONT_SIZE: f64 = 15.0;
56const LINE_HEIGHT_FACTOR: f64 = 1.7;
57const TEXT_TOP_GUESS_FACTOR: f64 = 0.15;
58const X_PADDING: f64 = 6.0;
59
60/// The argument is a string containing the tooltip text.
61const SHOW_TOOLTIP: Selector<ArcStr> = Selector::new("scribl.show-tooltip");
62
63impl<T: Data, W: Widget<T>> TooltipHost<T, W> {
64    pub fn child(&self) -> &W {
65        self.inner.widget()
66    }
67
68    pub fn child_mut(&mut self) -> &mut W {
69        self.inner.widget_mut()
70    }
71}
72
73impl<T: Data, W: Widget<T>> Widget<T> for TooltipHost<T, W> {
74    fn event(&mut self, ctx: &mut EventCtx, ev: &Event, data: &mut T, env: &Env) {
75        match ev {
76            Event::MouseDown(_) | Event::MouseUp(_) => {
77                self.timer = TimerToken::INVALID;
78                self.last_mouse_move = None;
79            }
80            Event::MouseMove(_) => {
81                self.last_mouse_move = if ctx.is_hot() {
82                    if self.timer == TimerToken::INVALID {
83                        self.timer = ctx.request_timer(TOOLTIP_DELAY);
84                    }
85                    Some(Instant::now())
86                } else {
87                    None
88                };
89            }
90            Event::Timer(tok) if tok == &self.timer => {
91                self.timer = TimerToken::INVALID;
92                if let Some(move_time) = self.last_mouse_move {
93                    let elapsed = Instant::now().duration_since(move_time);
94                    if elapsed > TOOLTIP_DELAY_CHECK {
95                        self.text.resolve(data, env);
96                        ctx.submit_command(SHOW_TOOLTIP.with(self.text.display_text()));
97                        self.timer = TimerToken::INVALID;
98                        self.last_mouse_move = None;
99                    } else {
100                        self.timer = ctx.request_timer(TOOLTIP_DELAY - elapsed);
101                    }
102                }
103            }
104            _ => {}
105        }
106        self.inner.event(ctx, ev, data, env);
107    }
108
109    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, ev: &LifeCycle, data: &T, env: &Env) {
110        self.inner.lifecycle(ctx, ev, data, env);
111    }
112
113    fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) {
114        self.inner.update(ctx, data, env);
115    }
116
117    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
118        let size = self.inner.layout(ctx, bc, data, env);
119        self.inner.set_origin(ctx, data, env, Point::ZERO);
120        ctx.set_paint_insets(self.inner.paint_insets());
121        size
122    }
123
124    fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
125        self.inner.paint(ctx, data, env);
126    }
127}
128
129impl ModalHost<(), ()> {
130    pub const DISMISS_MODAL: Selector<()> = Selector::new("scribl.dismiss-modal");
131}
132
133impl<T> ModalHost<T, ()> {
134    pub const SHOW_MODAL: Selector<SingleUse<Box<dyn Widget<T>>>> =
135        Selector::new("scribl.show-modal");
136}
137
138impl<T, W: Widget<T>> ModalHost<T, W> {
139    pub fn new(inner: W) -> ModalHost<T, W> {
140        ModalHost {
141            mouse_pos: Point::ZERO,
142            inner,
143            marker: std::marker::PhantomData,
144            cur_tooltip: None,
145            tooltip_layout: TextLayout::new(),
146            modal: None,
147        }
148    }
149}
150
151impl<T: Data, W: Widget<T>> Widget<T> for ModalHost<T, W> {
152    fn event(&mut self, ctx: &mut EventCtx, ev: &Event, data: &mut T, env: &Env) {
153        match ev {
154            Event::MouseMove(ev) => {
155                self.mouse_pos = ev.pos;
156            }
157            Event::Command(c) => {
158                if let Some(string) = c.get(SHOW_TOOLTIP) {
159                    self.cur_tooltip = Some((self.mouse_pos, Arc::clone(string)));
160                    ctx.request_paint();
161                    ctx.set_handled();
162                } else if let Some(modal) = c.get(ModalHost::SHOW_MODAL) {
163                    if self.modal.is_none() {
164                        self.modal = Some(WidgetPod::new(modal.take().unwrap()));
165                        ctx.children_changed();
166                    } else {
167                        log::warn!("already showing modal");
168                    }
169                    ctx.set_handled();
170                } else if c.is(ModalHost::DISMISS_MODAL) {
171                    if self.modal.is_some() {
172                        self.modal = None;
173                        ctx.children_changed();
174                    } else {
175                        log::warn!("not showing modal");
176                    }
177                    ctx.set_handled();
178                }
179            }
180            _ => {}
181        }
182
183        if is_user_input(ev) {
184            if self.cur_tooltip.is_some() {
185                self.cur_tooltip = None;
186                ctx.request_paint();
187            }
188
189            match self.modal.as_mut() {
190                Some(modal) => modal.event(ctx, ev, data, env),
191                None => self.inner.event(ctx, ev, data, env),
192            }
193        } else {
194            self.inner.event(ctx, ev, data, env)
195        }
196    }
197
198    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, ev: &LifeCycle, data: &T, env: &Env) {
199        if let Some(ref mut modal) = self.modal {
200            modal.lifecycle(ctx, ev, data, env);
201        }
202        self.inner.lifecycle(ctx, ev, data, env)
203    }
204
205    fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) {
206        if let Some(ref mut modal) = self.modal {
207            modal.update(ctx, data, env);
208        }
209        self.inner.update(ctx, old_data, data, env)
210    }
211
212    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
213        let size = self.inner.layout(ctx, bc, data, env);
214        if let Some(modal) = self.modal.as_mut() {
215            let modal_constraints = BoxConstraints::new(Size::ZERO, size);
216            let modal_size = modal.layout(ctx, &modal_constraints, data, env);
217            let modal_origin = (size.to_vec2() - modal_size.to_vec2()) / 2.0;
218            let modal_frame = Rect::from_origin_size(modal_origin.to_point(), modal_size);
219            modal.set_layout_rect(ctx, data, env, modal_frame);
220        }
221        size
222    }
223
224    fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
225        self.inner.paint(ctx, data, env);
226
227        if let Some(modal) = self.modal.as_mut() {
228            let frame = ctx.size().to_rect();
229            ctx.fill(frame, &Color::BLACK.with_alpha(0.45));
230            let modal_rect = modal.layout_rect() + Vec2::new(5.0, 5.0);
231            let blur_color = Color::grey8(100);
232            ctx.blurred_rect(modal_rect, 5.0, &blur_color);
233            modal.paint(ctx, data, env);
234        }
235
236        if let Some((point, string)) = &self.cur_tooltip {
237            let mut tooltip_origin = *point + TOOLTIP_OFFSET;
238            self.tooltip_layout
239                .set_font(FontDescriptor::new(FontFamily::SANS_SERIF).with_size(FONT_SIZE));
240            self.tooltip_layout.set_text(Arc::clone(string));
241            self.tooltip_layout.set_text_color(TOOLTIP_TEXT_COLOR);
242            self.tooltip_layout.rebuild_if_needed(&mut ctx.text(), env);
243            let line_height = FONT_SIZE * LINE_HEIGHT_FACTOR;
244            let size = ctx.size();
245            let text_size = self.tooltip_layout.size();
246
247            // If necessary, try to offset the tooltip so that it fits in the widget.
248            if tooltip_origin.y + line_height > size.height {
249                tooltip_origin.y = size.height - line_height;
250            }
251            if tooltip_origin.x + text_size.width > size.width {
252                tooltip_origin.x = size.width - text_size.width;
253            }
254
255            let rect = Rect::from_origin_size(
256                tooltip_origin,
257                (text_size.width + X_PADDING * 2.0, line_height),
258            )
259            .inset(-TOOLTIP_STROKE_WIDTH / 2.0)
260            .to_rounded_rect(env.get(druid::theme::BUTTON_BORDER_RADIUS));
261            let text_origin =
262                tooltip_origin + Vec2::new(X_PADDING, line_height * TEXT_TOP_GUESS_FACTOR);
263
264            ctx.fill(rect, &TOOLTIP_COLOR);
265            ctx.stroke(rect, &TOOLTIP_STROKE_COLOR, TOOLTIP_STROKE_WIDTH);
266            self.tooltip_layout.draw(ctx, text_origin);
267        }
268    }
269}
270
271fn is_user_input(event: &Event) -> bool {
272    match event {
273        Event::MouseUp(_)
274        | Event::MouseDown(_)
275        | Event::MouseMove(_)
276        | Event::KeyUp(_)
277        | Event::KeyDown(_)
278        | Event::Paste(_)
279        | Event::Wheel(_)
280        | Event::Zoom(_) => true,
281        _ => false,
282    }
283}