From 071c77c1a4bcbb60c023c5ae1419a8afe55903c0 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 8 Oct 2025 23:52:53 -0400 Subject: [PATCH] refactor: applet row and column for improved overlap hover handling --- src/applet/column.rs | 488 +++++++++++++++++++++++++++++++++++++++++++ src/applet/mod.rs | 5 +- src/applet/row.rs | 477 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 969 insertions(+), 1 deletion(-) create mode 100644 src/applet/column.rs create mode 100644 src/applet/row.rs diff --git a/src/applet/column.rs b/src/applet/column.rs new file mode 100644 index 00000000..992b9421 --- /dev/null +++ b/src/applet/column.rs @@ -0,0 +1,488 @@ +//! Distribute content vertically. +use crate::iced; +use iced::core::alignment::{self, Alignment}; +use iced::core::event::{self, Event}; +use iced::core::layout; +use iced::core::mouse; +use iced::core::overlay; +use iced::core::renderer; +use iced::core::widget::{Operation, Tree}; +use iced::core::{ + Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, + widget, +}; + +/// A container that distributes its contents vertically. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{button, column}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// column![ +/// "I am on top!", +/// button("I am in the center!"), +/// "I am below.", +/// ].into() +/// } +/// ``` +#[allow(missing_debug_implementations)] +#[must_use] +pub struct Column<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> { + spacing: f32, + padding: Padding, + width: Length, + height: Length, + max_width: f32, + align: Alignment, + clip: bool, + children: Vec>, +} + +impl<'a, Message, Theme, Renderer> Column<'a, Message, Theme, Renderer> +where + Renderer: iced::core::Renderer, +{ + /// Creates an empty [`Column`]. + pub fn new() -> Self { + Self::from_vec(Vec::new()) + } + + /// Creates a [`Column`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + + /// Creates a [`Column`] with the given elements. + pub fn with_children( + children: impl IntoIterator>, + ) -> Self { + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) + } + + /// Creates a [`Column`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`Column`] will not inspect the [`Vec`], which means + /// it won't automatically adapt to the sizing strategy of its contents. + /// + /// If any of the children have a [`Length::Fill`] strategy, you will need to + /// call [`Column::width`] or [`Column::height`] accordingly. + pub fn from_vec(children: Vec>) -> Self { + Self { + spacing: 0.0, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + max_width: f32::INFINITY, + align: Alignment::Start, + clip: false, + children, + } + } + + /// Sets the vertical spacing _between_ elements. + /// + /// Custom margins per element do not exist in iced. You should use this + /// method instead! While less flexible, it helps you keep spacing between + /// elements consistent. + pub fn spacing(mut self, amount: impl Into) -> Self { + self.spacing = amount.into().0; + self + } + + /// Sets the [`Padding`] of the [`Column`]. + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`Column`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Column`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the maximum width of the [`Column`]. + pub fn max_width(mut self, max_width: impl Into) -> Self { + self.max_width = max_width.into().0; + self + } + + /// Sets the horizontal alignment of the contents of the [`Column`] . + pub fn align_x(mut self, align: impl Into) -> Self { + self.align = Alignment::from(align.into()); + self + } + + /// Sets whether the contents of the [`Column`] should be clipped on + /// overflow. + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; + self + } + + /// Adds an element to the [`Column`]. + pub fn push(mut self, child: impl Into>) -> Self { + let child = child.into(); + let child_size = child.as_widget().size_hint(); + + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); + + self.children.push(child); + self + } + + /// Adds an element to the [`Column`], if `Some`. + #[must_use] + pub fn push_maybe( + self, + child: Option>>, + ) -> Self { + if let Some(child) = child { + self.push(child) + } else { + self + } + } + + /// Extends the [`Column`] with the given children. + pub fn extend( + self, + children: impl IntoIterator>, + ) -> Self { + children.into_iter().fold(self, Self::push) + } +} + +impl Default for Column<'_, Message, Renderer> +where + Renderer: iced::core::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message, Theme, Renderer: iced::core::Renderer> + FromIterator> for Column<'a, Message, Theme, Renderer> +{ + fn from_iter>>(iter: T) -> Self { + Self::with_children(iter) + } +} + +impl Widget + for Column<'_, Message, Theme, Renderer> +where + Renderer: iced::core::Renderer, +{ + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn state(&self) -> widget::tree::State { + widget::tree::State::new(State::default()) + } + + fn tag(&self) -> widget::tree::Tag { + widget::tree::Tag::of::() + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(self.children.as_mut_slice()); + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.max_width(self.max_width); + + layout::flex::resolve( + layout::flex::Axis::Vertical, + renderer, + &limits, + self.width, + self.height, + self.padding, + self.spacing, + self.align, + &self.children, + &mut tree.children, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), c_layout)| { + child.as_widget().operate( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + renderer, + operation, + ); + }); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let my_state = tree.state.downcast_mut::(); + + if let Some(hovered) = my_state.hovered { + let child_layout = layout.children().nth(hovered); + if let Some(child_layout) = child_layout + && cursor.is_over(child_layout.bounds()) + { + // if mouse event, we can skip checking other children + if let Event::Mouse(e) = &event { + if !matches!( + e, + mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. } + ) { + return self.children[hovered].as_widget_mut().on_event( + &mut tree.children[hovered], + event, + child_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + } else if let Event::Touch(t) = &event { + if !matches!( + t, + iced::core::touch::Event::FingerLifted { .. } + | iced::core::touch::Event::FingerLost { .. } + ) { + return self.children[hovered].as_widget_mut().on_event( + &mut tree.children[hovered], + event, + child_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + } + } else { + my_state.hovered = None; + } + } + + self.children + .iter_mut() + .enumerate() + .zip(&mut tree.children) + .zip(layout.children()) + .map(|(((i, child), state), c_layout)| { + if matches!( + event, + Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) + | Event::Touch( + iced_core::touch::Event::FingerMoved { .. } + | iced_core::touch::Event::FingerPressed { .. } + ) + ) && cursor.is_over(c_layout.bounds()) + { + my_state.hovered = Some(i); + } + + child.as_widget_mut().on_event( + state, + event.clone(), + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, state), c_layout)| { + child.as_widget().mouse_interaction( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + let viewport = if self.clip { + &clipped_viewport + } else { + viewport + }; + + for ((child, state), c_layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .filter(|(_, layout)| layout.bounds().intersects(viewport)) + { + child.as_widget().draw( + state, + renderer, + theme, + style, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + viewport, + ); + } + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + overlay::from_children(&mut self.children, tree, layout, renderer, translation) + } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::A11yTree; + A11yTree::join( + self.children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + .map(|((c, c_layout), state)| { + c.as_widget().a11y_nodes( + c_layout.with_virtual_offset(layout.virtual_offset()), + state, + cursor, + ) + }), + ) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced::core::clipboard::DndDestinationRectangles, + ) { + for ((e, c_layout), state) in self + .children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + { + e.as_widget().drag_destinations( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + renderer, + dnd_rectangles, + ); + } + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: iced::core::Renderer + 'a, +{ + fn from(column: Column<'a, Message, Theme, Renderer>) -> Self { + Self::new(column) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct State { + hovered: Option, +} diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 2509a083..d7f0d721 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -1,6 +1,7 @@ #[cfg(feature = "applet-token")] pub mod token; +use crate::app::cosmic; use crate::{ Application, Element, Renderer, app::iced_settings, @@ -27,7 +28,9 @@ use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gra use std::{borrow::Cow, num::NonZeroU32, rc::Rc, sync::LazyLock, time::Duration}; use tracing::info; -use crate::app::cosmic; +pub mod column; +pub mod row; + static AUTOSIZE_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize")); static AUTOSIZE_MAIN_ID: LazyLock = diff --git a/src/applet/row.rs b/src/applet/row.rs new file mode 100644 index 00000000..3a98e6f1 --- /dev/null +++ b/src/applet/row.rs @@ -0,0 +1,477 @@ +//! Distribute content horizontally. +use crate::iced; +use iced::core::alignment::{self, Alignment}; +use iced::core::event::{self, Event}; +use iced::core::layout::{self, Layout}; +use iced::core::mouse; +use iced::core::overlay; +use iced::core::renderer; +use iced::core::widget::{Operation, Tree}; +use iced::core::{ + Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, widget, +}; +use iced::touch; + +/// A container that distributes its contents horizontally. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{button, row}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// row![ +/// "I am to the left!", +/// button("I am in the middle!"), +/// "I am to the right!", +/// ].into() +/// } +/// ``` +#[allow(missing_debug_implementations)] +#[must_use] +pub struct Row<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> { + spacing: f32, + padding: Padding, + width: Length, + height: Length, + align: Alignment, + clip: bool, + children: Vec>, +} + +impl<'a, Message, Theme, Renderer> Row<'a, Message, Theme, Renderer> +where + Renderer: iced::core::Renderer, +{ + /// Creates an empty [`Row`]. + pub fn new() -> Self { + Self::from_vec(Vec::new()) + } + + /// Creates a [`Row`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + + /// Creates a [`Row`] with the given elements. + pub fn with_children( + children: impl IntoIterator>, + ) -> Self { + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) + } + + /// Creates a [`Row`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`Row`] will not inspect the [`Vec`], which means + /// it won't automatically adapt to the sizing strategy of its contents. + /// + /// If any of the children have a [`Length::Fill`] strategy, you will need to + /// call [`Row::width`] or [`Row::height`] accordingly. + pub fn from_vec(children: Vec>) -> Self { + Self { + spacing: 0.0, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + align: Alignment::Start, + clip: false, + children, + } + } + + /// Sets the horizontal spacing _between_ elements. + /// + /// Custom margins per element do not exist in iced. You should use this + /// method instead! While less flexible, it helps you keep spacing between + /// elements consistent. + pub fn spacing(mut self, amount: impl Into) -> Self { + self.spacing = amount.into().0; + self + } + + /// Sets the [`Padding`] of the [`Row`]. + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`Row`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Row`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the vertical alignment of the contents of the [`Row`] . + pub fn align_y(mut self, align: impl Into) -> Self { + self.align = Alignment::from(align.into()); + self + } + + /// Sets whether the contents of the [`Row`] should be clipped on + /// overflow. + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; + self + } + + /// Adds an [`Element`] to the [`Row`]. + pub fn push(mut self, child: impl Into>) -> Self { + let child = child.into(); + let child_size = child.as_widget().size_hint(); + + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); + + self.children.push(child); + self + } + + /// Adds an element to the [`Row`], if `Some`. + pub fn push_maybe( + self, + child: Option>>, + ) -> Self { + if let Some(child) = child { + self.push(child) + } else { + self + } + } + + /// Extends the [`Row`] with the given children. + pub fn extend( + self, + children: impl IntoIterator>, + ) -> Self { + children.into_iter().fold(self, Self::push) + } +} + +impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> +where + Renderer: iced::core::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message, Theme, Renderer: iced::core::Renderer> + FromIterator> for Row<'a, Message, Theme, Renderer> +{ + fn from_iter>>(iter: T) -> Self { + Self::with_children(iter) + } +} + +impl Widget + for Row<'_, Message, Theme, Renderer> +where + Renderer: iced::core::Renderer, +{ + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn state(&self) -> widget::tree::State { + widget::tree::State::new(State::default()) + } + + fn tag(&self) -> widget::tree::Tag { + widget::tree::Tag::of::() + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(&mut self.children); + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout::flex::resolve( + layout::flex::Axis::Horizontal, + renderer, + limits, + self.width, + self.height, + self.padding, + self.spacing, + self.align, + &self.children, + &mut tree.children, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), c_layout)| { + child.as_widget().operate( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + renderer, + operation, + ); + }); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let my_state = tree.state.downcast_mut::(); + + if let Some(hovered) = my_state.hovered { + let child_layout = layout.children().nth(hovered); + if let Some(child_layout) = child_layout + && cursor.is_over(child_layout.bounds()) + { + // if mouse event, we can skip checking other children + if let Event::Mouse(e) = &event { + if !matches!( + e, + mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. } + ) { + return self.children[hovered].as_widget_mut().on_event( + &mut tree.children[hovered], + event, + child_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + } else if let Event::Touch(t) = &event { + if !matches!( + t, + iced::core::touch::Event::FingerLifted { .. } + | iced::core::touch::Event::FingerLost { .. } + ) { + return self.children[hovered].as_widget_mut().on_event( + &mut tree.children[hovered], + event, + child_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + } + } else { + my_state.hovered = None; + } + } + + self.children + .iter_mut() + .enumerate() + .zip(&mut tree.children) + .zip(layout.children()) + .map(|(((i, child), state), c_layout)| { + if matches!( + event, + Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) + | Event::Touch( + iced_core::touch::Event::FingerMoved { .. } + | iced_core::touch::Event::FingerPressed { .. } + ) + ) && cursor.is_over(c_layout.bounds()) + { + my_state.hovered = Some(i); + } + + child.as_widget_mut().on_event( + state, + event.clone(), + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, state), c_layout)| { + child.as_widget().mouse_interaction( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + let viewport = if self.clip { + &clipped_viewport + } else { + viewport + }; + + for ((child, state), c_layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .filter(|(_, layout)| layout.bounds().intersects(viewport)) + { + child.as_widget().draw( + state, + renderer, + theme, + style, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + viewport, + ); + } + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + overlay::from_children(&mut self.children, tree, layout, renderer, translation) + } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::A11yTree; + A11yTree::join( + self.children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + .map(|((c, c_layout), state)| { + c.as_widget().a11y_nodes( + c_layout.with_virtual_offset(layout.virtual_offset()), + state, + cursor, + ) + }), + ) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced::core::clipboard::DndDestinationRectangles, + ) { + for ((e, c_layout), state) in self + .children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + { + e.as_widget().drag_destinations( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + renderer, + dnd_rectangles, + ); + } + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: iced::core::Renderer + 'a, +{ + fn from(row: Row<'a, Message, Theme, Renderer>) -> Self { + Self::new(row) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct State { + hovered: Option, +}