Krolyxon
Commit 19d99c1

Branches

Refs →

budctlcratesbudctl-guisrcui.rs

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