Krolyxon
Commit 047794f

Branches

Refs →

budctlcratesbudctl-guisrcui.rs

352 lines · 11.5 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 heading above a card.
12pub fn heading<'a>(look: Look, words: &'a str) -> Element<'a, Message> {
13 text(words).size(15).font(look.medium()).color(look.p.dim).into()
14}
15
16/// A page title.
17pub fn title<'a>(look: Look, title: &'a str) -> Element<'a, Message> {
18 text(title).size(30).font(look.bold()).color(look.p.fg).into()
19}
20
21/// A rounded card holding rows, with a thin line between them.
22pub fn card<'a>(look: Look, rows: Vec<Element<'a, Message>>) -> Element<'a, Message> {
23 let p = look.p;
24 let mut column = Column::new();
25 for (n, item) in rows.into_iter().enumerate() {
26 if n > 0 {
27 column = column.push(separator(look));
28 }
29 column = column.push(item);
30 }
31 container(column)
32 .padding(5)
33 .width(Length::Fill)
34 .style(move |_| container::Style {
35 background: Some(p.surface.into()),
36 border: Border {
37 color: p.line,
38 width: 1.0,
39 radius: 18.0.into(),
40 },
41 ..container::Style::default()
42 })
43 .into()
44}
45
46/// A rounded panel like a card, around content of its own.
47pub fn panel<'a>(look: Look, content: Element<'a, Message>) -> Element<'a, Message> {
48 let p = look.p;
49 container(content)
50 .padding(24)
51 .width(Length::Fill)
52 .style(move |_| container::Style {
53 background: Some(p.surface.into()),
54 border: Border {
55 color: p.line,
56 width: 1.0,
57 radius: 18.0.into(),
58 },
59 ..container::Style::default()
60 })
61 .into()
62}
63
64fn separator<'a>(look: Look) -> Element<'a, Message> {
65 let p = look.p;
66 container(rule::horizontal(1).style(move |_| rule::Style {
67 color: p.line,
68 radius: 0.0.into(),
69 fill_mode: rule::FillMode::Full,
70 snap: true,
71 }))
72 .padding([0, 14])
73 .into()
74}
75
76/// A setting row: icon, title, an optional subtitle, and something on the right. With
77/// `on_press`, the whole row is the click target. `focused`: the keyboard cursor is on it.
78pub fn setting<'a>(
79 look: Look,
80 icon: Icon,
81 title: impl text::IntoFragment<'a>,
82 subtitle: Option<String>,
83 trailing: Element<'a, Message>,
84 on_press: Option<Message>,
85 focused: bool,
86) -> Element<'a, Message> {
87 let p = look.p;
88 let words = column![text(title).size(15).font(look.medium()).color(p.fg)]
89 .push(subtitle.map(|s| text(s).size(13).color(p.dim)))
90 .spacing(2)
91 .width(Length::Fill);
92 let content = row![icons::icon(icon, 22.0, p.icon), words, trailing]
93 .spacing(16)
94 .align_y(Alignment::Center)
95 .padding([13, 16]);
96 button(content)
97 .on_press_maybe(on_press)
98 .padding(0)
99 .width(Length::Fill)
100 .style(move |_, status| button::Style {
101 background: (focused || hovered(status)).then_some(p.surface_high.into()),
102 text_color: p.fg,
103 border: cursor(p, focused, 14.0),
104 ..button::Style::default()
105 })
106 .into()
107}
108
109/// The keyboard cursor: a 2 px accent frame. No frame without it.
110fn cursor(p: crate::theme::Palette, focused: bool, radius: f32) -> Border {
111 Border {
112 color: p.accent,
113 width: if focused { 2.0 } else { 0.0 },
114 radius: radius.into(),
115 }
116}
117
118fn hovered(status: button::Status) -> bool {
119 matches!(status, button::Status::Hovered | button::Status::Pressed)
120}
121
122/// An on/off switch. It only shows the state: the row around it takes the click.
123pub fn switch<'a>(look: Look, on: bool) -> Element<'a, Message> {
124 let p = look.p;
125 toggler(on)
126 .size(24)
127 .style(move |_, _| toggler::Style {
128 background: (if on { p.accent } else { p.surface_high }).into(),
129 background_border_width: 1.0,
130 background_border_color: if on { p.accent } else { p.line },
131 foreground: (if on { p.accent_fg } else { p.dim }).into(),
132 foreground_border_width: 0.0,
133 foreground_border_color: p.accent_fg,
134 text_color: None,
135 border_radius: None,
136 padding_ratio: 0.12,
137 })
138 .into()
139}
140
141/// A chevron: the row opens a page.
142pub fn chevron<'a>(look: Look) -> Element<'a, Message> {
143 icons::icon(Icon::Chevron, 18.0, look.p.dim)
144}
145
146/// One choice of a [`segmented`] control.
147pub struct Segment<'a> {
148 pub icon: Option<Icon>,
149 pub label: &'a str,
150 pub selected: bool,
151 pub on_press: Message,
152}
153
154/// A pill of choices with the selected one filled, like the noise-control modes. `large` puts an
155/// icon above each label. `focused`: the keyboard cursor is on it.
156pub fn segmented<'a>(look: Look, segments: Vec<Segment<'a>>, large: bool, focused: bool) -> Element<'a, Message> {
157 let p = look.p;
158 let mut choices = Row::new().spacing(4);
159 for segment in segments {
160 let selected = segment.selected;
161 let color = if selected { p.accent_fg } else { p.dim };
162 let label = text(segment.label)
163 .size(if large { 13.0 } else { 13.5 })
164 .font(look.medium())
165 .color(color);
166 let inner: Element<'a, Message> = match segment.icon {
167 Some(icon) if large => column![icons::icon(icon, 22.0, color), label]
168 .spacing(6)
169 .align_x(Alignment::Center)
170 .into(),
171 _ => label.into(),
172 };
173 let choice = button(container(inner).center_x(Length::Fill))
174 .on_press(segment.on_press)
175 .width(Length::Fill)
176 .padding(if large { [11, 8] } else { [7, 12] })
177 .style(move |_, status| button::Style {
178 background: if selected {
179 Some(p.accent.into())
180 } else {
181 hovered(status).then_some(p.surface_high.into())
182 },
183 text_color: color,
184 border: Border::default().rounded(999),
185 ..button::Style::default()
186 });
187 choices = choices.push(choice);
188 }
189 container(choices)
190 .padding(5)
191 .width(Length::Fill)
192 .style(move |_| container::Style {
193 background: Some(p.surface.into()),
194 border: if focused {
195 cursor(p, true, 999.0)
196 } else {
197 Border {
198 color: p.line,
199 width: 1.0,
200 radius: 999.0.into(),
201 }
202 },
203 ..container::Style::default()
204 })
205 .into()
206}
207
208/// A page in the sidebar.
209pub fn nav_item<'a>(look: Look, icon: Icon, label: &'a str, open: bool, on_press: Message) -> Element<'a, Message> {
210 let p = look.p;
211 let (color, icon_color) = if open { (p.fg, p.icon) } else { (p.dim, p.dim) };
212 let content = row![
213 icons::icon(icon, 19.0, icon_color),
214 text(label)
215 .size(14.5)
216 .font(if open { look.medium() } else { look.font })
217 .color(color)
218 ]
219 .spacing(13)
220 .align_y(Alignment::Center);
221 button(content)
222 .on_press(on_press)
223 .width(Length::Fill)
224 .padding([10, 14])
225 .style(move |_, status| button::Style {
226 background: if open {
227 Some(p.accent_dim.into())
228 } else {
229 hovered(status).then_some(p.surface_high.into())
230 },
231 text_color: color,
232 border: Border::default().rounded(12),
233 ..button::Style::default()
234 })
235 .into()
236}
237
238/// A small rounded button: outlined, or `filled` with the accent.
239pub fn pill<'a>(look: Look, label: &'a str, filled: bool, on_press: Message) -> Element<'a, Message> {
240 pill_with(look, label, filled, false, on_press)
241}
242
243/// A [`pill`] that can carry the keyboard cursor.
244pub fn pill_with<'a>(
245 look: Look,
246 label: &'a str,
247 filled: bool,
248 focused: bool,
249 on_press: Message,
250) -> Element<'a, Message> {
251 let p = look.p;
252 let color = if filled { p.accent_fg } else { p.fg };
253 button(text(label).size(13.5).font(look.medium()).color(color))
254 .on_press(on_press)
255 .padding([7, 16])
256 .style(move |_, status| button::Style {
257 background: if filled {
258 Some(p.accent.into())
259 } else {
260 (focused || hovered(status)).then_some(p.surface_high.into())
261 },
262 text_color: color,
263 border: Border {
264 color: if filled || focused { p.accent } else { p.line },
265 width: if focused { 2.0 } else { 1.0 },
266 radius: 999.0.into(),
267 },
268 ..button::Style::default()
269 })
270 .into()
271}
272
273/// A round button with just an icon, like the pencil on a preset's row.
274pub fn icon_button<'a>(look: Look, icon: Icon, focused: bool, on_press: Message) -> Element<'a, Message> {
275 let p = look.p;
276 button(icons::icon(icon, 19.0, if focused { p.fg } else { p.dim }))
277 .on_press(on_press)
278 .padding(8)
279 .style(move |_, status| button::Style {
280 background: (focused || hovered(status)).then_some(p.surface_highest.into()),
281 text_color: p.fg,
282 border: Border {
283 color: p.accent,
284 width: if focused { 2.0 } else { 0.0 },
285 radius: 999.0.into(),
286 },
287 ..button::Style::default()
288 })
289 .into()
290}
291
292/// A wide button with an outline, like "Disconnect".
293pub fn outline_button<'a>(look: Look, label: &'a str, on_press: Message) -> Element<'a, Message> {
294 let p = look.p;
295 button(container(text(label).size(13.5).font(look.medium()).color(p.fg)).center_x(Length::Fill))
296 .on_press(on_press)
297 .width(Length::Fill)
298 .padding([8, 12])
299 .style(move |_, status| button::Style {
300 background: hovered(status).then_some(p.surface_high.into()),
301 text_color: p.fg,
302 border: Border {
303 color: p.line,
304 width: 1.0,
305 radius: 999.0.into(),
306 },
307 ..button::Style::default()
308 })
309 .into()
310}
311
312/// A small filled circle, like the "connected" light.
313pub fn light<'a>(color: iced::Color) -> Element<'a, Message> {
314 container(space())
315 .width(8)
316 .height(8)
317 .style(move |_| container::Style {
318 background: Some(color.into()),
319 border: Border::default().rounded(4),
320 ..container::Style::default()
321 })
322 .into()
323}
324
325/// A small outlined tag, like "demo".
326pub fn tag<'a>(look: Look, label: &'a str) -> Element<'a, Message> {
327 let p = look.p;
328 container(text(label).size(11).font(look.medium()).color(p.icon))
329 .padding([1, 8])
330 .style(move |_| container::Style {
331 border: Border {
332 color: p.icon,
333 width: 1.0,
334 radius: 999.0.into(),
335 },
336 ..container::Style::default()
337 })
338 .into()
339}
340
341/// A thin vertical line, between the sidebar and the page.
342pub fn divider<'a>(look: Look) -> Element<'a, Message> {
343 let p = look.p;
344 rule::vertical(1)
345 .style(move |_| rule::Style {
346 color: p.line,
347 radius: 0.0.into(),
348 fill_mode: rule::FillMode::Full,
349 snap: true,
350 })
351 .into()
352}