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