feat(list_column): button list items

This commit is contained in:
Vukašin Vojinović 2026-04-04 16:34:25 +02:00 • committed by Michael Murphy
parent 9cac422c24
commit 9b465a8b5c
6 changed files with 298 additions and 163 deletions

View file

@ -1,128 +0,0 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use iced_core::Padding;
use iced_widget::container::Catalog;
use crate::{
Apply, Element, theme,
widget::{container, divider, space::vertical},
};
#[inline]
pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> {
ListColumn::default()
}
#[must_use]
pub struct ListColumn<'a, Message> {
spacing: u16,
padding: Padding,
list_item_padding: Padding,
divider_padding: u16,
style: theme::Container<'a>,
children: Vec<Element<'a, Message>>,
}
impl<Message: 'static> Default for ListColumn<'_, Message> {
fn default() -> Self {
let cosmic_theme::Spacing {
space_xxs, space_m, ..
} = theme::spacing();
Self {
spacing: 0,
padding: Padding::from(0),
divider_padding: 16,
list_item_padding: [space_xxs, space_m].into(),
style: theme::Container::List,
children: Vec::with_capacity(4),
}
}
}
impl<'a, Message: 'static> ListColumn<'a, Message> {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[allow(clippy::should_implement_trait)]
pub fn add(self, item: impl Into<Element<'a, Message>>) -> Self {
#[inline(never)]
fn inner<'a, Message: 'static>(
mut this: ListColumn<'a, Message>,
item: Element<'a, Message>,
) -> ListColumn<'a, Message> {
if !this.children.is_empty() {
this.children.push(
container(divider::horizontal::default())
.padding([0, this.divider_padding])
.into(),
);
}
// Ensure a minimum height of 32.
let list_item = crate::widget::row![
container(item).align_y(iced::Alignment::Center),
vertical().height(iced::Length::Fixed(32.))
]
.padding(this.list_item_padding)
.align_y(iced::Alignment::Center);
this.children.push(list_item.into());
this
}
inner(self, item.into())
}
#[inline]
pub fn spacing(mut self, spacing: u16) -> Self {
self.spacing = spacing;
self
}
/// Sets the style variant of this [`Circular`].
#[inline]
pub fn style(mut self, style: <crate::Theme as Catalog>::Class<'a>) -> Self {
self.style = style;
self
}
#[inline]
pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
self.padding = padding.into();
self
}
#[inline]
pub fn divider_padding(mut self, padding: u16) -> Self {
self.divider_padding = padding;
self
}
pub fn list_item_padding(mut self, padding: impl Into<Padding>) -> Self {
self.list_item_padding = padding.into();
self
}
#[must_use]
pub fn into_element(self) -> Element<'a, Message> {
crate::widget::column::with_children(self.children)
.spacing(self.spacing)
.padding(self.padding)
.width(iced::Length::Fill)
.apply(container)
.padding([self.spacing, 0])
.class(self.style)
.width(iced::Length::Fill)
.into()
}
}
impl<'a, Message: 'static> From<ListColumn<'a, Message>> for Element<'a, Message> {
fn from(column: ListColumn<'a, Message>) -> Self {
column.into_element()
}
}

View file

