Krolyxon
Commit 99a16bb

Branches

Refs →

budctlcratesbudctl-guisrcui.rs

330 lines · 10.9 KB · Rust

1//! The controls of the owner's shell (Aurora), rebuilt in iced. Aurora is flat and dense: square
2//! corners, 1 px lines, accent fills with background-colored text, uppercase accent section
3//! labels, lowercase words.
4
5use iced::alignment::{Horizontal, Vertical};
6use iced::widget::{Button, Column, Row, button, column, container, progress_bar, row, rule, scrollable, space, text};
7use iced::{Alignment, Border, Color, Element, Length, Padding};
8
9use crate::Message;
10use crate::glyphs;
11use crate::theme::Look;
12
13/// The page title: uppercase and bold.
14pub fn title<'a>(look: Look, title: &str) -> Element<'a, Message> {
15 text(title.to_uppercase())
16 .size(look.px(4.0))
17 .font(look.bold())
18 .color(look.p.fg)
19 .into()
20}
21
22/// An uppercase accent label, with a line filling the rest of the row.
23pub fn section<'a>(look: Look, label: &str) -> Element<'a, Message> {
24 let p = look.p;
25 let line = rule::horizontal(1).style(move |_| rule::Style {
26 color: p.line,
27 radius: 0.0.into(),
28 fill_mode: rule::FillMode::Full,
29 snap: true,
30 });
31 row![text(label.to_uppercase()).size(look.px(-2.0)).color(p.accent), line]
32 .spacing(8)
33 .align_y(Alignment::Center)
34 .into()
35}
36
37/// Text in the base size and color.
38pub fn label<'a>(look: Look, words: impl text::IntoFragment<'a>) -> text::Text<'a> {
39 text(words).size(look.size).color(look.p.fg)
40}
41
42/// Smaller, dimmer text, for hints.
43pub fn hint<'a>(look: Look, words: impl text::IntoFragment<'a>) -> text::Text<'a> {
44 text(words).size(look.px(-2.0)).color(look.p.dim)
45}
46
47fn hovered(status: button::Status) -> bool {
48 matches!(status, button::Status::Hovered | button::Status::Pressed)
49}
50
51/// An on/off setting: its label, an optional hint below, and a square switch on the right. The
52/// whole row is the click target.
53pub fn switch_row<'a>(
54 look: Look,
55 name: String,
56 about: Option<&'a str>,
57 on: bool,
58 toggle: Message,
59) -> Element<'a, Message> {
60 let p = look.p;
61 let words = column![label(look, name)]
62 .push(about.map(|about| hint(look, about)))
63 .width(Length::Fill);
64 let content = row![words, switch(look, on)]
65 .spacing(10)
66 .align_y(Alignment::Center)
67 .padding([5, 0]);
68 button(content)
69 .on_press(toggle)
70 .width(Length::Fill)
71 .padding(0)
72 .style(move |_, status| button::Style {
73 background: hovered(status).then_some(p.surface.into()),
74 text_color: p.fg,
75 ..button::Style::default()
76 })
77 .into()
78}
79
80/// A square track with a square knob: left and dim when off, right on the accent when on.
81fn switch<'a>(look: Look, on: bool) -> Element<'a, Message> {
82 let p = look.p;
83 let knob = container(space())
84 .width(12)
85 .height(12)
86 .style(move |_| container::Style {
87 background: Some((if on { p.bg } else { p.dim }).into()),
88 ..container::Style::default()
89 });
90 container(knob)
91 .width(36)
92 .height(18)
93 .padding(3)
94 .align_x(if on { Horizontal::Right } else { Horizontal::Left })
95 .align_y(Vertical::Center)
96 .style(move |_| container::Style {
97 background: Some((if on { p.accent } else { p.surface_high }).into()),
98 border: Border {
99 color: if on { p.accent } else { p.line },
100 width: 1.0,
101 radius: 0.0.into(),
102 },
103 ..container::Style::default()
104 })
105 .into()
106}
107
108/// A button with a 1 px border, an optional glyph and a label. `active` fills it with the accent.
109pub fn button_<'a>(
110 look: Look,
111 glyph: Option<char>,
112 words: &'a str,
113 active: bool,
114 on_press: Message,
115) -> Button<'a, Message> {
116 let p = look.p;
117 let color = if active { p.bg } else { p.fg };
118 let content = Row::new()
119 .push(glyph.map(|g| text(g.to_string()).size(15).color(color)))
120 .push((!words.is_empty()).then(|| text(words).size(look.px(-1.0)).color(color)))
121 .spacing(8)
122 .align_y(Alignment::Center);
123 button(container(content).center(Length::Fill))
124 .on_press(on_press)
125 .height(26)
126 .padding(0)
127 .style(move |_, status| {
128 let hover = hovered(status);
129 button::Style {
130 background: if active {
131 Some(p.accent.into())
132 } else {
133 hover.then_some(p.surface_high.into())
134 },
135 text_color: color,
136 border: Border {
137 color: if active || hover { p.accent } else { p.line },
138 width: 1.0,
139 radius: 0.0.into(),
140 },
141 ..button::Style::default()
142 }
143 })
144}
145
146/// One option of a [`choice`].
147pub struct Opt<'a> {
148 pub glyph: Option<char>,
149 pub words: &'a str,
150 pub chosen: bool,
151 pub on_press: Message,
152}
153
154/// A label, then one button per option, all the same width, the chosen one filled.
155pub fn choice<'a>(look: Look, name: &'a str, options: Vec<Opt<'a>>) -> Element<'a, Message> {
156 let buttons = options.into_iter().fold(Row::new().spacing(2), |buttons, o| {
157 buttons.push(button_(look, o.glyph, o.words, o.chosen, o.on_press).width(Length::FillPortion(1)))
158 });
159 row![label(look, name).width(150), buttons]
160 .spacing(8)
161 .align_y(Alignment::Center)
162 .into()
163}
164
165/// A label, a level bar and the value, like a battery's charge.
166pub fn stat_bar<'a>(look: Look, name: &'a str, fraction: f32, value: String, fill: Color) -> Element<'a, Message> {
167 let p = look.p;
168 let bar = progress_bar(0.0..=1.0, fraction)
169 .girth(6)
170 .style(move |_| progress_bar::Style {
171 background: p.surface_high.into(),
172 bar: fill.into(),
173 border: Border::default(),
174 });
175 row![
176 text(name).size(look.px(-1.0)).color(p.dim).width(72),
177 bar,
178 text(value)
179 .size(look.px(-1.0))
180 .color(p.fg)
181 .width(96)
182 .align_x(Horizontal::Right)
183 ]
184 .spacing(10)
185 .align_y(Alignment::Center)
186 .into()
187}
188
189/// A row that opens a page: its label, an optional hint below, and on the right a value and an
190/// arrow. Lined up with the switch rows.
191pub fn link_row<'a>(
192 look: Look,
193 name: &'a str,
194 about: Option<&'a str>,
195 value: Option<String>,
196 on_press: Message,
197) -> Element<'a, Message> {
198 let p = look.p;
199 let words = column![label(look, name)]
200 .push(about.map(|about| hint(look, about)))
201 .width(Length::Fill);
202 let content = row![words]
203 .push(value.map(|value| text(value).size(look.px(-1.0)).color(p.dim)))
204 .push(text(glyphs::CHEVRON.to_string()).size(look.px(4.0)).color(p.dim))
205 .spacing(10)
206 .align_y(Alignment::Center)
207 .padding([5, 0]);
208 button(content)
209 .on_press(on_press)
210 .width(Length::Fill)
211 .padding(0)
212 .style(move |_, status| button::Style {
213 background: hovered(status).then_some(p.surface.into()),
214 text_color: p.fg,
215 ..button::Style::default()
216 })
217 .into()
218}
219
220/// The sidebar's title: a glyph and a name, bold, on the accent.
221pub fn sidebar_title<'a>(look: Look, glyph: char, name: &str) -> Element<'a, Message> {
222 text(format!("{glyph} {name}"))
223 .size(look.px(1.0))
224 .font(look.bold())
225 .color(look.p.accent)
226 .into()
227}
228
229/// A group heading in the sidebar.
230pub fn group<'a>(look: Look, name: &str) -> Element<'a, Message> {
231 text(name.to_uppercase()).size(look.px(-3.0)).color(look.p.dim).into()
232}
233
234/// A page in the sidebar. The open one is filled with the accent.
235pub fn nav_item<'a>(look: Look, glyph: char, name: &str, open: bool, on_press: Message) -> Element<'a, Message> {
236 let p = look.p;
237 let words = text(format!("{glyph} {name}"))
238 .size(look.size)
239 .font(if open { look.bold() } else { look.font })
240 .color(if open { p.bg } else { p.fg });
241 button(container(words).height(Length::Fill).align_y(Vertical::Center))
242 .on_press(on_press)
243 .width(Length::Fill)
244 .height(30)
245 .padding([0, 10])
246 .style(move |_, status| button::Style {
247 background: if open {
248 Some(p.accent.into())
249 } else {
250 hovered(status).then_some(p.surface_high.into())
251 },
252 text_color: if open { p.bg } else { p.fg },
253 ..button::Style::default()
254 })
255 .into()
256}
257
258/// The buds in the sidebar: a glyph, the name and a status line, in a 1 px frame that is the
259/// accent while they are connected.
260pub fn device<'a>(look: Look, name: &'a str, status: String, connected: bool) -> Element<'a, Message> {
261 let p = look.p;
262 let mark = if connected { p.accent } else { p.dim };
263 let content = row![
264 text(glyphs::HEADPHONES.to_string()).size(17).color(mark),
265 column![
266 text(name).size(look.size).font(look.bold()).color(p.fg),
267 text(status).size(look.px(-3.0)).color(mark)
268 ]
269 ]
270 .spacing(10)
271 .align_y(Alignment::Center);
272 container(content)
273 .padding([7, 10])
274 .width(Length::Fill)
275 .style(move |_| container::Style {
276 border: Border {
277 color: if connected { p.accent } else { p.line },
278 width: 1.0,
279 radius: 0.0.into(),
280 },
281 ..container::Style::default()
282 })
283 .into()
284}
285
286/// A column of sections with Aurora's spacing: 8 px between rows, a little more above each section.
287pub fn sections<'a>(sections: Vec<Vec<Element<'a, Message>>>) -> Element<'a, Message> {
288 let mut page = Column::new().spacing(14);
289 for rows in sections {
290 page = page.push(Column::with_children(rows).spacing(8));
291 }
292 page.into()
293}
294
295/// A scrolling area with a thin, square scrollbar, clear of the content.
296pub fn scroll<'a>(look: Look, content: Element<'a, Message>) -> Element<'a, Message> {
297 let p = look.p;
298 let padded = container(content).padding(Padding {
299 top: 0.0,
300 right: 14.0,
301 bottom: 8.0,
302 left: 0.0,
303 });
304 scrollable(padded)
305 .height(Length::Fill)
306 .style(move |theme, status| {
307 let mut style = scrollable::default(theme, status);
308 for rail in [&mut style.vertical_rail, &mut style.horizontal_rail] {
309 rail.background = None;
310 rail.border = Border::default();
311 rail.scroller.background = p.surface_high.into();
312 rail.scroller.border = Border::default();
313 }
314 style
315 })
316 .into()
317}
318
319/// A 1 px vertical line, between the sidebar and the page.
320pub fn divider<'a>(look: Look) -> Element<'a, Message> {
321 let p = look.p;
322 rule::vertical(1)
323 .style(move |_| rule::Style {
324 color: p.line,
325 radius: 0.0.into(),
326 fill_mode: rule::FillMode::Full,
327 snap: true,
328 })
329 .into()
330}