diff --git a/examples/widget-gallery/src/labels.rs b/examples/widget-gallery/src/labels.rs index c6809378..894ffda2 100644 --- a/examples/widget-gallery/src/labels.rs +++ b/examples/widget-gallery/src/labels.rs @@ -2,7 +2,8 @@ use floem::{ cosmic_text::{Style as FontStyle, Weight}, peniko::Color, view::View, - views::{label, Decorators}, + views::{label, static_label, Decorators}, + widgets::tooltip, }; use crate::form::{form, form_item}; @@ -11,7 +12,10 @@ pub fn label_view() -> impl View { form({ ( form_item("Simple Label:".to_string(), 120.0, || { - label(move || "This is a simple label".to_owned()) + tooltip( + label(move || "This is a simple label".to_owned()), + static_label("This is a tooltip for the label."), + ) }), form_item("Styled Label:".to_string(), 120.0, || { label(move || "This is a styled label".to_owned()).style(|s| { diff --git a/src/view.rs b/src/view.rs index c6de0462..bae09ecc 100644 --- a/src/view.rs +++ b/src/view.rs @@ -220,16 +220,7 @@ pub trait View { id_path: Option<&[Id]>, event: Event, ) -> EventPropagation { - let mut handled = false; - self.for_each_child_rev_mut(&mut |child| { - handled |= cx.view_event(child, id_path, event.clone()).is_processed(); - handled - }); - if handled { - EventPropagation::Stop - } else { - EventPropagation::Continue - } + default_event(self, cx, id_path, event) } /// `View`-specific implementation. Will be called in the [`View::paint_main`] entry point method. @@ -298,6 +289,24 @@ pub fn default_compute_layout( layout_rect } +pub fn default_event( + view: &mut V, + cx: &mut EventCx, + id_path: Option<&[Id]>, + event: Event, +) -> EventPropagation { + let mut handled = false; + view.for_each_child_rev_mut(&mut |child| { + handled |= cx.view_event(child, id_path, event.clone()).is_processed(); + handled + }); + if handled { + EventPropagation::Stop + } else { + EventPropagation::Continue + } +} + pub(crate) fn paint_bg( cx: &mut PaintCx, computed_style: &Style, diff --git a/src/views/mod.rs b/src/views/mod.rs index 68fd6382..202cbe50 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -42,6 +42,9 @@ pub use scroll::{scroll, Scroll}; mod tab; pub use tab::*; +mod tooltip; +pub use tooltip::*; + mod stack; pub use stack::*; diff --git a/src/views/tooltip.rs b/src/views/tooltip.rs new file mode 100644 index 00000000..98d6ed70 --- /dev/null +++ b/src/views/tooltip.rs @@ -0,0 +1,141 @@ +use kurbo::Point; +use std::time::Duration; +use taffy::style::Display; + +use crate::{ + action::{exec_after, TimerToken}, + context::{EventCx, StyleCx}, + event::Event, + id::Id, + prop, prop_extracter, + style::DisplayProp, + view::{default_event, View, ViewData}, + EventPropagation, +}; + +prop!(pub Delay: f64 {} = 0.6); + +prop_extracter! { + TooltipStyle { + delay: Delay, + } +} + +/// A view that displays a tooltip for its child. +pub struct Tooltip { + data: ViewData, + hover: Option<(Point, TimerToken)>, + visible: bool, + child: Box, + tip: Box, + style: TooltipStyle, +} + +/// A view that displays a tooltip for its child. +pub fn tooltip(child: V, tip: T) -> Tooltip { + Tooltip { + data: ViewData::new(Id::next()), + child: Box::new(child), + tip: Box::new(tip), + hover: None, + visible: false, + style: Default::default(), + } +} + +impl View for Tooltip { + fn view_data(&self) -> &ViewData { + &self.data + } + + fn view_data_mut(&mut self) -> &mut ViewData { + &mut self.data + } + + fn for_each_child<'a>(&'a self, for_each: &mut dyn FnMut(&'a dyn View) -> bool) { + for_each(&self.child); + for_each(&self.tip); + } + + fn for_each_child_mut<'a>(&'a mut self, for_each: &mut dyn FnMut(&'a mut dyn View) -> bool) { + for_each(&mut self.child); + for_each(&mut self.tip); + } + + fn for_each_child_rev_mut<'a>( + &'a mut self, + for_each: &mut dyn FnMut(&'a mut dyn View) -> bool, + ) { + for_each(&mut self.tip); + for_each(&mut self.child); + } + + fn debug_name(&self) -> std::borrow::Cow<'static, str> { + "Tooltip".into() + } + + fn update(&mut self, cx: &mut crate::context::UpdateCx, state: Box) { + if let Ok(token) = state.downcast::() { + if self.hover.map(|(_, t)| t) == Some(*token) { + self.visible = true; + cx.request_style(self.tip.id()); + cx.request_layout(self.tip.id()); + } + } + } + + fn style(&mut self, cx: &mut StyleCx<'_>) { + self.style.read(cx); + + cx.style_view(&mut self.child); + cx.style_view(&mut self.tip); + + let tip_view = cx.app_state_mut().view_state(self.tip.id()); + tip_view.combined_style = tip_view + .combined_style + .clone() + .set( + DisplayProp, + if self.visible { + Display::Flex + } else { + Display::None + }, + ) + .absolute() + .inset_left(self.hover.map(|(p, _)| p.x).unwrap_or(0.0)) + .inset_top(self.hover.map(|(p, _)| p.y).unwrap_or(0.0)) + .z_index(100); + } + + fn event( + &mut self, + cx: &mut EventCx, + id_path: Option<&[Id]>, + event: Event, + ) -> EventPropagation { + match &event { + Event::PointerMove(e) => { + if !self.visible { + let id = self.id(); + let token = + exec_after(Duration::from_secs_f64(self.style.delay()), move |token| { + id.update_state(token, false); + }); + self.hover = Some((e.pos, token)); + } + } + Event::PointerLeave => { + self.hover = None; + if self.visible { + self.visible = false; + cx.request_style(self.tip.id()); + cx.request_layout(self.tip.id()); + } + } + _ => {} + } + + default_event(self, cx, id_path, event) + } +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 76c13e71..bba1b468 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -18,6 +18,9 @@ pub use checkbox::*; mod toggle_button; pub use toggle_button::*; +mod tooltip; +pub use tooltip::*; + pub mod slider; mod button; @@ -170,6 +173,19 @@ pub(crate) fn default_theme() -> Theme { .set(slider::CircleRad, PxPct::Pct(100.)) .set(slider::BarExtends, false) }) + .class(TooltipClass, |s| { + s.border(0.5) + .border_color(Color::rgb8(140, 140, 140)) + .color(Color::rgb8(80, 80, 80)) + .border_radius(2.0) + .padding(padding) + .margin(10.0) + .background(Color::WHITE_SMOKE) + .box_shadow_blur(2.0) + .box_shadow_h_offset(2.0) + .box_shadow_v_offset(2.0) + .box_shadow_color(Color::BLACK.with_alpha_factor(0.2)) + }) .font_size(FONT_SIZE) .color(Color::BLACK); diff --git a/src/widgets/tooltip.rs b/src/widgets/tooltip.rs new file mode 100644 index 00000000..8515c6e2 --- /dev/null +++ b/src/widgets/tooltip.rs @@ -0,0 +1,11 @@ +use crate::{ + style_class, + view::View, + views::{self, container, Decorators}, +}; + +style_class!(pub TooltipClass); + +pub fn tooltip(child: V, tip: T) -> impl View { + views::tooltip(child, container(tip).class(TooltipClass)) +}