diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 07c5f874..830746a1 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -7,6 +7,7 @@ pub mod portal; pub mod style; +use ::iced::Alignment; use cosmic_config::{CosmicConfigEntry, config_subscription}; use cosmic_theme::{Component, LayeredTheme, Spacing, ThemeMode}; use iced_futures::Subscription; @@ -47,6 +48,7 @@ pub(crate) static THEME: Mutex = Mutex::new(Theme { theme_type: ThemeType::Dark, layer: cosmic_theme::Layer::Background, transparent: false, + list_item_position: None, }); /// Currently-defined theme. @@ -83,34 +85,6 @@ pub fn is_high_contrast() -> bool { active_type().is_high_contrast() } -// /// Watches for changes to the system's theme preference. -// #[cold] -// pub fn subscription(is_dark: bool) -> Subscription { -// config_subscription::<_, crate::cosmic_theme::Theme>( -// ( -// std::any::TypeId::of::(), -// is_dark, -// ), -// if is_dark { -// cosmic_theme::DARK_THEME_ID -// } else { -// cosmic_theme::LIGHT_THEME_ID -// } -// .into(), -// crate::cosmic_theme::Theme::VERSION, -// ) -// .map(|res| { -// for error in res.errors.into_iter().filter(cosmic_config::Error::is_err) { -// tracing::error!( -// ?error, -// "error while watching system theme preference changes" -// ); -// } - -// Theme::system(Arc::new(res.config)) -// }) -// } - pub fn system_dark() -> Theme { let Ok(helper) = crate::cosmic_theme::Theme::dark_config() else { return Theme::dark(); @@ -211,6 +185,8 @@ pub struct Theme { pub theme_type: ThemeType, pub layer: cosmic_theme::Layer, pub transparent: bool, + /// Only meaningful for widgets that must be in a list. Otherwise it should be ignored. + pub list_item_position: Option<(Alignment, usize)>, } impl Theme { @@ -281,9 +257,9 @@ impl Theme { /// can be used in a component that is intended to be a child of a `CosmicContainer` pub fn current_container(&self) -> &cosmic_theme::Container { match self.layer { - cosmic_theme::Layer::Background => &self.cosmic().background(self.transparent), - cosmic_theme::Layer::Primary => &self.cosmic().primary(self.transparent), - cosmic_theme::Layer::Secondary => &self.cosmic().secondary(self.transparent), + cosmic_theme::Layer::Background => self.cosmic().background(self.transparent), + cosmic_theme::Layer::Primary => self.cosmic().primary(self.transparent), + cosmic_theme::Layer::Secondary => self.cosmic().secondary(self.transparent), } } @@ -292,6 +268,14 @@ impl Theme { pub fn set_theme(&mut self, theme: ThemeType) { self.theme_type = theme; } + + #[inline] + /// Clone theme with a list position + pub fn with_list_item_position(&self, position: Option<(Alignment, usize)>) -> Self { + let mut new = self.clone(); + new.list_item_position = position; + new + } } impl LayeredTheme for Theme { diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index cca54fd2..aa2c8a78 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -6,6 +6,7 @@ //! //! A [`Button`] has some local [`State`]. +use iced::Alignment; use iced_runtime::core::widget::Id; use iced_runtime::{Action, Task, keyboard, task}; @@ -449,7 +450,7 @@ impl<'a, Message: 'a + Clone> Widget let state = tree.state.downcast_ref::(); - let styling = if !is_enabled { + let mut styling = if !is_enabled { theme.disabled(&self.style) } else if is_mouse_over { if state.is_pressed { @@ -471,6 +472,21 @@ impl<'a, Message: 'a + Clone> Widget theme.active(state.is_focused, self.selected, &self.style) }; + if matches!(self.style, crate::theme::Button::MenuItem) { + match theme.list_item_position { + Some((Alignment::Start, _)) => { + styling.border_radius = + styling.border_radius.bottom(theme.cosmic().radius_0()[3]); + } + Some((Alignment::End, _)) => { + styling.border_radius = styling.border_radius.top(theme.cosmic().radius_0()[0]); + } + Some((Alignment::Center, _)) => {} + None => { + styling.border_radius = theme.cosmic().radius_0().into(); + } + }; + } let mut icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color); diff --git a/src/widget/menu.rs b/src/widget/menu.rs index 9d4ce4b1..a452e2b8 100644 --- a/src/widget/menu.rs +++ b/src/widget/menu.rs @@ -66,6 +66,8 @@ mod menu_bar; pub(crate) use menu_bar::MenuBarState; pub use menu_bar::{MenuBar, menu_bar as bar}; +pub mod menu_column; + mod menu_inner; mod menu_tree; pub use menu_tree::{ diff --git a/src/widget/menu/menu_column.rs b/src/widget/menu/menu_column.rs new file mode 100644 index 00000000..ecb4a129 --- /dev/null +++ b/src/widget/menu/menu_column.rs @@ -0,0 +1,411 @@ +//! Distribute content vertically. +use crate::iced; +use iced::core::alignment::{self, Alignment}; +use iced::core::event::{self, Event}; +use iced::core::widget::{Operation, Tree}; +use iced::core::{ + Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, + layout, mouse, overlay, renderer, widget, +}; + +#[allow(missing_debug_implementations)] +#[must_use] +pub struct MenuColumn<'a, Message, Renderer = iced::Renderer> { + spacing: f32, + padding: Padding, + width: Length, + height: Length, + max_width: f32, + align: Alignment, + clip: bool, + children: Vec>, +} + +impl<'a, Message, Renderer> MenuColumn<'a, Message, Renderer> +where + Renderer: iced::core::Renderer, +{ + /// Creates an empty [`MenuColumn`]. + pub fn new() -> Self { + Self::from_vec(Vec::new()) + } + + /// Creates a [`MenuColumn`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + + /// Creates a [`MenuColumn`] 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 [`MenuColumn`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`MenuColumn`] 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 [`MenuColumn::width`] or [`MenuColumn::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 [`MenuColumn`]. + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`MenuColumn`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`MenuColumn`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the maximum width of the [`MenuColumn`]. + 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 [`MenuColumn`] . + pub fn align_x(mut self, align: impl Into) -> Self { + self.align = Alignment::from(align.into()); + self + } + + /// Sets whether the contents of the [`MenuColumn`] should be clipped on + /// overflow. + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; + self + } + + /// Adds an element to the [`MenuColumn`]. + 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 [`MenuColumn`], if `Some`. + #[must_use] + pub fn push_maybe( + self, + child: Option>>, + ) -> Self { + if let Some(child) = child { + self.push(child) + } else { + self + } + } + + /// Extends the [`MenuColumn`] with the given children. + pub fn extend( + self, + children: impl IntoIterator>, + ) -> Self { + children.into_iter().fold(self, Self::push) + } +} + +impl Default for MenuColumn<'_, Message, Renderer> +where + Renderer: iced::core::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message, Renderer: iced::core::Renderer> + FromIterator> + for MenuColumn<'a, Message, Renderer> +{ + fn from_iter>>( + iter: T, + ) -> Self { + Self::with_children(iter) + } +} + +impl Widget + for MenuColumn<'_, Message, Renderer> +where + Renderer: iced::core::Renderer, +{ + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + 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( + &mut 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, + &mut self.children, + &mut tree.children, + ) + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { + self.children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), c_layout)| { + child.as_widget_mut().operate( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + renderer, + operation, + ); + }); + }); + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + for (((i, child), state), c_layout) in self + .children + .iter_mut() + .enumerate() + .zip(&mut tree.children) + .zip(layout.children()) + { + child.as_widget_mut().update( + state, + &event, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + } + + 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: &crate::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 (i, ((child, state), c_layout)) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .filter(|(_, layout)| layout.bounds().intersects(viewport)) + .enumerate() + { + let t = theme.with_list_item_position(if self.children.len() == 1 { + Some((Alignment::Center, i)) + } else if 0 == i { + Some((Alignment::Start, i)) + } else if i == self.children.len() - 1 { + Some((Alignment::End, i)) + } else { + None + }); + child.as_widget().draw( + state, + renderer, + &t, + style, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + viewport, + ); + } + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'b>, + renderer: &Renderer, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + 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, Renderer> From> + for Element<'a, Message, crate::Theme, Renderer> +where + Message: 'a, + Renderer: iced::core::Renderer + 'a, +{ + fn from(column: MenuColumn<'a, Message, Renderer>) -> Self { + Self::new(column) + } +} diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 6211e201..bb33be5e 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -16,7 +16,7 @@ use super::menu_tree::MenuTree; use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::style::menu_bar::StyleSheet; -use iced::window; +use iced::{Alignment, window}; use iced_core::{Border, Renderer as IcedRenderer, Shadow, Widget}; use iced_widget::core::layout::{Limits, Node}; use iced_widget::core::mouse::{self, Cursor}; @@ -836,12 +836,25 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { // draw item menu_roots[start_index..=end_index] .iter() + .enumerate() .zip(children_layout.children()) - .for_each(|(mt, clo)| { + .for_each(|((i, mt), clo)| { + let t = theme.with_list_item_position( + if start_index == end_index { + Some((Alignment::Center, i)) + } else if 0 == i { + Some((Alignment::Start, i)) + } else if i == end_index - start_index { + Some((Alignment::End, i)) + } else { + None + }, + ); + mt.item.draw( &state.tree.children[active_root[0]].children[mt.index], r, - theme, + &t, style, clo, view_cursor,