1use std::{
18 cell::RefCell,
19 convert::{TryFrom, TryInto},
20 rc::Rc,
21 sync::Arc,
22};
23
24use crate::{Stack, StackChildParams, StackChildPosition};
25use druid::{
26 piet::{Text, TextAttribute, TextLayoutBuilder, TextStorage},
27 text::{Attribute, RichText},
28 widget::{
29 DefaultScopePolicy, Either, Label, LensScopeTransfer, RawLabel, Scope, SizedBox,
30 WidgetWrapper,
31 },
32 Color, Data, KeyOrValue, Lens, Point, RenderContext, Selector, SingleUse, Size, Widget,
33 WidgetExt, WidgetId, WidgetPod,
34};
35
36const FORWARD: Selector<SingleUse<(WidgetId, Point)>> = Selector::new("tooltip.forward");
37const POINT_UPDATED: Selector = Selector::new("tooltip.label.point_updated");
38pub(crate) const ADVISE_TOOLTIP_SHOW: Selector<Point> =
39 Selector::new("tooltip.advise_show_tooltip");
40pub(crate) const CANCEL_TOOLTIP_SHOW: Selector = Selector::new("tooltip.cancel_show_tooltip");
41
42type StackTooltipActual<T> = Scope<
43 DefaultScopePolicy<
44 fn(T) -> TooltipState<T>,
45 LensScopeTransfer<tooltip_state_derived_lenses::data<T>, T, TooltipState<T>>,
46 >,
47 StackTooltipInternal<T>,
48>;
49
50pub struct StackTooltip<T: Data>(StackTooltipActual<T>);
51
52impl<T: Data> StackTooltip<T> {
53 pub fn new<W: Widget<T> + 'static>(widget: W, label: impl Into<PlainOrRich>) -> Self {
54 Self(StackTooltipInternal::new(widget, label))
55 }
56
57 pub fn set_text_attribute(&mut self, attribute: Attribute) {
58 self.0.wrapped_mut().set_text_attribute(attribute);
59 }
60
61 pub fn with_text_attribute(mut self, attribute: Attribute) -> Self {
62 self.set_text_attribute(attribute);
63
64 self
65 }
66
67 pub fn set_background_color(&mut self, color: impl Into<KeyOrValue<Color>>) {
68 self.0.wrapped_mut().set_background_color(color)
69 }
70
71 pub fn with_background_color(mut self, color: impl Into<KeyOrValue<Color>>) -> Self {
72 self.set_background_color(color);
73
74 self
75 }
76
77 pub fn set_border_width(&mut self, width: f64) {
78 self.0.wrapped_mut().set_border_width(width)
79 }
80
81 pub fn with_border_width(mut self, width: f64) -> Self {
82 self.set_border_width(width);
83
84 self
85 }
86
87 pub fn set_border_color(&mut self, color: impl Into<KeyOrValue<Color>>) {
88 self.0.wrapped_mut().set_border_color(color);
89 }
90
91 pub fn with_border_color(mut self, color: impl Into<KeyOrValue<Color>>) -> Self {
92 self.set_border_color(color);
93
94 self
95 }
96
97 pub fn set_crosshair(&mut self, crosshair: bool) {
98 self.0.wrapped_mut().set_crosshair(crosshair)
99 }
100
101 pub fn with_crosshair(mut self, crosshair: bool) -> Self {
102 self.set_crosshair(crosshair);
103
104 self
105 }
106}
107
108impl<T: Data> Widget<T> for StackTooltip<T> {
109 fn event(
110 &mut self,
111 ctx: &mut druid::EventCtx,
112 event: &druid::Event,
113 data: &mut T,
114 env: &druid::Env,
115 ) {
116 self.0.event(ctx, event, data, env)
117 }
118
119 fn lifecycle(
120 &mut self,
121 ctx: &mut druid::LifeCycleCtx,
122 event: &druid::LifeCycle,
123 data: &T,
124 env: &druid::Env,
125 ) {
126 self.0.lifecycle(ctx, event, data, env)
127 }
128
129 fn update(&mut self, ctx: &mut druid::UpdateCtx, old_data: &T, data: &T, env: &druid::Env) {
130 self.0.update(ctx, old_data, data, env)
131 }
132
133 fn layout(
134 &mut self,
135 ctx: &mut druid::LayoutCtx,
136 bc: &druid::BoxConstraints,
137 data: &T,
138 env: &druid::Env,
139 ) -> Size {
140 self.0.layout(ctx, bc, data, env)
141 }
142
143 fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &T, env: &druid::Env) {
144 self.0.paint(ctx, data, env)
145 }
146}
147
148#[derive(Clone, Data, Lens)]
149struct TooltipState<T> {
150 data: T,
151 show: bool,
152 position: StackChildPosition,
153 label_size: Option<Size>,
154}
155
156type RichTextCell = Rc<RefCell<(RichText, Vec<YetAnotherAttribute>)>>;
157type BackgroundCell = Rc<RefCell<Option<KeyOrValue<Color>>>>;
158type BorderCell = Rc<RefCell<(Option<KeyOrValue<Color>>, Option<f64>)>>;
159
160struct StackTooltipInternal<T> {
161 widget: WidgetPod<TooltipState<T>, Stack<TooltipState<T>>>,
162 label_id: Option<WidgetId>,
163 text: RichTextCell,
164 background: BackgroundCell,
165 border: BorderCell,
166 use_crosshair: bool,
167}
168
169fn make_state<T: Data>(data: T) -> TooltipState<T> {
170 TooltipState {
171 data,
172 show: false,
173 position: StackChildPosition::new().height(Some(0.0)),
174 label_size: None,
175 }
176}
177
178impl<T: Data> StackTooltipInternal<T> {
179 fn new<W: Widget<T> + 'static>(
180 widget: W,
181 label: impl Into<PlainOrRich>,
182 ) -> StackTooltipActual<T> {
183 let rich_text = match label.into() {
184 PlainOrRich::Plain(plain) => RichText::new(plain.into()),
185 PlainOrRich::Rich(rich) => rich,
186 };
187 let attrs = vec![];
188
189 let text = Rc::new(RefCell::new((rich_text, attrs)));
190 let background = BackgroundCell::default();
191 let border = BorderCell::default();
192 let label_id = WidgetId::next();
193 let stack = Stack::new()
194 .with_child(widget.lens(TooltipState::data))
195 .with_positioned_child(
196 Either::new(
197 |state: &TooltipState<T>, _| state.show && is_some_position(&state.position),
198 TooltipLabel::new(text.clone(), label_id, background.clone(), border.clone()),
199 SizedBox::empty(),
200 ),
201 StackChildParams::dynamic(|TooltipState { position, .. }: &TooltipState<T>, _| {
202 position
203 }),
204 );
205
206 Scope::from_lens(
207 make_state as fn(T) -> TooltipState<T>,
208 TooltipState::data,
209 Self {
210 widget: WidgetPod::new(stack),
211 label_id: Some(label_id),
212 text,
213 background,
214 border,
215 use_crosshair: false,
216 },
217 )
218 }
219
220 pub fn set_text_attribute(&mut self, attribute: Attribute) {
221 self.text
222 .borrow_mut()
223 .0
224 .add_attribute(0.., attribute.clone());
225 match attribute.try_into() {
226 Ok(attr) => self.text.borrow_mut().1.push(attr),
227 Err(attrs) => self.text.borrow_mut().1.extend(attrs),
228 };
229 }
230
231 pub fn set_background_color(&mut self, color: impl Into<KeyOrValue<Color>>) {
232 self.background.borrow_mut().replace(color.into());
233 }
234
235 pub fn set_border_width(&mut self, width: f64) {
236 self.border.borrow_mut().1.replace(width);
237 }
238
239 pub fn set_border_color(&mut self, color: impl Into<KeyOrValue<Color>>) {
240 self.border.borrow_mut().0.replace(color.into());
241 }
242
243 pub fn set_crosshair(&mut self, crosshair: bool) {
244 self.use_crosshair = crosshair
245 }
246}
247
248impl<T: Data> Widget<TooltipState<T>> for StackTooltipInternal<T> {
249 fn event(
250 &mut self,
251 ctx: &mut druid::EventCtx,
252 event: &druid::Event,
253 data: &mut TooltipState<T>,
254 env: &druid::Env,
255 ) {
256 if let Some(pos) = if let druid::Event::MouseMove(mouse) = event {
257 Some(mouse.pos)
258 } else if let druid::Event::Command(cmd) = event {
259 cmd.get(FORWARD)
260 .and_then(SingleUse::take)
261 .and_then(|(id, point)| {
262 self.label_id
263 .filter(|label_id| label_id == &id)
264 .and(Some(point))
265 })
266 .map(|point| (point - ctx.window_origin()).to_point())
267 } else {
268 None
269 } {
270 if ctx.is_hot() && ctx.size().to_rect().contains(pos) {
271 let mut x = pos.x;
272 let mut y = pos.y;
273
274 if let Some(size) = data.label_size {
275 if x + size.width + ctx.window_origin().x
276 > ctx.window().get_size().width - ctx.window().content_insets().x_value()
277 {
278 x -= size.width
279 };
280 if y + size.height + ctx.window_origin().y
281 > ctx.window().get_size().height - ctx.window().content_insets().y_value()
282 {
283 y -= size.height
284 };
285 }
286
287 data.position = StackChildPosition::new()
288 .left(Some(x))
289 .top(Some(y))
290 .height(None);
291
292 data.show = true;
293
294 if self.use_crosshair {
295 ctx.set_cursor(&druid::Cursor::Crosshair);
296 }
297
298 if let Some(label_id) = self.label_id {
299 if data.label_size.is_none() {
300 ctx.submit_command(POINT_UPDATED.to(label_id));
301 }
302 ctx.submit_command(ADVISE_TOOLTIP_SHOW.with(ctx.to_window(pos)));
303 }
304 } else {
305 reset_position(&mut data.position);
306 data.position.height = Some(0.0);
307 data.show = false;
308 }
309
310 if let druid::Event::Command(_) = event {
311 return;
312 }
313 } else if let druid::Event::Notification(notif) = event {
314 if notif.is(CANCEL_TOOLTIP_SHOW) && notif.route() == self.widget.id() {
315 reset_position(&mut data.position);
316 data.position.height = Some(0.0);
317 data.show = false;
318
319 ctx.set_handled();
320 }
321 };
322
323 self.widget.event(ctx, event, data, env)
324 }
325
326 fn lifecycle(
327 &mut self,
328 ctx: &mut druid::LifeCycleCtx,
329 event: &druid::LifeCycle,
330 data: &TooltipState<T>,
331 env: &druid::Env,
332 ) {
333 self.widget.lifecycle(ctx, event, data, env)
334 }
335
336 fn update(
337 &mut self,
338 ctx: &mut druid::UpdateCtx,
339 _old_data: &TooltipState<T>,
340 data: &TooltipState<T>,
341 env: &druid::Env,
342 ) {
343 self.widget.update(ctx, data, env)
344 }
345
346 fn layout(
347 &mut self,
348 ctx: &mut druid::LayoutCtx,
349 bc: &druid::BoxConstraints,
350 data: &TooltipState<T>,
351 env: &druid::Env,
352 ) -> druid::Size {
353 self.widget.layout(ctx, bc, data, env)
354 }
355
356 fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &TooltipState<T>, env: &druid::Env) {
357 self.widget.paint(ctx, data, env)
358 }
359}
360
361struct TooltipLabel {
362 id: WidgetId,
363 label: WidgetPod<RichText, RawLabel<RichText>>,
364 text: RichTextCell,
365 background: BackgroundCell,
366 border: BorderCell,
367}
368
369impl TooltipLabel {
370 pub fn new(
371 text: RichTextCell,
372 id: WidgetId,
373 background: BackgroundCell,
374 border: BorderCell,
375 ) -> Self {
376 let label = WidgetPod::new(Label::raw());
377
378 Self {
379 id,
380 label,
381 text,
382 background,
383 border,
384 }
385 }
386}
387
388impl<T: Data> Widget<TooltipState<T>> for TooltipLabel {
389 fn event(
390 &mut self,
391 ctx: &mut druid::EventCtx,
392 event: &druid::Event,
393 data: &mut TooltipState<T>,
394 env: &druid::Env,
395 ) {
396 if let druid::Event::MouseMove(mouse) = event {
397 ctx.submit_command(FORWARD.with(SingleUse::new((ctx.widget_id(), mouse.window_pos))))
398 } else if let druid::Event::Command(cmd) = event {
399 if cmd.is(POINT_UPDATED) {
400 if let Some(left) = data.position.left {
401 let label_width = ctx.size().width;
402 if left + label_width + ctx.window_origin().x > ctx.window().get_size().width {
403 data.position.left.replace(left - label_width);
404 }
405 }
406 if let Some(top) = data.position.top {
407 let label_height = ctx.size().height;
408 if top + label_height + ctx.window_origin().y > ctx.window().get_size().height {
409 data.position.top.replace(top - label_height);
410 }
411 }
412
413 if !ctx.size().is_empty() {
414 data.label_size.replace(ctx.size());
415 }
416
417 ctx.request_paint();
418 }
419 }
420
421 self.label
422 .event(ctx, event, &mut self.text.borrow_mut().0, env)
423 }
424
425 fn lifecycle(
426 &mut self,
427 ctx: &mut druid::LifeCycleCtx,
428 event: &druid::LifeCycle,
429 _data: &TooltipState<T>,
430 env: &druid::Env,
431 ) {
432 self.label.lifecycle(ctx, event, &self.text.borrow().0, env)
433 }
434
435 fn update(
436 &mut self,
437 ctx: &mut druid::UpdateCtx,
438 _old_data: &TooltipState<T>,
439 _data: &TooltipState<T>,
440 env: &druid::Env,
441 ) {
442 self.label.update(ctx, &self.text.borrow().0, env)
443 }
444
445 fn layout(
446 &mut self,
447 ctx: &mut druid::LayoutCtx,
448 bc: &druid::BoxConstraints,
449 _data: &TooltipState<T>,
450 env: &druid::Env,
451 ) -> druid::Size {
452 self.label.layout(ctx, bc, &self.text.borrow().0, env)
453 }
454
455 fn paint(&mut self, ctx: &mut druid::PaintCtx, _data: &TooltipState<T>, env: &druid::Env) {
456 let mut rect = ctx.size().to_rect();
457 rect.x0 -= 2.0;
458 rect.y1 += 2.0;
459
460 let fill_brush = ctx.solid_brush(
461 if let Some(background) = self.background.borrow().as_ref() {
462 background.resolve(env)
463 } else {
464 env.get(druid::theme::BACKGROUND_DARK)
465 },
466 );
467 let border_brush = ctx.solid_brush(if let Some(border) = self.border.borrow().0.as_ref() {
468 border.resolve(env)
469 } else {
470 env.get(druid::theme::BORDER_DARK)
471 });
472 let border_width = if let Some(width) = self.border.borrow().1.as_ref() {
473 *width
474 } else {
475 env.get(druid::theme::TEXTBOX_BORDER_WIDTH)
476 };
477
478 let mut text = ctx.text().new_text_layout(<&str as Into<Arc<str>>>::into(
479 self.text.borrow().0.as_str(),
480 ));
481 text = text.default_attribute(TextAttribute::FontFamily(
482 env.get(druid::theme::UI_FONT).family,
483 ));
484 text = text.default_attribute(TextAttribute::FontSize(env.get(druid::theme::UI_FONT).size));
485 text = text.default_attribute(TextAttribute::Style(env.get(druid::theme::UI_FONT).style));
486 text = text.default_attribute(TextAttribute::Weight(env.get(druid::theme::UI_FONT).weight));
487 text = text.default_attribute(TextAttribute::TextColor(env.get(druid::theme::TEXT_COLOR)));
488 for attribute in self.text.borrow().1.iter() {
489 text = text.default_attribute(attribute.clone().resolve(env));
490 }
491 if let Ok(text) = text.build() {
492 ctx.paint_with_z_index(1_000_000, move |ctx| {
493 ctx.fill(rect, &fill_brush);
494
495 ctx.draw_text(&text, (0.0, 0.0));
496
497 ctx.stroke(rect, &border_brush, border_width);
498 });
499 };
500 }
501
502 fn id(&self) -> Option<WidgetId> {
503 Some(self.id)
504 }
505}
506
507fn is_some_position(position: &StackChildPosition) -> bool {
508 position.top.is_some()
509 || position.bottom.is_some()
510 || position.left.is_some()
511 || position.right.is_some()
512}
513
514fn reset_position(position: &mut StackChildPosition) {
515 position.top = None;
516 position.bottom = None;
517 position.left = None;
518 position.right = None;
519 position.width = None;
520 position.height = None;
521}
522
523pub enum PlainOrRich {
524 Plain(String),
525 Rich(RichText),
526}
527
528impl From<String> for PlainOrRich {
529 fn from(plain: String) -> Self {
530 PlainOrRich::Plain(plain)
531 }
532}
533
534impl From<&str> for PlainOrRich {
535 fn from(plain: &str) -> Self {
536 PlainOrRich::Plain(plain.to_owned())
537 }
538}
539
540impl From<Arc<str>> for PlainOrRich {
541 fn from(plain: Arc<str>) -> Self {
542 PlainOrRich::Plain(plain.to_string())
543 }
544}
545
546impl From<RichText> for PlainOrRich {
547 fn from(rich: RichText) -> Self {
548 PlainOrRich::Rich(rich)
549 }
550}
551
552enum YetAnotherAttribute {
553 Unresolved(Attribute),
554 UnresolvedFamily(Attribute),
555 UnresolvedSize(Attribute),
556 UnresolvedWeight(Attribute),
557 UnresolvedStyle(Attribute),
558 Resolved(TextAttribute),
559}
560
561impl YetAnotherAttribute {
562 fn resolve(self, env: &druid::Env) -> TextAttribute {
563 match self {
564 YetAnotherAttribute::Unresolved(unresolved) => match unresolved {
565 Attribute::FontSize(size) => TextAttribute::FontSize(size.resolve(env)),
566 Attribute::TextColor(color) => TextAttribute::TextColor(color.resolve(env)),
567 _ => unreachable!(),
568 },
569 YetAnotherAttribute::UnresolvedFamily(desc) => {
570 if let Attribute::Descriptor(desc) = desc {
571 TextAttribute::FontFamily(desc.resolve(env).family)
572 } else {
573 unreachable!()
574 }
575 }
576 YetAnotherAttribute::UnresolvedSize(desc) => {
577 if let Attribute::Descriptor(desc) = desc {
578 TextAttribute::FontSize(desc.resolve(env).size)
579 } else {
580 unreachable!()
581 }
582 }
583 YetAnotherAttribute::UnresolvedWeight(desc) => {
584 if let Attribute::Descriptor(desc) = desc {
585 TextAttribute::Weight(desc.resolve(env).weight)
586 } else {
587 unreachable!()
588 }
589 }
590 YetAnotherAttribute::UnresolvedStyle(desc) => {
591 if let Attribute::Descriptor(desc) = desc {
592 TextAttribute::Style(desc.resolve(env).style)
593 } else {
594 unreachable!()
595 }
596 }
597 YetAnotherAttribute::Resolved(attr) => attr,
598 }
599 }
600}
601
602impl TryFrom<Attribute> for YetAnotherAttribute {
603 type Error = [YetAnotherAttribute; 4];
604
605 fn try_from(value: Attribute) -> Result<Self, Self::Error> {
606 let res = match value {
607 Attribute::FontFamily(family) => Self::Resolved(TextAttribute::FontFamily(family)),
608 Attribute::Weight(attr) => Self::Resolved(TextAttribute::Weight(attr)),
609 Attribute::Style(attr) => Self::Resolved(TextAttribute::Style(attr)),
610 Attribute::Underline(attr) => Self::Resolved(TextAttribute::Underline(attr)),
611 Attribute::Strikethrough(attr) => Self::Resolved(TextAttribute::Strikethrough(attr)),
612 unresolved @ Attribute::FontSize(_) | unresolved @ Attribute::TextColor(_) => {
613 YetAnotherAttribute::Unresolved(unresolved)
614 }
615 descriptor @ Attribute::Descriptor(_) => Err([
616 YetAnotherAttribute::UnresolvedFamily(descriptor.clone()),
617 YetAnotherAttribute::UnresolvedSize(descriptor.clone()),
618 YetAnotherAttribute::UnresolvedWeight(descriptor.clone()),
619 YetAnotherAttribute::UnresolvedStyle(descriptor),
620 ])?,
621 };
622
623 Ok(res)
624 }
625}
626
627impl Clone for YetAnotherAttribute {
628 fn clone(&self) -> Self {
629 match self {
630 Self::Unresolved(attr) => Self::Unresolved(attr.clone()),
631 Self::UnresolvedFamily(attr) => Self::UnresolvedFamily(attr.clone()),
632 Self::UnresolvedSize(attr) => Self::UnresolvedSize(attr.clone()),
633 Self::UnresolvedWeight(attr) => Self::UnresolvedWeight(attr.clone()),
634 Self::UnresolvedStyle(attr) => Self::UnresolvedStyle(attr.clone()),
635 Self::Resolved(attr) => Self::Resolved(match attr {
636 TextAttribute::FontFamily(val) => TextAttribute::FontFamily(val.clone()),
637 TextAttribute::FontSize(val) => TextAttribute::FontSize(*val),
638 TextAttribute::Weight(val) => TextAttribute::Weight(*val),
639 TextAttribute::TextColor(val) => TextAttribute::TextColor(*val),
640 TextAttribute::Style(val) => TextAttribute::Style(*val),
641 TextAttribute::Underline(val) => TextAttribute::Underline(*val),
642 TextAttribute::Strikethrough(val) => TextAttribute::Strikethrough(*val),
643 }),
644 }
645 }
646}