diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 34e29a5c..e59ee950 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -6,9 +6,10 @@ use cosmic::iced::platform_specific::shell::commands::popup::{destroy_popup, get use cosmic::iced::window::Id; use cosmic::iced::{Length, Limits, Task}; use cosmic::iced_runtime::core::window; +use cosmic::iced_runtime::platform_specific::wayland::subsurface; use cosmic::theme::iced; -use cosmic::widget::{list_column, settings, toggler}; -use cosmic::{applet, Element, Theme}; +use cosmic::widget::{layer_container, list_column, settings, toggler}; +use cosmic::{applet, iced_core, Element, Theme}; const ID: &str = "com.system76.CosmicAppletExample"; @@ -16,7 +17,8 @@ const ID: &str = "com.system76.CosmicAppletExample"; pub struct Window { core: Core, popup: Option, - example_row: Arc>, + example_row: bool, + subsurface_id: Option, } #[derive(Clone, Debug)] @@ -24,6 +26,8 @@ pub enum Message { TogglePopup, PopupClosed(Id), ToggleExampleRow(bool), + Hover, + Leave, } impl cosmic::Application for Window { @@ -56,52 +60,57 @@ impl cosmic::Application for Window { match message { Message::TogglePopup => { return if let Some(p) = self.popup.take() { - destroy_popup(p) + cosmic::task::message(cosmic::app::message::destroy_popup(p)) } else { - let new_id = Id::unique(); - self.popup.replace(new_id); - let mut popup_settings = self.core.applet.get_popup_settings( - self.core.main_window_id().unwrap(), - new_id, - None, - None, - None, - ); - popup_settings.positioner.size_limits = Limits::NONE - .max_width(372.0) - .min_width(300.0) - .min_height(200.0) - .max_height(1080.0) - .height(500) - .width(500); - popup_settings.positioner.size = Some((500, 500)); + cosmic::task::message( + cosmic::app::message::get_popup( + |state: &mut Window| { + let new_id = Id::unique(); + state.popup = Some(new_id); + let mut popup_settings = state.core.applet.get_popup_settings( + state.core.main_window_id().unwrap(), + new_id, + None, + None, + None, + ); + popup_settings.positioner.size_limits = Limits::NONE + .max_width(372.0) + .min_width(300.0) + .min_height(200.0) + .max_height(1080.0) + .height(500) + .width(500); + popup_settings.positioner.size = Some((500, 500)); + popup_settings + }, + Some( + move |state: &Window| -> cosmic::Element< + 'static, + cosmic::app::Message, + > { + { + let content_list = list_column().padding(5).spacing(0).add( + settings::item( + "Example row", + cosmic::widget::container( + toggler(state.example_row).on_toggle(|value| { + Message::ToggleExampleRow(value) + }), + ) + .height(Length::Fixed(50.)), + ), + ); - let applet = self.core.applet.clone(); - let toggled = self.example_row.clone(); - popup_settings = popup_settings.with_view(Box::new( - move || -> Option>> { - { - let guard = toggled.lock().unwrap(); - - let content_list = - list_column().padding(5).spacing(0).add(settings::item( - "Example row", - cosmic::widget::container( - toggler(*guard).on_toggle(|value| { - Message::ToggleExampleRow(value) - }), + Element::from( + state.core.applet.popup_container(content_list), ) - .height(Length::Fixed(50.)), - )); - - Some( - Element::from(applet.popup_container(content_list)) - .map(cosmic::app::Message::App), - ) - } - }, - )); - get_popup(popup_settings) + .map(cosmic::app::Message::App) + } + }, + ), + ), + ) }; } Message::PopupClosed(id) => { @@ -110,19 +119,44 @@ impl cosmic::Application for Window { } } Message::ToggleExampleRow(toggled) => { - let mut guard = self.example_row.lock().unwrap(); - *guard = toggled; + self.example_row = toggled; + } + Message::Hover => { + return cosmic::task::message(cosmic::app::message::get_subsurface( + |app: &mut Window| { + let id = window::Id::unique(); + app.subsurface_id = Some(id); + + subsurface::SctkSubsurfaceSettings { + parent: window::Id::RESERVED, + id, + loc: iced_core::Point { x: -100., y: 0. }, + size: Some(iced_core::Size::new(100., 18.)), + z: 1, + } + }, + Some(|app: &Window| layer_container(cosmic::widget::text("hello")).into()), + )); + } + Message::Leave => { + return cosmic::task::message(cosmic::app::message::destroy_subsurface( + self.subsurface_id.unwrap_or(window::Id::NONE), + )); } } Task::none() } fn view(&self) -> Element { - self.core - .applet - .icon_button("display-symbolic") - .on_press(Message::TogglePopup) - .into() + cosmic::widget::wayland::tooltip::widget::Tooltip::new( + self.core + .applet + .icon_button("display-symbolic") + .on_press(Message::TogglePopup), + |layout| Message::Hover, + Message::Leave, + ) + .into() } fn view_window(&self, _id: Id) -> Element { diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index fcdee62c..f0ed9ef2 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -85,7 +85,7 @@ pub enum Message { pub struct Cosmic { pub app: App, #[cfg(feature = "wayland")] - pub subsurfaces: + pub surface_views: HashMap Element<'static, super::Message>>>, } @@ -129,18 +129,63 @@ where super::Message::DbusActivation(message) => self.app.dbus_activation(message), #[cfg(feature = "wayland")] super::Message::Subsurface(settings, view) => { - if let Some(view) = view.and_then(|view| match view.downcast() { - Ok(v) => Some(v), - Err(err) => { - tracing::error!("Invalid view for subsurface view: {err:?}"); - None + let Some(settings) = std::sync::Arc::try_unwrap(settings) + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for subsurface"); + return Task::none(); + }; + + if let Some(view) = view.and_then(|view| { + match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Element<'static, super::Message> + Send + Sync, + >>() { + Ok(v) => Some(v), + Err(err) => { + tracing::error!("Invalid view for subsurface view: {err:?}"); + None + } } }) { - self.get_subsurface(settings, view) + let settings = settings(&mut self.app); + self.get_subsurface(settings, *view) } else { - iced_winit::commands::subsurface::get_subsurface(settings) + iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app)) } } + #[cfg(feature = "wayland")] + super::Message::Popup(settings, view) => { + let Some(settings) = std::sync::Arc::try_unwrap(settings) + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for popup"); + return Task::none(); + }; + + if let Some(view) = view.and_then(|view| { + match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Element<'static, super::Message> + Send + Sync, + >>() { + Ok(v) => Some(v), + Err(err) => { + tracing::error!("Invalid view for subsurface view: {err:?}"); + None + } + } + }) { + let settings = settings(&mut self.app); + + self.get_popup(settings, *view) + } else { + iced_winit::commands::popup::get_popup(settings(&mut self.app)) + } + } + #[cfg(feature = "wayland")] + super::Message::DestroyPopup(id) => iced_winit::commands::popup::destroy_popup(id), + #[cfg(feature = "wayland")] + super::Message::DestroySubsurface(id) => { + iced_winit::commands::subsurface::destroy_subsurface(id) + } }; #[cfg(target_env = "gnu")] @@ -306,7 +351,7 @@ where #[cfg(feature = "multi-window")] pub fn view(&self, id: window::Id) -> Element> { #[cfg(feature = "wayland")] - if let Some(v) = self.subsurfaces.get(&id) { + if let Some(v) = self.surface_views.get(&id) { return v(&self.app); } if !self @@ -722,7 +767,7 @@ impl Cosmic { Self { app, #[cfg(feature = "wayland")] - subsurfaces: HashMap::new(), + surface_views: HashMap::new(), } } @@ -731,11 +776,24 @@ impl Cosmic { pub fn get_subsurface( &mut self, settings: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings, - view: Box Element<'static, super::Message>>, + view: Box Element<'static, super::Message> + Send + Sync>, ) -> Task> { use iced_winit::commands::subsurface::get_subsurface; - self.subsurfaces.insert(settings.id, view); + self.surface_views.insert(settings.id, view); get_subsurface(settings) } + + #[cfg(feature = "wayland")] + /// Create a subsurface + pub fn get_popup( + &mut self, + settings: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings, + view: Box Element<'static, super::Message> + Send + Sync>, + ) -> Task> { + use iced_winit::commands::popup::get_popup; + + self.surface_views.insert(settings.id, view); + get_popup(settings) + } } diff --git a/src/app/mod.rs b/src/app/mod.rs index cd2b31a5..daad39f3 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -15,7 +15,7 @@ pub(crate) mod multi_window; pub mod settings; pub mod message { - #[derive(Clone, Debug)] + #[derive(Clone)] #[must_use] pub enum Message { /// Messages from the application, for the application. @@ -30,9 +30,21 @@ pub mod message { #[cfg(feature = "wayland")] /// Create a subsurface with a view function Subsurface( - iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings, - Option>, + std::sync::Arc>, + Option>>, ), + #[cfg(feature = "wayland")] + /// Destroy a subsurface with a view function + DestroySubsurface(iced::window::Id), + #[cfg(feature = "wayland")] + /// Create a popup with a view function + Popup( + std::sync::Arc>, + Option>>, + ), + #[cfg(feature = "wayland")] + /// Destroy a subsurface with a view function + DestroyPopup(iced::window::Id), } pub const fn app(message: M) -> Message { @@ -47,11 +59,118 @@ pub mod message { Message::None } + #[cfg(feature = "wayland")] + pub const fn destroy_popup(id: iced_core::window::Id) -> Message { + Message::DestroyPopup(id) + } + + #[cfg(feature = "wayland")] + pub const fn destroy_subsurface(id: iced_core::window::Id) -> Message { + Message::DestroySubsurface(id) + } + + #[cfg(feature = "wayland")] + pub fn get_popup( + settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + + Send + + Sync + + 'static, + view: Option< + impl Fn(&App) -> crate::Element<'static, super::Message> + + Send + + Sync + + 'static, + >, + ) -> Message { + use std::{any::Any, sync::Arc}; + let boxed: Box< + dyn Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + Message::Popup( + Arc::new(boxed), + view.map(|view| { + let boxed: Box< + dyn Fn(&App) -> crate::Element<'static, super::Message> + + Send + + Sync + + 'static, + > = Box::new(view); + let boxed: Box = Box::new(boxed); + Arc::new(boxed) + }), + ) + } + + #[cfg(feature = "wayland")] + pub fn get_subsurface( + settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync + 'static, + view: Option< + impl Fn(&App) -> crate::Element<'static, super::Message> + + Send + + Sync + + 'static, + >, + ) -> Message { + use std::{any::Any, sync::Arc}; + let boxed: Box< + dyn Fn( + &mut App, + ) + -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + Message::Subsurface( + Arc::new(boxed), + view.map(|view| { + let boxed: Box< + dyn Fn(&App) -> crate::Element<'static, super::Message> + + Send + + Sync + + 'static, + > = Box::new(view); + let boxed: Box = Box::new(boxed); + Arc::new(boxed) + }), + ) + } + impl From for Message { fn from(value: M) -> Self { Self::App(value) } } + + impl std::fmt::Debug for Message { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::App(arg0) => f.debug_tuple("App").field(arg0).finish(), + Self::Cosmic(arg0) => f.debug_tuple("Cosmic").field(arg0).finish(), + + #[cfg(feature = "single-instance")] + Self::DbusActivation(arg0) => f.debug_tuple("DbusActivation").field(arg0).finish(), + Self::None => write!(f, "None"), + #[cfg(feature = "wayland")] + Self::Subsurface(arg0, arg1) => f.debug_tuple("Subsurface").field(arg1).finish(), + #[cfg(feature = "wayland")] + Self::DestroySubsurface(arg0) => { + f.debug_tuple("DestroySubsurface").field(arg0).finish() + } + #[cfg(feature = "wayland")] + Self::Popup(arg0, arg1) => f.debug_tuple("Popup").field(arg1).finish(), + #[cfg(feature = "wayland")] + Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(), + } + } + } } use std::borrow::Cow; diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 531f65c3..0ca8a4f1 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -308,7 +308,6 @@ impl Context { }, parent_size: None, grab: true, - popup_view: None, } } diff --git a/src/theme/style/mod.rs b/src/theme/style/mod.rs index 1cbd4ef5..c62d6be9 100644 --- a/src/theme/style/mod.rs +++ b/src/theme/style/mod.rs @@ -31,3 +31,6 @@ pub use self::segmented_button::SegmentedButton; mod text_input; #[doc(inline)] pub use self::text_input::TextInput; + +pub mod tooltip; +pub use tooltip::Tooltip; diff --git a/src/theme/style/tooltip.rs b/src/theme/style/tooltip.rs new file mode 100644 index 00000000..a1e77774 --- /dev/null +++ b/src/theme/style/tooltip.rs @@ -0,0 +1,31 @@ +use iced::{Background, Color}; + +use crate::widget::wayland::tooltip::Catalog; + +#[derive(Default)] +pub enum Tooltip { + #[default] + Default, +} + +impl Catalog for crate::Theme { + type Class = Tooltip; + + fn style(&self, style: &Self::Class) -> crate::widget::wayland::tooltip::Style { + let cosmic = self.cosmic(); + + match style { + Tooltip::Default => crate::widget::wayland::tooltip::Style { + text_color: cosmic.on_bg_color().into(), + background: Some(Background::Color(cosmic.background.component.base.into())), + border_width: 0.0, + border_radius: cosmic.corner_radii.radius_m.into(), + border_color: Color::TRANSPARENT, + shadow_offset: iced::Vector::default(), + outline_width: Default::default(), + outline_color: Color::TRANSPARENT, + icon_color: None, + }, + } + } +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 06dd4e85..0b0eeb8f 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -335,6 +335,10 @@ pub use toggler::toggler; #[doc(inline)] pub use tooltip::{tooltip, Tooltip}; + +#[cfg(feature = "wayland")] +pub mod wayland; + pub mod tooltip { use crate::Element; use std::borrow::Cow; diff --git a/src/widget/wayland/mod.rs b/src/widget/wayland/mod.rs new file mode 100644 index 00000000..7c53d374 --- /dev/null +++ b/src/widget/wayland/mod.rs @@ -0,0 +1 @@ +pub mod tooltip; diff --git a/src/widget/wayland/tooltip/mod.rs b/src/widget/wayland/tooltip/mod.rs new file mode 100644 index 00000000..79a2fda9 --- /dev/null +++ b/src/widget/wayland/tooltip/mod.rs @@ -0,0 +1,76 @@ +//! Change the apperance of a tooltip. + +pub mod widget; + +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use iced_core::{border::Radius, Background, Color, Vector}; + +use crate::theme::THEME; + +/// The appearance of a tooltip. +#[must_use] +#[derive(Debug, Clone, Copy)] +pub struct Style { + /// The amount of offset to apply to the shadow of the tooltip. + pub shadow_offset: Vector, + + /// The [`Background`] of the tooltip. + pub background: Option, + + /// The border radius of the tooltip. + pub border_radius: Radius, + + /// The border width of the tooltip. + pub border_width: f32, + + /// The border [`Color`] of the tooltip. + pub border_color: Color, + + /// An outline placed around the border. + pub outline_width: f32, + + /// The [`Color`] of the outline. + pub outline_color: Color, + + /// The icon [`Color`] of the tooltip. + pub icon_color: Option, + + /// The text [`Color`] of the tooltip. + pub text_color: Color, +} + +impl Style { + // TODO: `Radius` is not `const fn` compatible. + pub fn new() -> Self { + let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0; + Self { + shadow_offset: Vector::new(0.0, 0.0), + background: None, + border_radius: Radius::from(rad_0), + border_width: 0.0, + border_color: Color::TRANSPARENT, + outline_width: 0.0, + outline_color: Color::TRANSPARENT, + icon_color: None, + text_color: Color::BLACK, + } + } +} + +impl std::default::Default for Style { + fn default() -> Self { + Self::new() + } +} + +// TODO update to match other styles +/// A set of rules that dictate the style of a tooltip. +pub trait Catalog { + /// The supported style of the [`StyleSheet`]. + type Class: Default; + + /// Produces the active [`Appearance`] of a tooltip. + fn style(&self, style: &Self::Class) -> Style; +} diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs new file mode 100644 index 00000000..1950504f --- /dev/null +++ b/src/widget/wayland/tooltip/widget.rs @@ -0,0 +1,545 @@ +// Copyright 2019 H�ctor Ram�n, Iced contributors +// Copyright 2023 System76 +// SPDX-License-Identifier: MIT + +//! Allow your users to perform actions by pressing a button. +//! +//! A [`Tooltip`] has some local [`State`]. + +use iced_runtime::core::widget::Id; +use iced_runtime::{keyboard, task, Action, Task}; + +use iced_core::event::{self, Event}; +use iced_core::renderer::{self, Quad, Renderer}; +use iced_core::touch; +use iced_core::widget::tree::{self, Tree}; +use iced_core::widget::Operation; +use iced_core::{layout, svg}; +use iced_core::{mouse, Border}; +use iced_core::{overlay, Shadow}; +use iced_core::{ + Background, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, +}; +use iced_renderer::core::widget::operation; + +use crate::theme::THEME; + +pub use super::{Catalog, Style}; + +/// Internally defines different button widget variants. +enum Variant { + Normal, + Image { + close_icon: svg::Handle, + on_remove: Option, + }, +} + +/// A generic button which emits a message when pressed. +#[allow(missing_debug_implementations)] +#[must_use] +pub struct Tooltip<'a, Message> { + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, + content: crate::Element<'a, Message>, + on_hover: Box Message + 'a>, + on_leave: Message, + width: Length, + height: Length, + padding: Padding, + selected: bool, + style: crate::theme::Tooltip, +} + +impl<'a, Message> Tooltip<'a, Message> { + /// Creates a new [`Tooltip`] with the given content. + pub fn new( + content: impl Into>, + on_hover: impl Fn(iced_core::Layout) -> Message + 'a, + on_leave: Message, + ) -> Self { + Self { + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, + content: content.into(), + on_hover: Box::new(on_hover), + width: Length::Shrink, + height: Length::Shrink, + padding: Padding::new(5.0), + selected: false, + style: crate::theme::Tooltip::default(), + on_leave, + } + } + + /// Sets the [`Id`] of the [`Tooltip`]. + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } + + /// Sets the width of the [`Tooltip`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Tooltip`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the [`Padding`] of the [`Tooltip`]. + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the widget to a selected state. + /// + /// Displays a selection indicator on image buttons. + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + + self + } + + /// Sets the style variant of this [`Tooltip`]. + pub fn class(mut self, style: crate::theme::Tooltip) -> Self { + self.style = style; + self + } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Tooltip`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Tooltip`]. + pub fn description_widget(mut self, description: &T) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Tooltip`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Tooltip`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } +} + +impl<'a, Message: 'a + Clone> Widget + for Tooltip<'a, Message> +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)); + } + + fn size(&self) -> iced_core::Size { + iced_core::Size::new(self.width, self.height) + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.width, + self.height, + self.padding, + |renderer, limits| { + self.content + .as_widget() + .layout(&mut tree.children[0], renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &crate::Renderer, + operation: &mut dyn Operation<()>, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); + let state = tree.state.downcast_mut::(); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &crate::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + if self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout.children().next().unwrap(), + cursor, + renderer, + clipboard, + shell, + viewport, + ) == event::Status::Captured + { + return event::Status::Captured; + } + + update( + self.id.clone(), + event, + layout, + cursor, + shell, + &self.on_hover, + &self.on_leave, + || tree.state.downcast_mut::(), + ) + } + + #[allow(clippy::too_many_lines)] + fn draw( + &self, + tree: &Tree, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + if !viewport.intersects(&bounds) { + return; + } + let content_layout = layout.children().next().unwrap(); + + let state = tree.state.downcast_ref::(); + + let styling = theme.style(&self.style); + + let mut icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color); + + draw::<_, crate::Theme>( + renderer, + bounds, + *viewport, + &styling, + |renderer, _styling| { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + &renderer::Style { + icon_color, + text_color: styling.text_color, + scale_factor: renderer_style.scale_factor, + }, + content_layout, + cursor, + &viewport.intersection(&bounds).unwrap_or_default(), + ); + }, + ); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &crate::Renderer, + ) -> mouse::Interaction { + self.content + .as_widget() + .mouse_interaction(tree, layout, cursor, viewport, renderer) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &crate::Renderer, + mut translation: Vector, + ) -> Option> { + let position = layout.bounds().position(); + translation.x += position.x; + translation.y += position.y; + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + translation, + ) + } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + p: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + let c_layout = layout.children().next().unwrap(); + + self.content.as_widget().a11y_nodes(c_layout, state, p) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } +} + +impl<'a, Message: Clone + 'a> From> for crate::Element<'a, Message> { + fn from(button: Tooltip<'a, Message>) -> Self { + Self::new(button) + } +} + +/// The local state of a [`Tooltip`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[allow(clippy::struct_field_names)] +pub struct State { + is_hovered: bool, +} + +impl State { + /// Returns whether the [`Tooltip`] is currently hovered or not. + pub fn is_hovered(self) -> bool { + self.is_hovered + } +} + +/// Processes the given [`Event`] and updates the [`State`] of a [`Tooltip`] +/// accordingly. +#[allow(clippy::needless_pass_by_value)] +pub fn update<'a, Message: Clone>( + _id: Id, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + shell: &mut Shell<'_, Message>, + on_hover: &dyn Fn(Layout<'_>) -> Message, + on_leave: &Message, + state: impl FnOnce() -> &'a mut State, +) -> event::Status { + match event { + Event::Touch(touch::Event::FingerLifted { .. }) => { + let state = state(); + + if state.is_hovered { + state.is_hovered = false; + + shell.publish(on_leave.clone()); + + return event::Status::Captured; + } + } + + Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => { + let state = state(); + if state.is_hovered { + state.is_hovered = false; + + shell.publish(on_leave.clone()); + } + } + + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + let state = state(); + let bounds = layout.bounds(); + + if state.is_hovered { + state.is_hovered = cursor.is_over(bounds); + if !state.is_hovered { + shell.publish(on_leave.clone()); + } + } else { + state.is_hovered = cursor.is_over(bounds); + if state.is_hovered { + shell.publish(on_hover(layout).clone()); + } + } + } + _ => {} + } + + event::Status::Ignored +} + +#[allow(clippy::too_many_arguments)] +pub fn draw( + renderer: &mut Renderer, + bounds: Rectangle, + viewport_bounds: Rectangle, + styling: &super::Style, + draw_contents: impl FnOnce(&mut Renderer, &Style), +) where + Theme: super::Catalog, +{ + let doubled_border_width = styling.border_width * 2.0; + let doubled_outline_width = styling.outline_width * 2.0; + + if styling.outline_width > 0.0 { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x - styling.border_width - styling.outline_width, + y: bounds.y - styling.border_width - styling.outline_width, + width: bounds.width + doubled_border_width + doubled_outline_width, + height: bounds.height + doubled_border_width + doubled_outline_width, + }, + border: Border { + width: styling.outline_width, + color: styling.outline_color, + radius: styling.border_radius, + }, + shadow: Shadow::default(), + }, + Color::TRANSPARENT, + ); + } + + if styling.background.is_some() || styling.border_width > 0.0 { + if styling.shadow_offset != Vector::default() { + // TODO: Implement proper shadow support + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + styling.shadow_offset.x, + y: bounds.y + styling.shadow_offset.y, + width: bounds.width, + height: bounds.height, + }, + border: Border { + radius: styling.border_radius, + ..Default::default() + }, + shadow: Shadow::default(), + }, + Background::Color([0.0, 0.0, 0.0, 0.5].into()), + ); + } + + // Draw the button background first. + if let Some(background) = styling.background { + renderer.fill_quad( + renderer::Quad { + bounds, + border: Border { + radius: styling.border_radius, + ..Default::default() + }, + shadow: Shadow::default(), + }, + background, + ); + } + + // Then draw the button contents onto the background. + draw_contents(renderer, styling); + + let mut clipped_bounds = viewport_bounds.intersection(&bounds).unwrap_or_default(); + clipped_bounds.height += styling.border_width; + + renderer.with_layer(clipped_bounds, |renderer| { + // Finish by drawing the border above the contents. + renderer.fill_quad( + renderer::Quad { + bounds, + border: Border { + width: styling.border_width, + color: styling.border_color, + radius: styling.border_radius, + }, + shadow: Shadow::default(), + }, + Color::TRANSPARENT, + ); + }); + } else { + draw_contents(renderer, styling); + } +} + +/// Computes the layout of a [`Tooltip`]. +pub fn layout( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + padding: Padding, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.width(width).height(height); + + let mut content = layout_content(renderer, &limits.shrink(padding)); + let padding = padding.fit(content.size(), limits.max()); + let size = limits + .shrink(padding) + .resolve(width, height, content.size()) + .expand(padding); + + content = content.move_to(Point::new(padding.left, padding.top)); + + layout::Node::with_children(size, vec![content]) +}