cosmic/widget/list/
list_column.rs

1// Copyright 2022 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use crate::widget::container::Catalog;
5use crate::widget::{button, column, container, divider, row, space::vertical};
6use crate::{Apply, Element, theme};
7use iced::{Length, Padding};
8
9/// A button list item for use in a [`ListColumn`].
10pub struct ListButton<'a, Message> {
11    content: Element<'a, Message>,
12    on_press: Option<Message>,
13    selected: bool,
14}
15
16/// Creates a [`ListButton`] with the given content.
17pub fn button<'a, Message>(content: impl Into<Element<'a, Message>>) -> ListButton<'a, Message> {
18    ListButton {
19        content: content.into(),
20        on_press: None,
21        selected: false,
22    }
23}
24
25impl<'a, Message: 'static> ListButton<'a, Message> {
26    pub fn on_press(mut self, on_press: Message) -> Self {
27        self.on_press = Some(on_press);
28        self
29    }
30
31    pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
32        self.on_press = on_press;
33        self
34    }
35
36    pub fn selected(mut self, selected: bool) -> Self {
37        self.selected = selected;
38        self
39    }
40}
41
42pub enum ListItem<'a, Message> {
43    Element(Element<'a, Message>),
44    Button(ListButton<'a, Message>),
45}
46
47/// A trait for types that can be added to a [`ListColumn`].
48pub trait IntoListItem<'a, Message> {
49    fn into_list_item(self) -> ListItem<'a, Message>;
50}
51
52impl<'a, Message, T> IntoListItem<'a, Message> for T
53where
54    T: Into<Element<'a, Message>>,
55{
56    fn into_list_item(self) -> ListItem<'a, Message> {
57        ListItem::Element(self.into())
58    }
59}
60
61impl<'a, Message> IntoListItem<'a, Message> for ListButton<'a, Message> {
62    fn into_list_item(self) -> ListItem<'a, Message> {
63        ListItem::Button(self)
64    }
65}
66
67// Snapshots the padding values at the moment an item is added
68struct ListEntry<'a, Message> {
69    item: ListItem<'a, Message>,
70    item_padding: Padding,
71    divider_padding: u16,
72}
73
74#[must_use]
75pub struct ListColumn<'a, Message> {
76    list_item_padding: Padding,
77    divider_padding: u16,
78    style: theme::Container<'a>,
79    children: Vec<ListEntry<'a, Message>>,
80}
81
82#[inline]
83pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> {
84    ListColumn::default()
85}
86
87pub fn with_capacity<'a, Message: 'static>(capacity: usize) -> ListColumn<'a, Message> {
88    let cosmic_theme::Spacing {
89        space_xxs, space_m, ..
90    } = theme::spacing();
91
92    ListColumn {
93        list_item_padding: [space_xxs, space_m].into(),
94        divider_padding: 0,
95        style: theme::Container::List,
96        children: Vec::with_capacity(capacity),
97    }
98}
99
100impl<Message: 'static> Default for ListColumn<'_, Message> {
101    fn default() -> Self {
102        with_capacity(4)
103    }
104}
105
106impl<'a, Message: Clone + 'static> ListColumn<'a, Message> {
107    #[inline]
108    pub fn new() -> Self {
109        Self::default()
110    }
111
112    /// Adds a [`ListItem`] to the [`ListColumn`].
113    #[allow(clippy::should_implement_trait)]
114    pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self {
115        self.children.push(ListEntry {
116            item: item.into_list_item(),
117            item_padding: self.list_item_padding,
118            divider_padding: self.divider_padding,
119        });
120        self
121    }
122
123    /// Sets the style variant of this [`ListColumn`].
124    #[inline]
125    pub fn style(mut self, style: <crate::Theme as Catalog>::Class<'a>) -> Self {
126        self.style = style;
127        self
128    }
129
130    pub fn list_item_padding(mut self, padding: impl Into<Padding>) -> Self {
131        self.list_item_padding = padding.into();
132        self
133    }
134
135    #[inline]
136    pub fn divider_padding(mut self, padding: u16) -> Self {
137        self.divider_padding = padding;
138        self
139    }
140
141    #[must_use]
142    pub fn into_element(self) -> Element<'a, Message> {
143        let count = self.children.len();
144        let last_index = count.saturating_sub(1);
145        let radius_s = theme::active().cosmic().radius_s();
146        let mut col = column::with_capacity((2 * count).saturating_sub(1));
147
148        // Ensure minimum height of 32
149        let content_row = |content| {
150            row![container(content), vertical().height(32)].align_y(iced::Alignment::Center)
151        };
152
153        for (
154            i,
155            ListEntry {
156                item,
157                item_padding,
158                divider_padding,
159            },
160        ) in self.children.into_iter().enumerate()
161        {
162            if i > 0 {
163                col = col
164                    .push(container(divider::horizontal::default()).padding([0, divider_padding]));
165            }
166
167            col = match item {
168                ListItem::Element(content) => col.push(
169                    content_row(content)
170                        .padding(item_padding)
171                        .width(Length::Fill),
172                ),
173                ListItem::Button(ListButton {
174                    content,
175                    on_press,
176                    selected,
177                }) => col.push(
178                    content_row(content)
179                        .apply(button::custom)
180                        .padding(item_padding)
181                        .width(Length::Fill)
182                        .on_press_maybe(on_press)
183                        .selected(selected)
184                        .class(theme::Button::ListItem(get_radius(
185                            radius_s,
186                            i == 0,
187                            i == last_index,
188                        ))),
189                ),
190            };
191        }
192
193        col.width(Length::Fill)
194            .apply(container)
195            .class(self.style)
196            .into()
197    }
198}
199
200impl<'a, Message: Clone + 'static> From<ListColumn<'a, Message>> for Element<'a, Message> {
201    fn from(column: ListColumn<'a, Message>) -> Self {
202        column.into_element()
203    }
204}
205
206fn get_radius(radius: [f32; 4], first: bool, last: bool) -> [f32; 4] {
207    match (first, last) {
208        (true, true) => radius,
209        (true, false) => [radius[0], radius[1], 0.0, 0.0],
210        (false, true) => [0.0, 0.0, radius[2], radius[3]],
211        (false, false) => [0.0, 0.0, 0.0, 0.0],
212    }
213}