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 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);
51const 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
60const 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 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}