druid_widget_nursery/
stack_tooltip.rs

1// Copyright 2020 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! A stack based tooltip widget.
16
17use 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}