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