@ -0,0 +1,188 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use crate::widget::container::Catalog;
use crate::widget::{button, column, container, divider, row, space::vertical};
use crate::{Apply, Element, theme};
use iced::{Length, Padding};
/// A button list item for use in a [`ListColumn`].
pub struct ListButton<'a, Message> {
content: Element<'a, Message>,
on_press: Option<Message>,
selected: bool,
}
/// Creates a [`ListButton`] with the given content.
pub fn button<'a, Message>(content: impl Into<Element<'a, Message>>) -> ListButton<'a, Message> {
ListButton {
content: content.into(),
on_press: None,
selected: false,
}
}
impl<'a, Message: 'static> ListButton<'a, Message> {
pub fn on_press(mut self, on_press: Message) -> Self {
self.on_press = Some(on_press);
self
}
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
self.on_press = on_press;
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
pub enum ListItem<'a, Message> {
Element(Element<'a, Message>),
Button(ListButton<'a, Message>),
}
/// A trait for types that can be added to a [`ListColumn`].
pub trait IntoListItem<'a, Message> {
fn into_list_item(self) -> ListItem<'a, Message>;
}
impl<'a, Message, T> IntoListItem<'a, Message> for T
where
T: Into<Element<'a, Message>>,
{
fn into_list_item(self) -> ListItem<'a, Message> {
ListItem::Element(self.into())
}
}
impl<'a, Message> IntoListItem<'a, Message> for ListButton<'a, Message> {
fn into_list_item(self) -> ListItem<'a, Message> {
ListItem::Button(self)
}
}
#[must_use]
pub struct ListColumn<'a, Message> {
list_item_padding: Padding,
style: theme::Container<'a>,
children: Vec<ListItem<'a, Message>>,
}
#[inline]
pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> {
ListColumn::default()
}
pub fn with_capacity<'a, Message: 'static>(capacity: usize) -> ListColumn<'a, Message> {
let cosmic_theme::Spacing {
space_xxs, space_m, ..
} = theme::spacing();
ListColumn {
list_item_padding: [space_xxs, space_m].into(),
style: theme::Container::List,
children: Vec::with_capacity(capacity),
}
}
impl<Message: 'static> Default for ListColumn<'_, Message> {
fn default() -> Self {
with_capacity(4)
}
}
impl<'a, Message: Clone + 'static> ListColumn<'a, Message> {
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Adds an element to the list column.
#[allow(clippy::should_implement_trait)]
pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self {
self.children.push(item.into_list_item());
self
}
/// Sets the style variant of this [`ListColumn`].
#[inline]
pub fn style(mut self, style: <crate::Theme as Catalog>::Class<'a>) -> Self {
self.style = style;
self
}
pub fn list_item_padding(mut self, padding: impl Into<Padding>) -> Self {
self.list_item_padding = padding.into();
self
}
#[must_use]
pub fn into_element(self) -> Element<'a, Message> {
let padding = self.list_item_padding;
let count = self.children.len();
let last_index = count.saturating_sub(1);
let radius_s = theme::active().cosmic().radius_s();
// Ensure minimum height of 32
let content_row = |content| {
row![container(content), vertical().height(32)].align_y(iced::Alignment::Center)
};
self.children
.into_iter()
.enumerate()
.fold(
column::with_capacity((2 * count).saturating_sub(1)),
|mut col, (i, item)| {
if i > 0 {
col = col.push(divider::horizontal::default());
}
match item {
ListItem::Element(content) => {
col.push(content_row(content).padding(padding).width(Length::Fill))
}
ListItem::Button(ListButton {
content,
on_press,
selected,
}) => col.push(
content_row(content)
.apply(button::custom)
.padding(padding)
.width(Length::Fill)
.on_press_maybe(on_press)
.selected(selected)
.class(theme::Button::ListItem(get_radius(
radius_s,
i == 0,
i == last_index,
))),
),
}
},
)
.width(Length::Fill)
.apply(container)
.class(self.style)
.into()
}
}
impl<'a, Message: Clone + 'static> From<ListColumn<'a, Message>> for Element<'a, Message> {
fn from(column: ListColumn<'a, Message>) -> Self {
column.into_element()
}
}
fn get_radius(radius: [f32; 4], first: bool, last: bool) -> [f32; 4] {
match (first, last) {
(true, true) => radius,
(true, false) => [radius[0], radius[1], 0.0, 0.0],
(false, true) => [0.0, 0.0, radius[2], radius[3]],
(false, false) => [0.0, 0.0, 0.0, 0.0],
}
}

View file

@ -1,6 +1,6 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
pub mod column;
pub mod list_column;
pub use self::column::{ListColumn, list_column};
pub use self::list_column::{ListButton, ListColumn, button, list_column};