Skip to main content

cosmic/widget/settings/
item.rs

1// Copyright 2022 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use std::borrow::Cow;
5
6use crate::widget::{FlexRow, Row, column, container, flex_row, list, row, text};
7use crate::{Element, Theme, theme};
8use derive_setters::Setters;
9use iced_core::Length;
10use iced_core::text::Wrapping;
11use iced_widget::space;
12use taffy::AlignContent;
13
14/// A settings item aligned in a row
15#[must_use]
16#[allow(clippy::module_name_repetitions)]
17pub fn item<'a, Message: 'static>(
18    title: impl Into<Cow<'a, str>> + 'a,
19    widget: impl Into<Element<'a, Message>> + 'a,
20) -> Row<'a, Message, Theme> {
21    #[inline(never)]
22    fn inner<'a, Message: 'static>(
23        title: Cow<'a, str>,
24        widget: Element<'a, Message>,
25    ) -> Row<'a, Message, Theme> {
26        item_row(vec![
27            text(title).wrapping(Wrapping::Word).into(),
28            space::horizontal().into(),
29            widget,
30        ])
31    }
32
33    inner(title.into(), widget.into())
34}
35
36/// A settings item aligned in a row
37#[must_use]
38#[allow(clippy::module_name_repetitions)]
39pub fn item_row<Message>(children: Vec<Element<Message>>) -> Row<Message, Theme> {
40    row::with_children(children)
41        .spacing(theme::spacing().space_xs)
42        .align_y(iced::Alignment::Center)
43        .width(Length::Fill)
44}
45
46/// A settings item aligned in a flex row
47#[allow(clippy::module_name_repetitions)]
48pub fn flex_item<'a, Message: 'static>(
49    title: impl Into<Cow<'a, str>> + 'a,
50    widget: impl Into<Element<'a, Message>> + 'a,
51) -> FlexRow<'a, Message> {
52    #[inline(never)]
53    fn inner<'a, Message: 'static>(
54        title: Cow<'a, str>,
55        widget: Element<'a, Message>,
56    ) -> FlexRow<'a, Message> {
57        flex_item_row(vec![
58            text(title)
59                .wrapping(Wrapping::Word)
60                .width(Length::Fill)
61                .into(),
62            container(widget).width(Length::Shrink).into(),
63        ])
64        .width(Length::Fill)
65    }
66
67    inner(title.into(), widget.into())
68}
69
70/// A settings item aligned in a flex row
71#[allow(clippy::module_name_repetitions)]
72pub fn flex_item_row<Message>(children: Vec<Element<Message>>) -> FlexRow<Message> {
73    flex_row(children)
74        .spacing(theme::spacing().space_xs)
75        .min_item_width(200.0)
76        .justify_items(iced::Alignment::Center)
77        .justify_content(AlignContent::SpaceBetween)
78        .width(Length::Fill)
79}
80
81/// Creates a builder for an item, beginning with the title.
82pub fn builder<'a, Message: 'static>(title: impl Into<Cow<'a, str>>) -> Item<'a, Message> {
83    Item {
84        title: title.into(),
85        description: None,
86        icon: None,
87    }
88}
89
90/// A builder for a settings item.
91#[derive(Setters)]
92pub struct Item<'a, Message> {
93    /// Describes the item being controlled.
94    title: Cow<'a, str>,
95
96    /// A description to display beneath the title.
97    #[setters(strip_option, into)]
98    description: Option<Cow<'a, str>>,
99
100    /// A custom icon to display before the text.
101    #[setters(strip_option, into)]
102    icon: Option<Element<'a, Message>>,
103}
104
105impl<'a, Message: Clone + 'static> Item<'a, Message> {
106    /// Assigns a control to the item.
107    pub fn control(self, widget: impl Into<Element<'a, Message>>) -> Row<'a, Message, Theme> {
108        item_row(self.control_(widget.into()))
109    }
110
111    /// Assigns a control which flexes.
112    pub fn flex_control(self, widget: impl Into<Element<'a, Message>>) -> FlexRow<'a, Message> {
113        flex_item_row(self.control_(widget.into()))
114    }
115
116    fn label(self) -> Element<'a, Message> {
117        if let Some(description) = self.description {
118            column::with_capacity(2)
119                .spacing(2)
120                .push(text::body(self.title).wrapping(Wrapping::Word))
121                .push(text::caption(description).wrapping(Wrapping::Word))
122                .width(Length::Fill)
123                .into()
124        } else {
125            text(self.title).width(Length::Fill).into()
126        }
127    }
128
129    #[inline(never)]
130    fn control_(mut self, widget: Element<'a, Message>) -> Vec<Element<'a, Message>> {
131        let mut contents = Vec::with_capacity(3);
132        if let Some(icon) = self.icon.take() {
133            contents.push(icon);
134        }
135        contents.push(self.label());
136        contents.push(widget);
137        contents
138    }
139
140    fn control_start(self, widget: impl Into<Element<'a, Message>>) -> Row<'a, Message, Theme> {
141        item_row(vec![widget.into(), self.label()])
142    }
143
144    pub fn toggler(
145        self,
146        is_checked: bool,
147        message: impl Fn(bool) -> Message + 'static,
148    ) -> list::ListButton<'a, Message> {
149        let on_press = message(!is_checked);
150        list::button(
151            self.control(
152                crate::widget::toggler(is_checked)
153                    .width(Length::Shrink)
154                    .on_toggle(message),
155            ),
156        )
157        .on_press(on_press)
158    }
159
160    pub fn toggler_maybe(
161        self,
162        is_checked: bool,
163        message: Option<impl Fn(bool) -> Message + 'static>,
164    ) -> list::ListButton<'a, Message> {
165        let on_press = message.as_ref().map(|f| f(!is_checked));
166        list::button(
167            self.control(
168                crate::widget::toggler(is_checked)
169                    .width(Length::Shrink)
170                    .on_toggle_maybe(message),
171            ),
172        )
173        .on_press_maybe(on_press)
174    }
175
176    pub fn checkbox(
177        self,
178        is_checked: bool,
179        message: impl Fn(bool) -> Message + 'static,
180    ) -> list::ListButton<'a, Message> {
181        let on_press = message(!is_checked);
182        list::button(
183            self.control_start(
184                crate::widget::checkbox(is_checked)
185                    .width(Length::Shrink)
186                    .on_toggle(message),
187            ),
188        )
189        .on_press(on_press)
190    }
191
192    pub fn checkbox_maybe(
193        self,
194        is_checked: bool,
195        message: Option<impl Fn(bool) -> Message + 'static>,
196    ) -> list::ListButton<'a, Message> {
197        let on_press = message.as_ref().map(|f| f(!is_checked));
198        list::button(
199            self.control_start(
200                crate::widget::checkbox(is_checked)
201                    .width(Length::Shrink)
202                    .on_toggle_maybe(message),
203            ),
204        )
205        .on_press_maybe(on_press)
206    }
207
208    pub fn radio<V, F>(self, value: V, selected: Option<V>, f: F) -> list::ListButton<'a, Message>
209    where
210        V: Eq + Copy,
211        F: Fn(V) -> Message,
212    {
213        let on_press = f(value);
214        list::button(
215            self.control_start(crate::widget::radio::Radio::new_no_label(
216                value, selected, f,
217            )),
218        )
219        .on_press(on_press)
220    }
221}