improv(segmented_button): rework layout, drawing, and styling to fix visual flaws

This commit is contained in:
Michael Aaron Murphy 2024-02-23 16:05:55 +01:00 • committed by Michael Murphy
parent bd353c6b54
commit 366a450977
9 changed files with 364 additions and 170 deletions

View file

@ -1,4 +1,4 @@
use cosmic_config::{Config, ConfigGet, ConfigSet, CosmicConfigEntry}; use cosmic_config::{Config, ConfigGet, CosmicConfigEntry};
/// ID for the ThemeMode config /// ID for the ThemeMode config
pub const THEME_MODE_ID: &str = "com.system76.CosmicTheme.Mode"; pub const THEME_MODE_ID: &str = "com.system76.CosmicTheme.Mode";

View file

@ -157,19 +157,32 @@ impl Theme {
pub fn accent_color(&self) -> Srgba { pub fn accent_color(&self) -> Srgba {
self.accent.base.clone() self.accent.base.clone()
} }
/// get @success_color /// get @success_color
pub fn success_color(&self) -> Srgba { pub fn success_color(&self) -> Srgba {
self.success.base.clone() self.success.base.clone()
} }
/// get @destructive_color /// get @destructive_color
pub fn destructive_color(&self) -> Srgba { pub fn destructive_color(&self) -> Srgba {
self.destructive.base.clone() self.destructive.base.clone()
} }
/// get @warning_color /// get @warning_color
pub fn warning_color(&self) -> Srgba { pub fn warning_color(&self) -> Srgba {
self.warning.base.clone() 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 // Containers
/// get @bg_color /// get @bg_color
pub fn bg_color(&self) -> Srgba { pub fn bg_color(&self) -> Srgba {

View file

@ -64,7 +64,7 @@ impl StyleSheet for Theme {
Appearance { Appearance {
border_radius: cosmic.corner_radii.radius_0.into(), border_radius: cosmic.corner_radii.radius_0.into(),
inactive: ItemStatusAppearance { inactive: ItemStatusAppearance {
background: Some(Background::Color(neutral_5.into())), background: Some(Background::Color(cosmic.small_container_widget().into())),
first: ItemAppearance { first: ItemAppearance {
border_radius: Radius::from([rad_m[0], rad_0[1], rad_0[2], rad_m[3]]), border_radius: Radius::from([rad_m[0], rad_0[1], rad_0[2], rad_m[3]]),
..Default::default() ..Default::default()

View file

@ -27,18 +27,23 @@ pub fn nav_bar<Message>(
where where
Message: Clone + 'static, 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) segmented_button::vertical(model)
.button_height(32) .button_height(32)
.button_padding([16, 10, 16, 10]) .button_padding([space_s, space_xs, space_s, space_xs])
.button_spacing(8) .button_spacing(space_xxs)
.spacing(space_xxs)
.on_activate(on_activate) .on_activate(on_activate)
.spacing(8)
.style(crate::theme::SegmentedButton::ViewSwitcher) .style(crate::theme::SegmentedButton::ViewSwitcher)
.apply(scrollable) .apply(scrollable)
.height(Length::Fill) .height(Length::Fill)
.apply(container) .apply(container)
.padding(space_xxs)
.height(Length::Fill) .height(Length::Fill)
.padding(11)
.style(theme::Container::custom(nav_bar_style)) .style(theme::Container::custom(nav_bar_style))
} }

View file

@ -3,9 +3,9 @@
//! Implementation details for the horizontal layout of a segmented button. //! 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::style::StyleSheet;
use super::widget::{LocalState, SegmentedButton, SegmentedVariant}; use super::widget::{ItemBounds, LocalState, SegmentedButton, SegmentedVariant};
use iced::{Length, Rectangle, Size}; use iced::{Length, Rectangle, Size};
use iced_core::layout; use iced_core::layout;
@ -21,7 +21,6 @@ pub struct Horizontal;
/// Horizontal implementation of the [`SegmentedButton`]. /// Horizontal implementation of the [`SegmentedButton`].
/// ///
/// For details on the model, see the [`segmented_button`](super) module for more details. /// For details on the model, see the [`segmented_button`](super) module for more details.
#[must_use]
pub fn horizontal<SelectionMode: Default, Message>( pub fn horizontal<SelectionMode: Default, Message>(
model: &Model<SelectionMode>, model: &Model<SelectionMode>,
) -> SegmentedButton<Horizontal, SelectionMode, Message> ) -> SegmentedButton<Horizontal, SelectionMode, Message>
@ -45,11 +44,11 @@ where
} }
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_precision_loss)]
fn variant_button_bounds<'b>( fn variant_bounds<'b>(
&'b self, &'b self,
state: &'b LocalState, state: &'b LocalState,
mut bounds: Rectangle, mut bounds: Rectangle,
) -> Box<dyn Iterator<Item = (Entity, Rectangle)> + 'b> { ) -> Box<dyn Iterator<Item = ItemBounds> + 'b> {
let num = state.buttons_visible; let num = state.buttons_visible;
let spacing = f32::from(self.spacing); let spacing = f32::from(self.spacing);
let mut homogenous_width = 0.0; let mut homogenous_width = 0.0;
@ -66,6 +65,8 @@ where
/ num as f32; / num as f32;
} }
let segmented_selection = matches!(self.style, crate::theme::SegmentedButton::Selection);
Box::new( Box::new(
self.model self.model
.order .order
@ -74,17 +75,32 @@ where
.enumerate() .enumerate()
.skip(state.buttons_offset) .skip(state.buttons_offset)
.take(state.buttons_visible) .take(state.buttons_visible)
.map(move |(nth, key)| { .flat_map(move |(nth, key)| {
let mut this_bounds = bounds; let mut layout_bounds = bounds;
let layout_size = &state.internal_layout[nth].0;
if !state.collapsed && Length::Shrink == self.width { if !state.collapsed && Length::Shrink == self.width {
this_bounds.width = state.internal_layout[nth].width; layout_bounds.width = layout_size.width;
} else { } else {
this_bounds.width = homogenous_width; layout_bounds.width = homogenous_width;
} }
bounds.x += this_bounds.width + spacing; bounds.x += layout_bounds.width + spacing;
(key, this_bounds)
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, state: &mut LocalState,
renderer: &crate::Renderer, renderer: &crate::Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> Size {
state.internal_layout.clear();
let num = self.model.order.len(); let num = self.model.order.len();
let mut total_width = 0.0;
let spacing = f32::from(self.spacing); let spacing = f32::from(self.spacing);
let limits = limits.width(self.width); let limits = limits.width(self.width);
let mut size; let mut size;
@ -116,31 +132,20 @@ where
if let Length::Shrink = self.width { if let Length::Shrink = self.width {
// Buttons will be rendered at their smallest widths possible. // Buttons will be rendered at their smallest widths possible.
state.internal_layout.clear(); let max_height = self.max_button_dimensions(state, renderer).1;
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));
}
// Get the max available width for placing buttons into. // 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, Length::Fill,
total_height, max_height,
Size::new(f32::MAX, total_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; 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; visible_width += button_size.width;
if max_size.width >= visible_width { if max_size.width >= visible_width {
@ -152,27 +157,48 @@ where
visible_width += spacing; visible_width += spacing;
} }
visible_width -= spacing;
state.collapsed = num > 1 && state.buttons_visible != num; state.collapsed = num > 1 && state.buttons_visible != num;
// If collapsed, use the maximum width available. // If collapsed, use the maximum width available.
visible_width = if state.collapsed { if state.collapsed {
max_size.width visible_width = max_size.width;
} else { }
total_width
};
size = limits size = limits
.width(Length::Fixed(visible_width)) .width(Length::Fixed(visible_width))
.height(Length::Fixed(total_height)) .height(Length::Fixed(max_height))
.resolve( .resolve(
visible_width, visible_width,
total_height, max_height,
Size::new(visible_width, total_height), Size::new(visible_width, max_height),
); );
} else { } else {
// Buttons will be rendered with equal widths. // Buttons will be rendered with equal widths.
state.buttons_visible = self.model.items.len(); 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); let total_width = (state.buttons_visible as f32) * (width + spacing);
size = limits.height(Length::Fixed(height)).resolve( size = limits.height(Length::Fixed(height)).resolve(
@ -201,6 +227,6 @@ where
state.buttons_offset = 0; state.buttons_offset = 0;
} }
layout::Node::new(size) size
} }
} }

View file

@ -5,9 +5,9 @@
use super::model::{Entity, Model, Selectable}; use super::model::{Entity, Model, Selectable};
use super::style::StyleSheet; 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; use iced_core::layout;
/// A type marker defining the vertical variant of a [`SegmentedButton`]. /// A type marker defining the vertical variant of a [`SegmentedButton`].
@ -45,11 +45,11 @@ where
} }
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_precision_loss)]
fn variant_button_bounds<'b>( fn variant_bounds<'b>(
&'b self, &'b self,
state: &'b LocalState, state: &'b LocalState,
mut bounds: Rectangle, mut bounds: Rectangle,
) -> Box<dyn Iterator<Item = (Entity, Rectangle)> + 'b> { ) -> Box<dyn Iterator<Item = ItemBounds> + 'b> {
let spacing = f32::from(self.spacing); let spacing = f32::from(self.spacing);
Box::new( Box::new(
@ -58,11 +58,16 @@ where
.iter() .iter()
.copied() .copied()
.enumerate() .enumerate()
.map(move |(_nth, key)| { .map(move |(nth, key)| {
let mut this_bounds = bounds; let mut layout_bounds = bounds;
this_bounds.height = state.internal_layout[0].height;
bounds.y += this_bounds.height + spacing; let layout_size = state.internal_layout[nth].0;
(key, this_bounds)
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, state: &mut LocalState,
renderer: &crate::Renderer, renderer: &crate::Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> Size {
state.internal_layout.clear(); state.internal_layout.clear();
let limits = limits.width(self.width); 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 num = self.model.items.len();
let spacing = f32::from(self.spacing); let spacing = f32::from(self.spacing);
@ -87,12 +97,11 @@ where
if num != 0 { if num != 0 {
height = (num as f32 * height) + (num as f32 * spacing) - spacing; 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.width,
self.height, self.height,
Size::new(width, height), Size::new(width, height),
); )
layout::Node::new(size)
} }
} }

View file

@ -7,8 +7,8 @@ use crate::widget::{icon, Icon};
use crate::{Element, Renderer}; use crate::{Element, Renderer};
use derive_setters::Setters; use derive_setters::Setters;
use iced::{ use iced::{
alignment, event, keyboard, mouse, touch, Background, Color, Command, Event, Length, Rectangle, alignment, event, keyboard, mouse, touch, Alignment, Background, Color, Command, Event, Length,
Size, Padding, Rectangle, Size,
}; };
use iced_core::mouse::ScrollDelta; use iced_core::mouse::ScrollDelta;
use iced_core::text::{LineHeight, Paragraph, Renderer as TextRenderer, Shaping}; use iced_core::text::{LineHeight, Paragraph, Renderer as TextRenderer, Shaping};
@ -24,6 +24,11 @@ pub fn focus<Message: 'static>(id: Id) -> Command<Message> {
Command::widget(operation::focusable::focus(id.0)) Command::widget(operation::focusable::focus(id.0))
} }
pub(super) enum ItemBounds {
Button(Entity, Rectangle),
Divider(Rectangle),
}
/// Isolates variant-specific behaviors from [`SegmentedButton`]. /// Isolates variant-specific behaviors from [`SegmentedButton`].
pub trait SegmentedVariant { pub trait SegmentedVariant {
/// Get the appearance for this variant of the widget. /// Get the appearance for this variant of the widget.
@ -33,11 +38,11 @@ pub trait SegmentedVariant {
) -> super::Appearance; ) -> super::Appearance;
/// Calculates the bounds for visible buttons. /// Calculates the bounds for visible buttons.
fn variant_button_bounds<'b>( fn variant_bounds<'b>(
&'b self, &'b self,
state: &'b LocalState, state: &'b LocalState,
bounds: Rectangle, bounds: Rectangle,
) -> Box<dyn Iterator<Item = (Entity, Rectangle)> + 'b>; ) -> Box<dyn Iterator<Item = ItemBounds> + 'b>;
/// Calculates the layout of this variant. /// Calculates the layout of this variant.
fn variant_layout( fn variant_layout(
@ -45,7 +50,7 @@ pub trait SegmentedVariant {
state: &mut LocalState, state: &mut LocalState,
renderer: &crate::Renderer, renderer: &crate::Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node; ) -> Size;
} }
/// A conjoined group of items that function together as a button. /// A conjoined group of items that function together as a button.
@ -67,6 +72,13 @@ where
pub(super) scrollable_focus: bool, pub(super) scrollable_focus: bool,
/// Show the close icon only when item is hovered. /// Show the close icon only when item is hovered.
pub(super) show_close_icon_on_hover: bool, 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. /// Padding around a button.
pub(super) button_padding: [u16; 4], pub(super) button_padding: [u16; 4],
/// Desired height of a button. /// Desired height of a button.
@ -119,9 +131,12 @@ where
close_icon: icon::from_name("window-close-symbolic").size(16).icon(), close_icon: icon::from_name("window-close-symbolic").size(16).icon(),
scrollable_focus: false, scrollable_focus: false,
show_close_icon_on_hover: 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_height: 32,
button_spacing: 4, button_spacing: 0,
minimum_button_width: 150, minimum_button_width: 150,
indent_spacing: 16, indent_spacing: 16,
font_active: None, font_active: None,
@ -338,14 +353,17 @@ where
// Add icon to measurement if icon was given. // Add icon to measurement if icon was given.
if let Some(icon) = self.model.icon(button) { 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); 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. // Add close button to measurement if found.
if self.model.is_closable(button) { 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);
width += f32::from(self.close_icon.size) + f32::from(self.button_spacing) + 8.0;
} }
// Add button padding to the max size found // Add button padding to the max size found
@ -360,7 +378,6 @@ where
&self, &self,
state: &mut LocalState, state: &mut LocalState,
renderer: &Renderer, renderer: &Renderer,
_bounds: Size,
) -> (f32, f32) { ) -> (f32, f32) {
let mut width = 0.0f32; let mut width = 0.0f32;
let mut height = 0.0f32; let mut height = 0.0f32;
@ -369,10 +386,25 @@ where
for key in self.model.order.iter().copied() { for key in self.model.order.iter().copied() {
let (button_width, button_height) = self.button_dimensions(state, font, key); 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); height = height.max(button_height);
width = width.max(button_width); width = width.max(button_width);
} }
for (size, actual) in &mut state.internal_layout {
size.height = height;
actual.height = height;
}
(width, height) (width, height)
} }
} }
@ -436,7 +468,12 @@ where
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
self.variant_layout(tree.state.downcast_mut::<LocalState>(), renderer, limits) let state = tree.state.downcast_mut::<LocalState>();
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)] #[allow(clippy::too_many_lines)]
@ -453,17 +490,14 @@ where
) -> event::Status { ) -> event::Status {
let bounds = layout.bounds(); let bounds = layout.bounds();
let state = tree.state.downcast_mut::<LocalState>(); let state = tree.state.downcast_mut::<LocalState>();
state.hovered = Item::None;
if cursor_position.is_over(bounds) { if cursor_position.is_over(bounds) {
// Check for clicks on the previous and next tab buttons, when tabs are collapsed. // Check for clicks on the previous and next tab buttons, when tabs are collapsed.
if state.collapsed { if state.collapsed {
// Check if the prev tab button was clicked. // Check if the prev tab button was clicked.
if cursor_position.is_over(Rectangle { if cursor_position.is_over(prev_tab_bounds(&bounds, f32::from(self.button_height)))
x: bounds.x, && self.prev_tab_sensitive(state)
y: bounds.y,
width: f32::from(self.button_height),
height: f32::from(self.button_height),
}) && self.prev_tab_sensitive(state)
{ {
state.hovered = Item::PrevButton; state.hovered = Item::PrevButton;
if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
@ -473,12 +507,9 @@ where
} }
} else { } else {
// Check if the next tab button was clicked. // Check if the next tab button was clicked.
if cursor_position.is_over(Rectangle { if cursor_position
x: bounds.x + bounds.width - f32::from(self.button_height), .is_over(next_tab_bounds(&bounds, f32::from(self.button_height)))
y: bounds.y, && self.next_tab_sensitive(state)
width: f32::from(self.button_height),
height: f32::from(self.button_height),
}) && self.next_tab_sensitive(state)
{ {
state.hovered = Item::NextButton; state.hovered = Item::NextButton;
@ -492,7 +523,11 @@ where
} }
for (key, bounds) in self 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::<Vec<_>>() .collect::<Vec<_>>()
{ {
if cursor_position.is_over(bounds) { if cursor_position.is_over(bounds) {
@ -504,11 +539,9 @@ where
if self.model.items[key].closable { if self.model.items[key].closable {
// Emit close message if the close button is pressed. // Emit close message if the close button is pressed.
if let Some(on_close) = self.on_close.as_ref() { if let Some(on_close) = self.on_close.as_ref() {
if cursor_position.is_over(close_bounds( if cursor_position
bounds, .is_over(close_bounds(bounds, f32::from(self.close_icon.size)))
f32::from(self.close_icon.size), {
self.button_padding,
)) {
if let Event::Mouse(mouse::Event::ButtonReleased( if let Event::Mouse(mouse::Event::ButtonReleased(
mouse::Button::Left, mouse::Button::Left,
)) ))
@ -595,8 +628,6 @@ where
} }
} }
} }
} else {
state.hovered = Item::None;
} }
} }
@ -698,14 +729,20 @@ where
let bounds = layout.bounds(); let bounds = layout.bounds();
if cursor_position.is_over(bounds) { if cursor_position.is_over(bounds) {
for (key, bounds) in self.variant_button_bounds(state, bounds) { let hovered_button = self
if cursor_position.is_over(bounds) { .variant_bounds(state, bounds)
return if self.model.items[key].enabled { .filter_map(|item| match item {
iced_core::mouse::Interaction::Pointer ItemBounds::Button(entity, bounds) => Some((entity, bounds)),
} else { _ => None,
iced_core::mouse::Interaction::Idle })
}; .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::<LocalState>(); let state = tree.state.downcast_ref::<LocalState>();
let appearance = Self::variant_appearance(theme, &self.style); let appearance = Self::variant_appearance(theme, &self.style);
let bounds = layout.bounds(); let bounds: Rectangle = layout.bounds();
let button_amount = self.model.items.len(); let button_amount = self.model.items.len();
// Modifies alpha color when `on_activate` is unset.
let apply_alpha = |mut c: Color| { let apply_alpha = |mut c: Color| {
if self.on_activate.is_some() { if self.on_activate.is_none() {
c
} else {
c.a /= 2.0; c.a /= 2.0;
c
} }
c
}; };
// Maps `apply_alpha` to background color.
let bg_with_alpha = |mut b| { let bg_with_alpha = |mut b| {
match &mut b { match &mut b {
Background::Color(c) => { Background::Color(c) => {
*c = apply_alpha(*c); *c = apply_alpha(*c);
} }
Background::Gradient(g) => { Background::Gradient(g) => {
let Gradient::Linear(mut l) = g; let Gradient::Linear(mut l) = g;
for c in &mut l.stops { for c in &mut l.stops {
@ -752,6 +793,7 @@ where
} }
b b
}; };
// Draw the background, if a background was defined. // Draw the background, if a background was defined.
if let Some(background) = appearance.background { if let Some(background) = appearance.background {
renderer.fill_quad( renderer.fill_quad(
@ -759,7 +801,7 @@ where
bounds, bounds,
border: Border { border: Border {
radius: appearance.border_radius, radius: appearance.border_radius,
..Default::default() ..Border::default()
}, },
shadow: Shadow::default(), shadow: Shadow::default(),
}, },
@ -769,6 +811,8 @@ where
// Draw previous and next tab buttons if there is a need to paginate tabs. // Draw previous and next tab buttons if there is a need to paginate tabs.
if state.collapsed { if state.collapsed {
let mut tab_bounds = prev_tab_bounds(&bounds, f32::from(self.button_height));
// Previous tab button // Previous tab button
let mut background_appearance = let mut background_appearance =
if self.on_activate.is_some() && Item::PrevButton == state.focused_item { if self.on_activate.is_some() && Item::PrevButton == state.focused_item {
@ -782,12 +826,7 @@ where
if let Some(background_appearance) = background_appearance.take() { if let Some(background_appearance) = background_appearance.take() {
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds: Rectangle { bounds: tab_bounds,
x: bounds.x,
y: bounds.y,
width: f32::from(self.button_height),
height: bounds.height,
},
border: Border { border: Border {
radius: theme.cosmic().radius_s().into(), radius: theme.cosmic().radius_s().into(),
..Default::default() ..Default::default()
@ -814,14 +853,16 @@ where
appearance.active.text_color appearance.active.text_color
}), }),
Rectangle { Rectangle {
x: bounds.x + f32::from(self.button_height) / 4.0, x: tab_bounds.x + 8.0,
y: bounds.y + f32::from(self.button_height) / 4.0, y: tab_bounds.y + f32::from(self.button_height) / 4.0,
width: 16.0, width: 16.0,
height: 16.0, height: 16.0,
}, },
icon::from_name("go-previous-symbolic").size(16).icon(), icon::from_name("go-previous-symbolic").size(16).icon(),
); );
tab_bounds = next_tab_bounds(&bounds, f32::from(self.button_height));
// Next tab button // Next tab button
background_appearance = background_appearance =
if self.on_activate.is_some() && Item::NextButton == state.focused_item { if self.on_activate.is_some() && Item::NextButton == state.focused_item {
@ -835,12 +876,7 @@ where
if let Some(background_appearance) = background_appearance { if let Some(background_appearance) = background_appearance {
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds: Rectangle { bounds: tab_bounds,
x: bounds.x + bounds.width - f32::from(self.button_height),
y: bounds.y,
width: f32::from(self.button_height),
height: bounds.height,
},
border: Border { border: Border {
radius: theme.cosmic().radius_s().into(), radius: theme.cosmic().radius_s().into(),
..Default::default() ..Default::default()
@ -866,10 +902,16 @@ where
} else { } else {
appearance.inactive.text_color 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 { Rectangle {
x: bounds.x + bounds.width - f32::from(self.button_height) x: tab_bounds.x + 8.0,
+ f32::from(self.button_height) / 4.0, y: tab_bounds.y + f32::from(self.button_height) / 4.0,
y: bounds.y + f32::from(self.button_height) / 4.0,
width: 16.0, width: 16.0,
height: 16.0, height: 16.0,
}, },
@ -878,10 +920,34 @@ where
} }
// Draw each of the items in the widget. // 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_active = self.model.is_active(key);
let key_is_hovered = self.on_activate.is_some() && state.hovered == Item::Tab(key); let key_is_hovered = self.on_activate.is_some() && state.hovered == Item::Tab(key);
let (status_appearance, font) = let (status_appearance, font) =
if self.on_activate.is_some() && Item::Tab(key) == state.focused_item { if self.on_activate.is_some() && Item::Tab(key) == state.focused_item {
(appearance.focus, &self.font_active) (appearance.focus, &self.font_active)
@ -892,6 +958,7 @@ where
} else { } else {
(appearance.inactive, &self.font_inactive) (appearance.inactive, &self.font_inactive)
}; };
let font = font.unwrap_or_else(|| renderer.default_font()); let font = font.unwrap_or_else(|| renderer.default_font());
let button_appearance = if nth == 0 { let button_appearance = if nth == 0 {
@ -941,7 +1008,8 @@ where
let original_bounds = bounds; 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 // Adjust bounds by indent
if let Some(indent) = self.model.indent(key) { if let Some(indent) = self.model.indent(key) {
@ -950,15 +1018,28 @@ where
bounds.width -= adjustment; bounds.width -= adjustment;
} }
// Draw the image beside the text. // Align contents of the button to the requested `button_alignment`.
let horizontal_alignment = if let Some(icon) = self.model.icon(key) { {
bounds.x += f32::from(self.button_padding[0]); 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 mut image_bounds = bounds;
let width = f32::from(icon.size); let width = f32::from(icon.size);
let offset = width + f32::from(self.button_spacing); let offset = width + f32::from(self.button_spacing);
image_bounds.y += f32::from(self.button_padding[1]); image_bounds.y = center_y - width / 2.0;
image_bounds.y = y - width / 2.0;
draw_icon::<Message>( draw_icon::<Message>(
renderer, renderer,
@ -977,12 +1058,44 @@ where
bounds.x += offset; bounds.x += offset;
bounds.width -= offset; bounds.width -= offset;
alignment::Horizontal::Left
} else { } else {
bounds.x = bounds.center_x(); // Draw the selection indicator if widget is a segmented selection, and the item is selected.
alignment::Horizontal::Center 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::<Message>(
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. // Whether to show the close button on this tab.
let show_close_button = let show_close_button =
@ -997,7 +1110,7 @@ where
}; };
if let Some(text) = self.model.text(key) { if let Some(text) = self.model.text(key) {
bounds.y = y; bounds.y = center_y;
// Draw the text for this segmented button or tab. // Draw the text for this segmented button or tab.
renderer.fill_text( renderer.fill_text(
@ -1006,7 +1119,7 @@ where
size: iced::Pixels(self.font_size), size: iced::Pixels(self.font_size),
bounds: bounds.size(), bounds: bounds.size(),
font, font,
horizontal_alignment, horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center, vertical_alignment: alignment::Vertical::Center,
shaping: Shaping::Advanced, shaping: Shaping::Advanced,
line_height: self.line_height, line_height: self.line_height,
@ -1014,15 +1127,7 @@ where
bounds.position(), bounds.position(),
apply_alpha(status_appearance.text_color), apply_alpha(status_appearance.text_color),
Rectangle { Rectangle {
width: { width: bounds.width - close_icon_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
}
},
..original_bounds ..original_bounds
}, },
); );
@ -1030,8 +1135,7 @@ where
// Draw a close button if set. // Draw a close button if set.
if show_close_button { if show_close_button {
let close_button_bounds = let close_button_bounds = close_bounds(original_bounds, close_icon_width);
close_bounds(original_bounds, close_icon_width, self.button_padding);
draw_icon::<Message>( draw_icon::<Message>(
renderer, renderer,
@ -1044,7 +1148,9 @@ where
self.close_icon.clone(), self.close_icon.clone(),
); );
} }
}
nth += 1;
});
} }
fn overlay<'b>( fn overlay<'b>(
@ -1093,14 +1199,14 @@ pub struct LocalState {
/// Last known length of the model. /// Last known length of the model.
pub(super) known_length: usize, pub(super) known_length: usize,
/// Dimensions of internal buttons when shrinking /// Dimensions of internal buttons when shrinking
pub(super) internal_layout: Vec<Size>, pub(super) internal_layout: Vec<(Size, Size)>,
/// The paragraphs for each text. /// The paragraphs for each text.
paragraphs: SecondaryMap<Entity, crate::Paragraph>, paragraphs: SecondaryMap<Entity, crate::Paragraph>,
/// Time since last tab activation from wheel movements. /// Time since last tab activation from wheel movements.
wheel_timestamp: Option<Instant>, wheel_timestamp: Option<Instant>,
} }
#[derive(Default, PartialEq)] #[derive(Debug, Default, PartialEq)]
enum Item { enum Item {
NextButton, NextButton,
#[default] #[default]
@ -1152,17 +1258,35 @@ impl From<Id> for widget::Id {
} }
/// Calculates the bounds of the close button within the area of an item. /// Calculates the bounds of the close button within the area of an item.
fn close_bounds(area: Rectangle<f32>, icon_size: f32, button_padding: [u16; 4]) -> Rectangle<f32> { fn close_bounds(area: Rectangle<f32>, icon_size: f32) -> Rectangle<f32> {
let unpadded_height = area.height - f32::from(button_padding[1]) - f32::from(button_padding[3]);
Rectangle { Rectangle {
x: area.x + area.width - icon_size - 8.0, 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, width: icon_size,
height: 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)] #[allow(clippy::too_many_arguments)]
fn draw_icon<Message: 'static>( fn draw_icon<Message: 'static>(
renderer: &mut Renderer, renderer: &mut Renderer,

View file

@ -14,16 +14,22 @@ use super::segmented_button::{
/// The data for the widget comes from a model that is maintained the application. /// 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. /// For details on the model, see the [`segmented_button`] module for more details.
#[must_use]
pub fn horizontal<SelectionMode: Default, Message>( pub fn horizontal<SelectionMode: Default, Message>(
model: &Model<SelectionMode>, model: &Model<SelectionMode>,
) -> HorizontalSegmentedButton<SelectionMode, Message> ) -> HorizontalSegmentedButton<SelectionMode, Message>
where where
Model<SelectionMode>: Selectable, Model<SelectionMode>: Selectable,
{ {
let theme = crate::theme::active();
let space_s = theme.cosmic().space_s();
let space_xxs = theme.cosmic().space_xxs();
segmented_button::horizontal(model) segmented_button::horizontal(model)
.button_padding([16, 0, 16, 0]) .button_alignment(iced::Alignment::Center)
.dividers(true)
.button_height(32) .button_height(32)
.button_padding([space_s, 0, space_s, 0])
.button_spacing(space_xxs)
.style(crate::theme::SegmentedButton::Selection) .style(crate::theme::SegmentedButton::Selection)
.font_active(Some(crate::font::FONT_SEMIBOLD)) .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. /// 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. /// For details on the model, see the [`segmented_button`] module for more details.
#[must_use]
pub fn vertical<SelectionMode, Message>( pub fn vertical<SelectionMode, Message>(
model: &Model<SelectionMode>, model: &Model<SelectionMode>,
) -> VerticalSegmentedButton<SelectionMode, Message> ) -> VerticalSegmentedButton<SelectionMode, Message>
@ -41,9 +46,16 @@ where
Model<SelectionMode>: Selectable, Model<SelectionMode>: Selectable,
SelectionMode: Default, 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) segmented_button::vertical(model)
.button_padding([16, 0, 16, 0]) .button_alignment(iced::Alignment::Center)
.dividers(true)
.button_height(32) .button_height(32)
.button_padding([space_s, 0, space_s, 0])
.button_spacing(space_xxs)
.style(crate::theme::SegmentedButton::Selection) .style(crate::theme::SegmentedButton::Selection)
.font_active(Some(crate::font::FONT_SEMIBOLD)) .font_active(Some(crate::font::FONT_SEMIBOLD))
} }

View file

@ -14,16 +14,19 @@ use super::segmented_button::{
/// The data for the widget comes from a model supplied by the application. /// 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. /// For details on the model, see the [`segmented_button`] module for more details.
#[must_use]
pub fn horizontal<SelectionMode: Default, Message>( pub fn horizontal<SelectionMode: Default, Message>(
model: &Model<SelectionMode>, model: &Model<SelectionMode>,
) -> HorizontalSegmentedButton<SelectionMode, Message> ) -> HorizontalSegmentedButton<SelectionMode, Message>
where where
Model<SelectionMode>: Selectable, Model<SelectionMode>: Selectable,
{ {
let theme = crate::theme::active();
let space_s = theme.cosmic().space_s();
let space_xs = theme.cosmic().space_xs();
segmented_button::horizontal(model) segmented_button::horizontal(model)
.button_padding([16, 0, 16, 0]) .button_height(44)
.button_height(48) .button_padding([space_s, space_xs, space_s, space_xs])
.style(crate::theme::SegmentedButton::ViewSwitcher) .style(crate::theme::SegmentedButton::ViewSwitcher)
.font_active(Some(crate::font::FONT_SEMIBOLD)) .font_active(Some(crate::font::FONT_SEMIBOLD))
} }
@ -31,9 +34,7 @@ where
/// A collection of tabs for developing a tabbed interface. /// A collection of tabs for developing a tabbed interface.
/// ///
/// The data for the widget comes from a model that is maintained the application. /// 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. /// For details on the model, see the [`segmented_button`] module for more details.
#[must_use]
pub fn vertical<SelectionMode, Message>( pub fn vertical<SelectionMode, Message>(
model: &Model<SelectionMode>, model: &Model<SelectionMode>,
) -> VerticalSegmentedButton<SelectionMode, Message> ) -> VerticalSegmentedButton<SelectionMode, Message>
@ -41,9 +42,13 @@ where
Model<SelectionMode>: Selectable, Model<SelectionMode>: Selectable,
SelectionMode: Default, SelectionMode: Default,
{ {
let theme = crate::theme::active();
let space_s = theme.cosmic().space_s();
let space_xs = theme.cosmic().space_xs();
SegmentedButton::new(model) SegmentedButton::new(model)
.button_padding([16, 0, 16, 0]) .button_height(44)
.button_height(48) .button_padding([space_s, space_xs, space_s, space_xs])
.style(crate::theme::SegmentedButton::ViewSwitcher) .style(crate::theme::SegmentedButton::ViewSwitcher)
.font_active(Some(crate::font::FONT_SEMIBOLD)) .font_active(Some(crate::font::FONT_SEMIBOLD))
} }