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