diff --git a/Cargo.toml b/Cargo.toml index 3ada985..b3bfbd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,12 @@ branch = "cosmic-design-system" # path = "../iced" features = ["cosmic-theme", "svg"] +[dependencies.iced_style] +git = "https://github.com/pop-os/iced.git" +branch = "cosmic-design-system" +# path = "../iced/native" +features = ["cosmic-theme"] + [dependencies.iced_native] git = "https://github.com/pop-os/iced.git" branch = "cosmic-design-system" diff --git a/examples/cosmic/src/main.rs b/examples/cosmic/src/main.rs index e781d1e..3464f89 100644 --- a/examples/cosmic/src/main.rs +++ b/examples/cosmic/src/main.rs @@ -5,10 +5,9 @@ use cosmic::{ list_row, list_section, list_view, - nav_bar, nav_button, toggler, - HeaderBar, + HeaderBar, nav_bar_style, }, settings, iced::{self, theme, Alignment, Application, Color, Command, Element, Length, Theme}, @@ -127,30 +126,23 @@ impl Application for Window { let content = responsive(|size| { let condensed = size.width < 900.0; - let sidebar: Option> = if self.headerbar.sidebar_active { - Some(nav_bar!( - //TODO: Support symbolic icons - nav_button!("network-wireless", "Wi-Fi", condensed) - .on_press(Message::Page(0)) - .style(if self.page == 0 { theme::Button::Primary } else { theme::Button::Text }) - , - nav_button!("preferences-desktop", "Desktop", condensed) - .on_press(Message::Page(1)) - .style(if self.page == 1 { theme::Button::Primary } else { theme::Button::Text }) - , - nav_button!("system-software-update", "OS Upgrade & Recovery", condensed) - .on_press(Message::Page(2)) - .style(if self.page == 2 { theme::Button::Primary } else { theme::Button::Text }) - ) - .max_width(if condensed { - 100 - } else { - 300 - }) - .into()) - } else { - None - }; + let sidebar: Element<_> = cosmic::navbar![ + nav_button!("network-wireless", "Wi-Fi", condensed) + .on_press(Message::Page(0)) + .style(if self.page == 0 { theme::Button::Primary } else { theme::Button::Text }) + , + nav_button!("preferences-desktop", "Desktop", condensed) + .on_press(Message::Page(1)) + .style(if self.page == 1 { theme::Button::Primary } else { theme::Button::Text }) + , + nav_button!("system-software-update", "OS Upgrade & Recovery", condensed) + .on_press(Message::Page(2)) + .style(if self.page == 2 { theme::Button::Primary } else { theme::Button::Text }) + ] + .active(self.headerbar.sidebar_active) + .condensed(condensed) + .style(theme::Container::Custom(nav_bar_style)) + .into(); let choose_theme = [Theme::Light, Theme::Dark].iter().fold( row![text("Debug theme:")].spacing(10).align_items(Alignment::Center), @@ -254,9 +246,7 @@ impl Application for Window { let mut widgets = Vec::with_capacity(2); - if let Some(sidebar) = sidebar { - widgets.push(if self.debug { sidebar.explain(Color::WHITE) } else { sidebar }); - } + widgets.push(if self.debug { sidebar.explain(Color::WHITE) } else { sidebar }); widgets.push( scrollable!(row![ diff --git a/src/widget/mod.rs b/src/widget/mod.rs index f26a0ca..e612af9 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -13,6 +13,9 @@ pub use list::*; mod nav; pub use nav::*; +mod navbar; +pub use navbar::*; + mod toggler; pub use toggler::*; diff --git a/src/widget/nav.rs b/src/widget/nav.rs index ac9719d..f1a393e 100644 --- a/src/widget/nav.rs +++ b/src/widget/nav.rs @@ -45,12 +45,12 @@ macro_rules! nav_button { ($icon: expr, $title:expr, $condensed:expr) => ({ if $condensed { $crate::iced::widget::Button::new( - $crate::widget::icon($icon, 20) + $crate::widget::icon($icon, 22) ) .padding(8) } else { $crate::widget::button!( - $crate::widget::icon($icon, 20), + $crate::widget::icon($icon, 22), $crate::iced::widget::Text::new($title), $crate::iced::widget::horizontal_space( $crate::iced::Length::Fill diff --git a/src/widget/navbar.rs b/src/widget/navbar.rs new file mode 100644 index 0000000..ba78988 --- /dev/null +++ b/src/widget/navbar.rs @@ -0,0 +1,347 @@ +use iced::{ + Element, + Padding, + Length, + Alignment, + widget::{ + Container, + Column, + scrollable + }, + alignment +}; +use iced_native::{ + Widget, + widget::{ + Tree, + container::{ + layout, + draw_background + } + }, + row, + renderer +}; +use iced_style::container::StyleSheet; + +pub struct NavBar<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, + Renderer::Theme: StyleSheet, +{ + spacing: u16, + padding: Padding, + width: Length, + height: Length, + max_width: u32, + max_height: u32, + align_items: Alignment, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + style: ::Style, + condensed: bool, + active: bool, + content: Element<'a, Message, Renderer>, +} + +impl<'a, Message: 'a, Renderer> NavBar<'a, Message, Renderer> +where + Renderer: iced_native::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + pub fn new() -> Self { + Self::default() + } + + /// Creates a [`NavBar`] with the given elements. + pub fn with_children( + children: Vec>, + ) -> Self where ::Theme: iced_style::scrollable::StyleSheet { + let nav = Self::default(); + NavBar { + content: Container::new( + scrollable( + row![ + Column::with_children(children) + .spacing(nav.spacing) + .padding(nav.padding) + ] + ) + .scrollbar_width(6) + .scroller_width(6) + ) + .into(), + ..Default::default() + } + } + + pub fn condensed(mut self, condensed: bool) -> Self { + self.condensed = condensed; + self + } + + pub fn active(mut self, active: bool) -> Self { + self.active = active; + self + } + /// 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, units: u16) -> Self { + self.spacing = units; + self + } + + /// Sets the [`Padding`] of the [`NavBar`]. + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`NavBar`]. + pub fn width(mut self, width: iced::Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`NavBar`]. + pub fn height(mut self, height: iced::Length) -> Self { + self.height = height; + self + } + + /// Sets the vertical alignment of the contents of the [`NavBar`] . + pub fn align_items(mut self, align: iced::Alignment) -> Self { + self.align_items = align; + self + } + + /// Sets the maximum width of the [`NavBar`]. + pub fn max_width(mut self, max_width: u32) -> Self { + self.max_width = max_width; + self + } + + /// Sets the maximum height of the [`NavBar`]. + pub fn max_height(mut self, max_height: u32) -> Self { + self.max_height = max_height; + self + } + + /// Sets the style of the [`NavBar`]. + pub fn style( + mut self, + style: impl Into<::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +impl<'a, Message: 'a, Renderer> Default for NavBar<'a, Message, Renderer> +where + Renderer: iced_native::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn default() -> Self { + Self { + spacing: 12, + padding: Padding::new(12), + width: Length::Fill, + height: Length::Fill, + max_width: 300, + max_height: u32::MAX, + align_items: Alignment::Start, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + style: Default::default(), + condensed: false, + active: true, + content: Container::new(row![ + Column::new() + ]).into(), + } + } +} + +impl<'a, Message, Renderer> Widget + for NavBar<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, + Renderer::Theme: StyleSheet, +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &iced_native::layout::Limits, + ) -> iced_native::layout::Node { + layout( + renderer, + limits, + self.width, + self.height, + if self.condensed { + 100 + } else { + self.max_width + }, + self.max_height, + if self.active { + self.padding + } else { + Padding::ZERO + }, + self.horizontal_alignment, + self.vertical_alignment, + |renderer, limits| { + if self.active { + self.content.as_widget().layout(renderer, limits) + } else { + let content: Element = Container::new(row![]).into(); + content.as_widget().layout(renderer, limits) + } + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: iced_native::Layout<'_>, + operation: &mut dyn iced_native::widget::Operation, + ) { + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + operation, + ); + }); + } + + fn on_event( + &mut self, + tree: &mut iced_native::widget::Tree, + event: iced::Event, + layout: iced_native::Layout<'_>, + cursor_position: iced::Point, + renderer: &Renderer, + clipboard: &mut dyn iced_native::Clipboard, + shell: &mut iced_native::Shell<'_, Message>, + ) -> iced::event::Status { + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn mouse_interaction( + &self, + tree: &iced_native::widget::Tree, + layout: iced_native::Layout<'_>, + cursor_position: iced::Point, + viewport: &iced::Rectangle, + renderer: &Renderer, + ) -> iced_native::mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout.children().next().unwrap(), + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + renderer_style: &iced_native::renderer::Style, + layout: iced_native::Layout<'_>, + cursor_position: iced::Point, + viewport: &iced::Rectangle, + ) { + if self.active { + let style = theme.appearance(self.style); + + draw_background(renderer, &style, layout.bounds()); + + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + &renderer::Style { + text_color: style + .text_color + .unwrap_or(renderer_style.text_color), + }, + layout.children().next().unwrap(), + cursor_position, + viewport, + ); + } + } + + fn overlay<'b>( + &'b self, + tree: &'b mut iced_native::widget::Tree, + layout: iced_native::Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content.as_widget().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } + +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: iced_native::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(row: NavBar<'a, Message, Renderer>) -> Self { + Self::new(row) + } +} + +/// Creates a [Row`] with the given children. +/// +/// [`Row`]: widget::Row +#[macro_export] +macro_rules! navbar { + () => ( + $crate::widget::NavBar::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::widget::NavBar::with_children(vec![$($crate::iced::Element::from($x)),+]) + ); +} \ No newline at end of file