Skip to main content

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