Krolyxon
Commit 6d1e074

Branches

Refs →

budctlcratesbudctl-guisrcui.rs

261 lines · 8.4 KB · Rust

1//! The building blocks every page uses: cards, setting rows, switches, the segmented pill and
2//! the sidebar's pieces. Everything takes its colors from the [`Look`].
3
4use iced::widget::{Column, Row, button, column, container, row, rule, space, text, toggler};
5use iced::{Alignment, Border, Element, Length};
6
7use crate::Message;
8use crate::icons::{self, Icon};
9use crate::theme::Look;
10
11/// A page title.
12pub fn title<'a>(look: Look, title: &'a str) -> Element<'a, Message> {
13 text(title).size(30).font(look.bold()).color(look.p.fg).into()
14}
15
16/// A rounded card holding rows, with a thin line between them.
17pub fn card<'a>(look: Look, rows: Vec<Element<'a, Message>>) -> Element<'a, Message> {
18 let p = look.p;
19 let mut column = Column::new();
20 for (n, item) in rows.into_iter().enumerate() {
21 if n > 0 {
22 column = column.push(separator(look));
23 }
24 column = column.push(item);
25 }
26 container(column)
27 .padding(5)
28 .width(Length::Fill)
29 .style(move |_| container::Style {
30 background: Some(p.surface.into()),
31 border: Border {
32 color: p.line,
33 width: 1.0,
34 radius: 18.0.into(),
35 },
36 ..container::Style::default()
37 })
38 .into()
39}
40
41fn separator<'a>(look: Look) -> Element<'a, Message> {
42 let p = look.p;
43 container(rule::horizontal(1).style(move |_| rule::Style {
44 color: p.line,
45 radius: 0.0.into(),
46 fill_mode: rule::FillMode::Full,
47 snap: true,
48 }))
49 .padding([0, 14])
50 .into()
51}
52
53/// A setting row: icon, title, an optional subtitle, and something on the right. The whole row
54/// is the click target.
55pub fn setting<'a>(
56 look: Look,
57 icon: Icon,
58 title: &'a str,
59 subtitle: Option<String>,
60 trailing: Element<'a, Message>,
61 on_press: Message,
62) -> Element<'a, Message> {
63 let p = look.p;
64 let words = column![text(title).size(15).font(look.medium()).color(p.fg)]
65 .push(subtitle.map(|s| text(s).size(13).color(p.dim)))
66 .spacing(2)
67 .width(Length::Fill);
68 let content = row![icons::icon(icon, 22.0, p.icon), words, trailing]
69 .spacing(16)
70 .align_y(Alignment::Center)
71 .padding([13, 16]);
72 button(content)
73 .on_press(on_press)
74 .padding(0)
75 .width(Length::Fill)
76 .style(move |_, status| button::Style {
77 background: hovered(status).then_some(p.surface_high.into()),
78 text_color: p.fg,
79 border: Border::default().rounded(14),
80 ..button::Style::default()
81 })
82 .into()
83}
84
85fn hovered(status: button::Status) -> bool {
86 matches!(status, button::Status::Hovered | button::Status::Pressed)
87}
88
89/// An on/off switch. It only shows the state: the row around it takes the click.
90pub fn switch<'a>(look: Look, on: bool) -> Element<'a, Message> {
91 let p = look.p;
92 toggler(on)
93 .size(24)
94 .style(move |_, _| toggler::Style {
95 background: (if on { p.accent } else { p.surface_high }).into(),
96 background_border_width: 1.0,
97 background_border_color: if on { p.accent } else { p.line },
98 foreground: (if on { p.accent_fg } else { p.dim }).into(),
99 foreground_border_width: 0.0,
100 foreground_border_color: p.accent_fg,
101 text_color: None,
102 border_radius: None,
103 padding_ratio: 0.12,
104 })
105 .into()
106}
107
108/// A chevron: the row opens a page.
109pub fn chevron<'a>(look: Look) -> Element<'a, Message> {
110 icons::icon(Icon::Chevron, 18.0, look.p.dim)
111}
112
113/// One choice of a [`segmented`] control.
114pub struct Segment<'a> {
115 pub icon: Option<Icon>,
116 pub label: &'a str,
117 pub selected: bool,
118 pub on_press: Message,
119}
120
121/// A pill of choices with the selected one filled, like the noise-control modes. `large` puts an
122/// icon above each label.
123pub fn segmented<'a>(look: Look, segments: Vec<Segment<'a>>, large: bool) -> Element<'a, Message> {
124 let p = look.p;
125 let mut choices = Row::new().spacing(4);
126 for segment in segments {
127 let selected = segment.selected;
128 let color = if selected { p.accent_fg } else { p.dim };
129 let label = text(segment.label)
130 .size(if large { 13.0 } else { 13.5 })
131 .font(look.medium())
132 .color(color);
133 let inner: Element<'a, Message> = match segment.icon {
134 Some(icon) if large => column![icons::icon(icon, 22.0, color), label]
135 .spacing(6)
136 .align_x(Alignment::Center)
137 .into(),
138 _ => label.into(),
139 };
140 let choice = button(container(inner).center_x(Length::Fill))
141 .on_press(segment.on_press)
142 .width(Length::Fill)
143 .padding(if large { [11, 8] } else { [7, 12] })
144 .style(move |_, status| button::Style {
145 background: if selected {
146 Some(p.accent.into())
147 } else {
148 hovered(status).then_some(p.surface_high.into())
149 },
150 text_color: color,
151 border: Border::default().rounded(999),
152 ..button::Style::default()
153 });
154 choices = choices.push(choice);
155 }
156 container(choices)
157 .padding(5)
158 .width(Length::Fill)
159 .style(move |_| container::Style {
160 background: Some(p.surface.into()),
161 border: Border {
162 color: p.line,
163 width: 1.0,
164 radius: 999.0.into(),
165 },
166 ..container::Style::default()
167 })
168 .into()
169}
170
171/// A page in the sidebar.
172pub fn nav_item<'a>(look: Look, icon: Icon, label: &'a str, open: bool, on_press: Message) -> Element<'a, Message> {
173 let p = look.p;
174 let (color, icon_color) = if open { (p.fg, p.icon) } else { (p.dim, p.dim) };
175 let content = row![
176 icons::icon(icon, 19.0, icon_color),
177 text(label)
178 .size(14.5)
179 .font(if open { look.medium() } else { look.font })
180 .color(color)
181 ]
182 .spacing(13)
183 .align_y(Alignment::Center);
184 button(content)
185 .on_press(on_press)
186 .width(Length::Fill)
187 .padding([10, 14])
188 .style(move |_, status| button::Style {
189 background: if open {
190 Some(p.accent_dim.into())
191 } else {
192 hovered(status).then_some(p.surface_high.into())
193 },
194 text_color: color,
195 border: Border::default().rounded(12),
196 ..button::Style::default()
197 })
198 .into()
199}
200
201/// A wide button with an outline, like "Disconnect".
202pub fn outline_button<'a>(look: Look, label: &'a str, on_press: Message) -> Element<'a, Message> {
203 let p = look.p;
204 button(container(text(label).size(13.5).font(look.medium()).color(p.fg)).center_x(Length::Fill))
205 .on_press(on_press)
206 .width(Length::Fill)
207 .padding([8, 12])
208 .style(move |_, status| button::Style {
209 background: hovered(status).then_some(p.surface_high.into()),
210 text_color: p.fg,
211 border: Border {
212 color: p.line,
213 width: 1.0,
214 radius: 999.0.into(),
215 },
216 ..button::Style::default()
217 })
218 .into()
219}
220
221/// A small filled circle, like the "connected" light.
222pub fn light<'a>(color: iced::Color) -> Element<'a, Message> {
223 container(space())
224 .width(8)
225 .height(8)
226 .style(move |_| container::Style {
227 background: Some(color.into()),
228 border: Border::default().rounded(4),
229 ..container::Style::default()
230 })
231 .into()
232}
233
234/// A small outlined tag, like "demo".
235pub fn tag<'a>(look: Look, label: &'a str) -> Element<'a, Message> {
236 let p = look.p;
237 container(text(label).size(11).font(look.medium()).color(p.icon))
238 .padding([1, 8])
239 .style(move |_| container::Style {
240 border: Border {
241 color: p.icon,
242 width: 1.0,
243 radius: 999.0.into(),
244 },
245 ..container::Style::default()
246 })
247 .into()
248}
249
250/// A thin vertical line, between the sidebar and the page.
251pub fn divider<'a>(look: Look) -> Element<'a, Message> {
252 let p = look.p;
253 rule::vertical(1)
254 .style(move |_| rule::Style {
255 color: p.line,
256 radius: 0.0.into(),
257 fill_mode: rule::FillMode::Full,
258 snap: true,
259 })
260 .into()
261}