Krolyxon
Commit b1f5115

Branches

Refs →

budctlcratesbudctl-guisrcui.rs

322 lines · 10.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 let p = look.p;
241 let color = if filled { p.accent_fg } else { p.fg };
242 button(text(label).size(13.5).font(look.medium()).color(color))
243 .on_press(on_press)
244 .padding([7, 16])
245 .style(move |_, status| button::Style {
246 background: if filled {
247 Some(p.accent.into())
248 } else {
249 hovered(status).then_some(p.surface_high.into())
250 },
251 text_color: color,
252 border: Border {
253 color: if filled { p.accent } else { p.line },
254 width: 1.0,
255 radius: 999.0.into(),
256 },
257 ..button::Style::default()
258 })
259 .into()
260}
261
262/// A wide button with an outline, like "Disconnect".
263pub fn outline_button<'a>(look: Look, label: &'a str, on_press: Message) -> Element<'a, Message> {
264 let p = look.p;
265 button(container(text(label).size(13.5).font(look.medium()).color(p.fg)).center_x(Length::Fill))
266 .on_press(on_press)
267 .width(Length::Fill)
268 .padding([8, 12])
269 .style(move |_, status| button::Style {
270 background: hovered(status).then_some(p.surface_high.into()),
271 text_color: p.fg,
272 border: Border {
273 color: p.line,
274 width: 1.0,
275 radius: 999.0.into(),
276 },
277 ..button::Style::default()
278 })
279 .into()
280}
281
282/// A small filled circle, like the "connected" light.
283pub fn light<'a>(color: iced::Color) -> Element<'a, Message> {
284 container(space())
285 .width(8)
286 .height(8)
287 .style(move |_| container::Style {
288 background: Some(color.into()),
289 border: Border::default().rounded(4),
290 ..container::Style::default()
291 })
292 .into()
293}
294
295/// A small outlined tag, like "demo".
296pub fn tag<'a>(look: Look, label: &'a str) -> Element<'a, Message> {
297 let p = look.p;
298 container(text(label).size(11).font(look.medium()).color(p.icon))
299 .padding([1, 8])
300 .style(move |_| container::Style {
301 border: Border {
302 color: p.icon,
303 width: 1.0,
304 radius: 999.0.into(),
305 },
306 ..container::Style::default()
307 })
308 .into()
309}
310
311/// A thin vertical line, between the sidebar and the page.
312pub fn divider<'a>(look: Look) -> Element<'a, Message> {
313 let p = look.p;
314 rule::vertical(1)
315 .style(move |_| rule::Style {
316 color: p.line,
317 radius: 0.0.into(),
318 fill_mode: rule::FillMode::Full,
319 snap: true,
320 })
321 .into()
322}