diff --git a/cosmic-theme/src/model/mode.rs b/cosmic-theme/src/model/mode.rs index b2ad99f9..f910ba26 100644 --- a/cosmic-theme/src/model/mode.rs +++ b/cosmic-theme/src/model/mode.rs @@ -1,4 +1,4 @@ -use cosmic_config::{Config, ConfigGet, ConfigSet, CosmicConfigEntry}; +use cosmic_config::{Config, ConfigGet, CosmicConfigEntry}; /// ID for the ThemeMode config pub const THEME_MODE_ID: &str = "com.system76.CosmicTheme.Mode"; diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 987314d8..ce314c80 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -157,19 +157,32 @@ impl Theme { pub fn accent_color(&self) -> Srgba { self.accent.base.clone() } + /// get @success_color pub fn success_color(&self) -> Srgba { self.success.base.clone() } + /// get @destructive_color pub fn destructive_color(&self) -> Srgba { self.destructive.base.clone() } + /// get @warning_color pub fn warning_color(&self) -> Srgba { self.warning.base.clone() } + /// get @small_container_widget + pub fn small_container_widget(&self) -> Srgba { + self.palette.gray_2.clone() + } + + /// get @small_widget_divider + pub fn small_widget_divider(&self) -> Srgba { + self.palette.neutral_9.clone() + } + // Containers /// get @bg_color pub fn bg_color(&self) -> Srgba { diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index c3c87a64..8496afdb 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -64,7 +64,7 @@ impl StyleSheet for Theme { Appearance { border_radius: cosmic.corner_radii.radius_0.into(), inactive: ItemStatusAppearance { - background: Some(Background::Color(neutral_5.into())), + background: Some(Background::Color(cosmic.small_container_widget().into())), first: ItemAppearance { border_radius: Radius::from([rad_m[0], rad_0[1], rad_0[2], rad_m[3]]), ..Default::default() diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 543d453a..096dd55c 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -27,18 +27,23 @@ pub fn nav_bar( where Message: Clone + 'static, { + let theme = crate::theme::active(); + let space_s = theme.cosmic().space_s(); + let space_xs = theme.cosmic().space_xs(); + let space_xxs = theme.cosmic().space_xxs(); + segmented_button::vertical(model) .button_height(32) - .button_padding([16, 10, 16, 10]) - .button_spacing(8) + .button_padding([space_s, space_xs, space_s, space_xs]) + .button_spacing(space_xxs) + .spacing(space_xxs) .on_activate(on_activate) - .spacing(8) .style(crate::theme::SegmentedButton::ViewSwitcher) .apply(scrollable) .height(Length::Fill) .apply(container) + .padding(space_xxs) .height(Length::Fill) - .padding(11) .style(theme::Container::custom(nav_bar_style)) } diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 848273bc..9fd74ae2 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -3,9 +3,9 @@ //! Implementation details for the horizontal layout of a segmented button. -use super::model::{Entity, Model, Selectable}; +use super::model::{Model, Selectable}; use super::style::StyleSheet; -use super::widget::{LocalState, SegmentedButton, SegmentedVariant}; +use super::widget::{ItemBounds, LocalState, SegmentedButton, SegmentedVariant}; use iced::{Length, Rectangle, Size}; use iced_core::layout; @@ -21,7 +21,6 @@ pub struct Horizontal; /// Horizontal implementation of the [`SegmentedButton`]. /// /// For details on the model, see the [`segmented_button`](super) module for more details. -#[must_use] pub fn horizontal( model: &Model, ) -> SegmentedButton @@ -45,11 +44,11 @@ where } #[allow(clippy::cast_precision_loss)] - fn variant_button_bounds<'b>( + fn variant_bounds<'b>( &'b self, state: &'b LocalState, mut bounds: Rectangle, - ) -> Box + 'b> { + ) -> Box + 'b> { let num = state.buttons_visible; let spacing = f32::from(self.spacing); let mut homogenous_width = 0.0; @@ -66,6 +65,8 @@ where / num as f32; } + let segmented_selection = matches!(self.style, crate::theme::SegmentedButton::Selection); + Box::new( self.model .order @@ -74,17 +75,32 @@ where .enumerate() .skip(state.buttons_offset) .take(state.buttons_visible) - .map(move |(nth, key)| { - let mut this_bounds = bounds; + .flat_map(move |(nth, key)| { + let mut layout_bounds = bounds; + + let layout_size = &state.internal_layout[nth].0; if !state.collapsed && Length::Shrink == self.width { - this_bounds.width = state.internal_layout[nth].width; + layout_bounds.width = layout_size.width; } else { - this_bounds.width = homogenous_width; + layout_bounds.width = homogenous_width; } - bounds.x += this_bounds.width + spacing; - (key, this_bounds) + bounds.x += layout_bounds.width + spacing; + + let button_bounds = ItemBounds::Button(key, layout_bounds); + let mut divider = None; + + if self.dividers && segmented_selection && nth + 1 < num { + divider = Some(ItemBounds::Divider(Rectangle { + width: 1.0, + ..bounds + })); + + bounds.x += 1.0; + } + + std::iter::once(button_bounds).chain(divider) }), ) } @@ -97,9 +113,9 @@ where state: &mut LocalState, renderer: &crate::Renderer, limits: &layout::Limits, - ) -> layout::Node { + ) -> Size { + state.internal_layout.clear(); let num = self.model.order.len(); - let mut total_width = 0.0; let spacing = f32::from(self.spacing); let limits = limits.width(self.width); let mut size; @@ -116,31 +132,20 @@ where if let Length::Shrink = self.width { // Buttons will be rendered at their smallest widths possible. - state.internal_layout.clear(); - - let font = renderer.default_font(); - let mut total_height = 0.0f32; - - for &button in &self.model.order { - let (mut width, height) = self.button_dimensions(state, font, button); - width = f32::from(self.minimum_button_width).max(width); - total_width += width + spacing; - total_height = total_height.max(height); - - state.internal_layout.push(Size::new(width, height)); - } + let max_height = self.max_button_dimensions(state, renderer).1; // Get the max available width for placing buttons into. - let max_size = limits.height(Length::Fixed(total_height)).resolve( + let max_size = limits.height(Length::Fixed(max_height)).resolve( Length::Fill, - total_height, - Size::new(f32::MAX, total_height), + max_height, + Size::new(f32::MAX, max_height), ); - let mut visible_width = f32::from(self.button_height) * 2.0; + // let mut visible_width = f32::from(self.button_height) * 2.0; + let mut visible_width = 0.0; state.buttons_visible = 0; - for button_size in &state.internal_layout { + for (button_size, _actual_size) in &state.internal_layout { visible_width += button_size.width; if max_size.width >= visible_width { @@ -152,27 +157,48 @@ where visible_width += spacing; } + visible_width -= spacing; + state.collapsed = num > 1 && state.buttons_visible != num; // If collapsed, use the maximum width available. - visible_width = if state.collapsed { - max_size.width - } else { - total_width - }; + if state.collapsed { + visible_width = max_size.width; + } size = limits .width(Length::Fixed(visible_width)) - .height(Length::Fixed(total_height)) + .height(Length::Fixed(max_height)) .resolve( visible_width, - total_height, - Size::new(visible_width, total_height), + max_height, + Size::new(visible_width, max_height), ); } else { // Buttons will be rendered with equal widths. state.buttons_visible = self.model.items.len(); - let (width, height) = self.max_button_dimensions(state, renderer, limits.max()); + + let mut width = 0.0f32; + let mut height = 0.0f32; + let font = renderer.default_font(); + + for key in self.model.order.iter().copied() { + let (button_width, button_height) = self.button_dimensions(state, font, key); + + state.internal_layout.push(( + Size::new(button_width, button_height), + Size::new( + button_width + - f32::from(self.button_padding[0]) + - f32::from(self.button_padding[2]), + button_height, + ), + )); + + height = height.max(button_height); + width = width.max(button_width); + } + let total_width = (state.buttons_visible as f32) * (width + spacing); size = limits.height(Length::Fixed(height)).resolve( @@ -201,6 +227,6 @@ where state.buttons_offset = 0; } - layout::Node::new(size) + size } } diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 9fac6aa4..adadaea1 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -5,9 +5,9 @@ use super::model::{Entity, Model, Selectable}; use super::style::StyleSheet; -use super::widget::{LocalState, SegmentedButton, SegmentedVariant}; +use super::widget::{ItemBounds, LocalState, SegmentedButton, SegmentedVariant}; -use iced::{Length, Rectangle, Size}; +use iced::{Alignment, Length, Rectangle, Size}; use iced_core::layout; /// A type marker defining the vertical variant of a [`SegmentedButton`]. @@ -45,11 +45,11 @@ where } #[allow(clippy::cast_precision_loss)] - fn variant_button_bounds<'b>( + fn variant_bounds<'b>( &'b self, state: &'b LocalState, mut bounds: Rectangle, - ) -> Box + 'b> { + ) -> Box + 'b> { let spacing = f32::from(self.spacing); Box::new( @@ -58,11 +58,16 @@ where .iter() .copied() .enumerate() - .map(move |(_nth, key)| { - let mut this_bounds = bounds; - this_bounds.height = state.internal_layout[0].height; - bounds.y += this_bounds.height + spacing; - (key, this_bounds) + .map(move |(nth, key)| { + let mut layout_bounds = bounds; + + let layout_size = state.internal_layout[nth].0; + + layout_bounds.height = layout_size.height; + + bounds.y += layout_bounds.height + spacing; + + ItemBounds::Button(key, layout_bounds) }), ) } @@ -75,11 +80,16 @@ where state: &mut LocalState, renderer: &crate::Renderer, limits: &layout::Limits, - ) -> layout::Node { + ) -> Size { state.internal_layout.clear(); let limits = limits.width(self.width); - let (width, mut height) = self.max_button_dimensions(state, renderer, limits.max()); - state.internal_layout.push(Size::new(width, height)); + + let (width, mut height) = self.max_button_dimensions(state, renderer); + + for (size, actual) in &mut state.internal_layout { + size.width = width; + actual.width = height; + } let num = self.model.items.len(); let spacing = f32::from(self.spacing); @@ -87,12 +97,11 @@ where if num != 0 { height = (num as f32 * height) + (num as f32 * spacing) - spacing; } - let size = limits.height(Length::Fixed(height)).resolve( + + limits.height(Length::Fixed(height)).resolve( self.width, self.height, Size::new(width, height), - ); - - layout::Node::new(size) + ) } } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 7a888e20..4e411405 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -7,8 +7,8 @@ use crate::widget::{icon, Icon}; use crate::{Element, Renderer}; use derive_setters::Setters; use iced::{ - alignment, event, keyboard, mouse, touch, Background, Color, Command, Event, Length, Rectangle, - Size, + alignment, event, keyboard, mouse, touch, Alignment, Background, Color, Command, Event, Length, + Padding, Rectangle, Size, }; use iced_core::mouse::ScrollDelta; use iced_core::text::{LineHeight, Paragraph, Renderer as TextRenderer, Shaping}; @@ -24,6 +24,11 @@ pub fn focus(id: Id) -> Command { Command::widget(operation::focusable::focus(id.0)) } +pub(super) enum ItemBounds { + Button(Entity, Rectangle), + Divider(Rectangle), +} + /// Isolates variant-specific behaviors from [`SegmentedButton`]. pub trait SegmentedVariant { /// Get the appearance for this variant of the widget. @@ -33,11 +38,11 @@ pub trait SegmentedVariant { ) -> super::Appearance; /// Calculates the bounds for visible buttons. - fn variant_button_bounds<'b>( + fn variant_bounds<'b>( &'b self, state: &'b LocalState, bounds: Rectangle, - ) -> Box + 'b>; + ) -> Box + 'b>; /// Calculates the layout of this variant. fn variant_layout( @@ -45,7 +50,7 @@ pub trait SegmentedVariant { state: &mut LocalState, renderer: &crate::Renderer, limits: &layout::Limits, - ) -> layout::Node; + ) -> Size; } /// A conjoined group of items that function together as a button. @@ -67,6 +72,13 @@ where pub(super) scrollable_focus: bool, /// Show the close icon only when item is hovered. pub(super) show_close_icon_on_hover: bool, + /// Padding of the whole widget. + #[setters(into)] + pub(super) padding: Padding, + /// Whether to place dividers between buttons. + pub(super) dividers: bool, + /// Alignment of button contents. + pub(super) button_alignment: Alignment, /// Padding around a button. pub(super) button_padding: [u16; 4], /// Desired height of a button. @@ -119,9 +131,12 @@ where close_icon: icon::from_name("window-close-symbolic").size(16).icon(), scrollable_focus: false, show_close_icon_on_hover: false, - button_padding: [4, 4, 4, 4], + button_alignment: Alignment::Start, + padding: Padding::from(0.0), + dividers: false, + button_padding: [0, 0, 0, 0], button_height: 32, - button_spacing: 4, + button_spacing: 0, minimum_button_width: 150, indent_spacing: 16, font_active: None, @@ -338,14 +353,17 @@ where // Add icon to measurement if icon was given. if let Some(icon) = self.model.icon(button) { - height = height.max(f32::from(icon.size)); width += f32::from(icon.size) + f32::from(self.button_spacing); + } else if self.model.is_active(button) { + // Add selection icon measurements when widget is a selection widget. + if let crate::theme::SegmentedButton::Selection = self.style { + width += 16.0 + f32::from(self.button_spacing); + } } // Add close button to measurement if found. if self.model.is_closable(button) { - height = height.max(f32::from(self.close_icon.size)); - width += f32::from(self.close_icon.size) + f32::from(self.button_spacing) + 8.0; + width += f32::from(self.close_icon.size) + f32::from(self.button_spacing); } // Add button padding to the max size found @@ -360,7 +378,6 @@ where &self, state: &mut LocalState, renderer: &Renderer, - _bounds: Size, ) -> (f32, f32) { let mut width = 0.0f32; let mut height = 0.0f32; @@ -369,10 +386,25 @@ where for key in self.model.order.iter().copied() { let (button_width, button_height) = self.button_dimensions(state, font, key); + state.internal_layout.push(( + Size::new(button_width, button_height), + Size::new( + button_width + - f32::from(self.button_padding[0]) + - f32::from(self.button_padding[2]), + button_height, + ), + )); + height = height.max(button_height); width = width.max(button_width); } + for (size, actual) in &mut state.internal_layout { + size.height = height; + actual.height = height; + } + (width, height) } } @@ -436,7 +468,12 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.variant_layout(tree.state.downcast_mut::(), renderer, limits) + let state = tree.state.downcast_mut::(); + let limits = limits.shrink(self.padding); + let size = self + .variant_layout(state, renderer, &limits) + .expand(self.padding); + layout::Node::new(size) } #[allow(clippy::too_many_lines)] @@ -453,17 +490,14 @@ where ) -> event::Status { let bounds = layout.bounds(); let state = tree.state.downcast_mut::(); + state.hovered = Item::None; if cursor_position.is_over(bounds) { // Check for clicks on the previous and next tab buttons, when tabs are collapsed. if state.collapsed { // Check if the prev tab button was clicked. - if cursor_position.is_over(Rectangle { - x: bounds.x, - y: bounds.y, - width: f32::from(self.button_height), - height: f32::from(self.button_height), - }) && self.prev_tab_sensitive(state) + if cursor_position.is_over(prev_tab_bounds(&bounds, f32::from(self.button_height))) + && self.prev_tab_sensitive(state) { state.hovered = Item::PrevButton; if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) @@ -473,12 +507,9 @@ where } } else { // Check if the next tab button was clicked. - if cursor_position.is_over(Rectangle { - x: bounds.x + bounds.width - f32::from(self.button_height), - y: bounds.y, - width: f32::from(self.button_height), - height: f32::from(self.button_height), - }) && self.next_tab_sensitive(state) + if cursor_position + .is_over(next_tab_bounds(&bounds, f32::from(self.button_height))) + && self.next_tab_sensitive(state) { state.hovered = Item::NextButton; @@ -492,7 +523,11 @@ where } for (key, bounds) in self - .variant_button_bounds(state, bounds) + .variant_bounds(state, bounds) + .filter_map(|item| match item { + ItemBounds::Button(entity, bounds) => Some((entity, bounds)), + _ => None, + }) .collect::>() { if cursor_position.is_over(bounds) { @@ -504,11 +539,9 @@ where if self.model.items[key].closable { // Emit close message if the close button is pressed. if let Some(on_close) = self.on_close.as_ref() { - if cursor_position.is_over(close_bounds( - bounds, - f32::from(self.close_icon.size), - self.button_padding, - )) { + if cursor_position + .is_over(close_bounds(bounds, f32::from(self.close_icon.size))) + { if let Event::Mouse(mouse::Event::ButtonReleased( mouse::Button::Left, )) @@ -595,8 +628,6 @@ where } } } - } else { - state.hovered = Item::None; } } @@ -698,14 +729,20 @@ where let bounds = layout.bounds(); if cursor_position.is_over(bounds) { - for (key, bounds) in self.variant_button_bounds(state, bounds) { - if cursor_position.is_over(bounds) { - return if self.model.items[key].enabled { - iced_core::mouse::Interaction::Pointer - } else { - iced_core::mouse::Interaction::Idle - }; - } + let hovered_button = self + .variant_bounds(state, bounds) + .filter_map(|item| match item { + ItemBounds::Button(entity, bounds) => Some((entity, bounds)), + _ => None, + }) + .find(|(_key, bounds)| cursor_position.is_over(*bounds)); + + if let Some((key, _bounds)) = hovered_button { + return if self.model.items[key].enabled { + iced_core::mouse::Interaction::Pointer + } else { + iced_core::mouse::Interaction::Idle + }; } } @@ -725,21 +762,25 @@ where ) { let state = tree.state.downcast_ref::(); let appearance = Self::variant_appearance(theme, &self.style); - let bounds = layout.bounds(); + let bounds: Rectangle = layout.bounds(); let button_amount = self.model.items.len(); + + // Modifies alpha color when `on_activate` is unset. let apply_alpha = |mut c: Color| { - if self.on_activate.is_some() { - c - } else { + if self.on_activate.is_none() { c.a /= 2.0; - c } + + c }; + + // Maps `apply_alpha` to background color. let bg_with_alpha = |mut b| { match &mut b { Background::Color(c) => { *c = apply_alpha(*c); } + Background::Gradient(g) => { let Gradient::Linear(mut l) = g; for c in &mut l.stops { @@ -752,6 +793,7 @@ where } b }; + // Draw the background, if a background was defined. if let Some(background) = appearance.background { renderer.fill_quad( @@ -759,7 +801,7 @@ where bounds, border: Border { radius: appearance.border_radius, - ..Default::default() + ..Border::default() }, shadow: Shadow::default(), }, @@ -769,6 +811,8 @@ where // Draw previous and next tab buttons if there is a need to paginate tabs. if state.collapsed { + let mut tab_bounds = prev_tab_bounds(&bounds, f32::from(self.button_height)); + // Previous tab button let mut background_appearance = if self.on_activate.is_some() && Item::PrevButton == state.focused_item { @@ -782,12 +826,7 @@ where if let Some(background_appearance) = background_appearance.take() { renderer.fill_quad( renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: f32::from(self.button_height), - height: bounds.height, - }, + bounds: tab_bounds, border: Border { radius: theme.cosmic().radius_s().into(), ..Default::default() @@ -814,14 +853,16 @@ where appearance.active.text_color }), Rectangle { - x: bounds.x + f32::from(self.button_height) / 4.0, - y: bounds.y + f32::from(self.button_height) / 4.0, + x: tab_bounds.x + 8.0, + y: tab_bounds.y + f32::from(self.button_height) / 4.0, width: 16.0, height: 16.0, }, icon::from_name("go-previous-symbolic").size(16).icon(), ); + tab_bounds = next_tab_bounds(&bounds, f32::from(self.button_height)); + // Next tab button background_appearance = if self.on_activate.is_some() && Item::NextButton == state.focused_item { @@ -835,12 +876,7 @@ where if let Some(background_appearance) = background_appearance { renderer.fill_quad( renderer::Quad { - bounds: Rectangle { - x: bounds.x + bounds.width - f32::from(self.button_height), - y: bounds.y, - width: f32::from(self.button_height), - height: bounds.height, - }, + bounds: tab_bounds, border: Border { radius: theme.cosmic().radius_s().into(), ..Default::default() @@ -866,10 +902,16 @@ where } else { appearance.inactive.text_color }), + // Rectangle { + // x: bounds.x + bounds.width - f32::from(self.button_height) + // + f32::from(self.button_height) / 4.0, + // y: f32::from(self.button_height) / 2.0 - f32::from(self.button_height) / 2.0, + // width: 16.0, + // height: 16.0, + // }, Rectangle { - x: bounds.x + bounds.width - f32::from(self.button_height) - + f32::from(self.button_height) / 4.0, - y: bounds.y + f32::from(self.button_height) / 4.0, + x: tab_bounds.x + 8.0, + y: tab_bounds.y + f32::from(self.button_height) / 4.0, width: 16.0, height: 16.0, }, @@ -878,10 +920,34 @@ where } // Draw each of the items in the widget. - for (nth, (key, mut bounds)) in self.variant_button_bounds(state, bounds).enumerate() { + let mut nth = 0; + self.variant_bounds(state, bounds).for_each(move |item| { + let (key, mut bounds) = match item { + // Draw a button + ItemBounds::Button(entity, bounds) => (entity, bounds), + + // Draw a divider between buttons + ItemBounds::Divider(bounds) => { + renderer.fill_quad( + renderer::Quad { + bounds, + border: Border::default(), + shadow: Shadow::default(), + }, + { + let theme = crate::theme::active(); + Background::Color(theme.cosmic().small_widget_divider().into()) + }, + ); + + return; + } + }; + + let center_y = bounds.center_y(); + let key_is_active = self.model.is_active(key); let key_is_hovered = self.on_activate.is_some() && state.hovered == Item::Tab(key); - let (status_appearance, font) = if self.on_activate.is_some() && Item::Tab(key) == state.focused_item { (appearance.focus, &self.font_active) @@ -892,6 +958,7 @@ where } else { (appearance.inactive, &self.font_inactive) }; + let font = font.unwrap_or_else(|| renderer.default_font()); let button_appearance = if nth == 0 { @@ -941,7 +1008,8 @@ where let original_bounds = bounds; - let y = bounds.center_y(); + bounds.x += f32::from(self.button_padding[0]); + bounds.width -= f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]); // Adjust bounds by indent if let Some(indent) = self.model.indent(key) { @@ -950,15 +1018,28 @@ where bounds.width -= adjustment; } - // Draw the image beside the text. - let horizontal_alignment = if let Some(icon) = self.model.icon(key) { - bounds.x += f32::from(self.button_padding[0]); + // Align contents of the button to the requested `button_alignment`. + { + let actual_width = state.internal_layout[nth].1.width; + let offset = match self.button_alignment { + Alignment::Start => None, + Alignment::Center => Some((bounds.width - actual_width) / 2.0), + Alignment::End => Some(bounds.width - actual_width), + }; + + if let Some(offset) = offset { + bounds.x += offset - f32::from(self.button_padding[0]); + bounds.width = actual_width; + } + } + + // Draw the image beside the text. + if let Some(icon) = self.model.icon(key) { let mut image_bounds = bounds; let width = f32::from(icon.size); let offset = width + f32::from(self.button_spacing); - image_bounds.y += f32::from(self.button_padding[1]); - image_bounds.y = y - width / 2.0; + image_bounds.y = center_y - width / 2.0; draw_icon::( renderer, @@ -977,12 +1058,44 @@ where bounds.x += offset; bounds.width -= offset; - - alignment::Horizontal::Left } else { - bounds.x = bounds.center_x(); - alignment::Horizontal::Center - }; + // Draw the selection indicator if widget is a segmented selection, and the item is selected. + if key_is_active { + if let crate::theme::SegmentedButton::Selection = self.style { + let mut image_bounds = bounds; + image_bounds.y = center_y - 16.0 / 2.0; + + draw_icon::( + renderer, + theme, + style, + cursor, + viewport, + apply_alpha(status_appearance.text_color), + Rectangle { + width: 16.0, + height: 16.0, + ..image_bounds + }, + crate::widget::icon( + match crate::widget::common::object_select().data() { + crate::iced_core::svg::Data::Bytes(bytes) => { + crate::widget::icon::from_svg_bytes(bytes.as_ref()) + } + crate::iced_core::svg::Data::Path(path) => { + crate::widget::icon::from_path(path.clone()) + } + }, + ), + ); + + let offset = 16.0 + f32::from(self.button_spacing); + + bounds.x += offset; + bounds.width -= offset; + } + } + } // Whether to show the close button on this tab. let show_close_button = @@ -997,7 +1110,7 @@ where }; if let Some(text) = self.model.text(key) { - bounds.y = y; + bounds.y = center_y; // Draw the text for this segmented button or tab. renderer.fill_text( @@ -1006,7 +1119,7 @@ where size: iced::Pixels(self.font_size), bounds: bounds.size(), font, - horizontal_alignment, + horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: Shaping::Advanced, line_height: self.line_height, @@ -1014,15 +1127,7 @@ where bounds.position(), apply_alpha(status_appearance.text_color), Rectangle { - width: { - let width = bounds.width - close_icon_width; - // TODO: determine cause of differences here. - if self.model.icon(key).is_some() { - width - f32::from(self.button_spacing) - } else { - width - 12.0 - } - }, + width: bounds.width - close_icon_width, ..original_bounds }, ); @@ -1030,8 +1135,7 @@ where // Draw a close button if set. if show_close_button { - let close_button_bounds = - close_bounds(original_bounds, close_icon_width, self.button_padding); + let close_button_bounds = close_bounds(original_bounds, close_icon_width); draw_icon::( renderer, @@ -1044,7 +1148,9 @@ where self.close_icon.clone(), ); } - } + + nth += 1; + }); } fn overlay<'b>( @@ -1093,14 +1199,14 @@ pub struct LocalState { /// Last known length of the model. pub(super) known_length: usize, /// Dimensions of internal buttons when shrinking - pub(super) internal_layout: Vec, + pub(super) internal_layout: Vec<(Size, Size)>, /// The paragraphs for each text. paragraphs: SecondaryMap, /// Time since last tab activation from wheel movements. wheel_timestamp: Option, } -#[derive(Default, PartialEq)] +#[derive(Debug, Default, PartialEq)] enum Item { NextButton, #[default] @@ -1152,17 +1258,35 @@ impl From for widget::Id { } /// Calculates the bounds of the close button within the area of an item. -fn close_bounds(area: Rectangle, icon_size: f32, button_padding: [u16; 4]) -> Rectangle { - let unpadded_height = area.height - f32::from(button_padding[1]) - f32::from(button_padding[3]); - +fn close_bounds(area: Rectangle, icon_size: f32) -> Rectangle { Rectangle { x: area.x + area.width - icon_size - 8.0, - y: area.y + (unpadded_height / 2.0) - (icon_size / 2.0), + y: area.center_y() - (icon_size / 2.0), width: icon_size, height: icon_size, } } +/// Calculate the bounds of the `next_tab` button. +fn next_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle { + Rectangle { + x: bounds.x + bounds.width - button_height, + y: bounds.y + button_height / 4.0, + width: button_height, + height: button_height, + } +} + +/// Calculate the bounds of the `prev_tab` button. +fn prev_tab_bounds(bounds: &Rectangle, button_height: f32) -> Rectangle { + Rectangle { + x: bounds.x, + y: bounds.y + button_height / 4.0, + width: button_height, + height: button_height, + } +} + #[allow(clippy::too_many_arguments)] fn draw_icon( renderer: &mut Renderer, diff --git a/src/widget/segmented_selection.rs b/src/widget/segmented_selection.rs index ee383c18..d2e05f9d 100644 --- a/src/widget/segmented_selection.rs +++ b/src/widget/segmented_selection.rs @@ -14,16 +14,22 @@ use super::segmented_button::{ /// The data for the widget comes from a model that is maintained the application. /// /// For details on the model, see the [`segmented_button`] module for more details. -#[must_use] pub fn horizontal( model: &Model, ) -> HorizontalSegmentedButton where Model: Selectable, { + let theme = crate::theme::active(); + let space_s = theme.cosmic().space_s(); + let space_xxs = theme.cosmic().space_xxs(); + segmented_button::horizontal(model) - .button_padding([16, 0, 16, 0]) + .button_alignment(iced::Alignment::Center) + .dividers(true) .button_height(32) + .button_padding([space_s, 0, space_s, 0]) + .button_spacing(space_xxs) .style(crate::theme::SegmentedButton::Selection) .font_active(Some(crate::font::FONT_SEMIBOLD)) } @@ -33,7 +39,6 @@ where /// The data for the widget comes from a model that is maintained the application. /// /// For details on the model, see the [`segmented_button`] module for more details. -#[must_use] pub fn vertical( model: &Model, ) -> VerticalSegmentedButton @@ -41,9 +46,16 @@ where Model: Selectable, SelectionMode: Default, { + let theme = crate::theme::active(); + let space_s = theme.cosmic().space_s(); + let space_xxs = theme.cosmic().space_xxs(); + segmented_button::vertical(model) - .button_padding([16, 0, 16, 0]) + .button_alignment(iced::Alignment::Center) + .dividers(true) .button_height(32) + .button_padding([space_s, 0, space_s, 0]) + .button_spacing(space_xxs) .style(crate::theme::SegmentedButton::Selection) .font_active(Some(crate::font::FONT_SEMIBOLD)) } diff --git a/src/widget/view_switcher.rs b/src/widget/view_switcher.rs index 7912e0fc..5a7d3c07 100644 --- a/src/widget/view_switcher.rs +++ b/src/widget/view_switcher.rs @@ -14,16 +14,19 @@ use super::segmented_button::{ /// The data for the widget comes from a model supplied by the application. /// /// For details on the model, see the [`segmented_button`] module for more details. -#[must_use] pub fn horizontal( model: &Model, ) -> HorizontalSegmentedButton where Model: Selectable, { + let theme = crate::theme::active(); + let space_s = theme.cosmic().space_s(); + let space_xs = theme.cosmic().space_xs(); + segmented_button::horizontal(model) - .button_padding([16, 0, 16, 0]) - .button_height(48) + .button_height(44) + .button_padding([space_s, space_xs, space_s, space_xs]) .style(crate::theme::SegmentedButton::ViewSwitcher) .font_active(Some(crate::font::FONT_SEMIBOLD)) } @@ -31,9 +34,7 @@ where /// A collection of tabs for developing a tabbed interface. /// /// The data for the widget comes from a model that is maintained the application. -/// /// For details on the model, see the [`segmented_button`] module for more details. -#[must_use] pub fn vertical( model: &Model, ) -> VerticalSegmentedButton @@ -41,9 +42,13 @@ where Model: Selectable, SelectionMode: Default, { + let theme = crate::theme::active(); + let space_s = theme.cosmic().space_s(); + let space_xs = theme.cosmic().space_xs(); + SegmentedButton::new(model) - .button_padding([16, 0, 16, 0]) - .button_height(48) + .button_height(44) + .button_padding([space_s, space_xs, space_s, space_xs]) .style(crate::theme::SegmentedButton::ViewSwitcher) .font_active(Some(crate::font::FONT_SEMIBOLD)) }