system settings
git clone https://git.lucas.co/cce-system-interface.git
src/app.rs (25.8K)
1 use cce_ui::layout::RenderTarget;
2
3 use crate::pages::audio;
4 use crate::pages::bluetooth;
5 use crate::pages::power;
6 use crate::pages::default_apps;
7 use crate::pages::network;
8 use crate::pages::processes;
9 use crate::pages::services;
10 use crate::pages::system_info;
11 use crate::pages::timers;
12 use crate::pages::storage;
13 use crate::pages::accounts;
14 use crate::pages::packages;
15 use crate::pages::notifications;
16 use crate::pages::browser;
17 use crate::pages::Page;
18
19 pub struct AppState {
20 pub current_page: Page,
21 pub audio: audio::AudioState,
22 pub bluetooth: bluetooth::BluetoothState,
23 pub power: power::PowerState,
24 pub browser: browser::BrowserState,
25 pub default_apps: default_apps::DefaultAppsState,
26 pub network: network::NetworkState,
27 pub processes: processes::ProcessesState,
28 pub services: services::ServicesState,
29 pub system_info: system_info::SystemState,
30 pub timers: timers::TimersState,
31 pub storage: storage::StorageState,
32 pub accounts: accounts::AccountsState,
33 pub packages: packages::PackagesState,
34 pub notifications: notifications::NotificationsState,
35 }
36
37 impl AppState {
38 /// Every page's state, their widgets inserted into `ctx`.
39 pub fn new(ctx: &mut cce_ui::context::UiContext) -> Self {
40 Self {
41 current_page: Page::ALL[0],
42 audio: audio::AudioState::default(),
43 bluetooth: bluetooth::BluetoothState::new(ctx),
44 power: power::PowerState::new(ctx),
45 browser: browser::BrowserState::new(ctx),
46 default_apps: default_apps::DefaultAppsState::new(ctx),
47 network: network::NetworkState::new(ctx),
48 processes: processes::ProcessesState::default(),
49 services: services::ServicesState::new(ctx),
50 system_info: system_info::SystemState::new(ctx),
51 timers: timers::TimersState::new(ctx),
52 storage: storage::StorageState::new(ctx),
53 accounts: accounts::AccountsState::new(ctx),
54 packages: packages::PackagesState::new(ctx),
55 notifications: notifications::NotificationsState::new(ctx),
56 }
57 }
58 }
59
60 impl AppState {
61 pub fn get_page(&self, page: Page) -> &dyn crate::pages::AppPage {
62 match page {
63 Page::Accounts => &self.accounts,
64 Page::Audio => &self.audio,
65 Page::Bluetooth => &self.bluetooth,
66 Page::Power => &self.power,
67 Page::Browser => &self.browser,
68 Page::DefaultApps => &self.default_apps,
69 Page::Packages => &self.packages,
70 Page::Processes => &self.processes,
71 Page::Services => &self.services,
72 Page::Network => &self.network,
73 Page::Storage => &self.storage,
74 Page::System => &self.system_info,
75 Page::Timers => &self.timers,
76 Page::Notifications => &self.notifications,
77 }
78 }
79
80 pub fn get_page_mut(&mut self, page: Page) -> &mut dyn crate::pages::AppPage {
81 match page {
82 Page::Accounts => &mut self.accounts,
83 Page::Audio => &mut self.audio,
84 Page::Bluetooth => &mut self.bluetooth,
85 Page::Power => &mut self.power,
86 Page::Browser => &mut self.browser,
87 Page::DefaultApps => &mut self.default_apps,
88 Page::Packages => &mut self.packages,
89 Page::Processes => &mut self.processes,
90 Page::Services => &mut self.services,
91 Page::Network => &mut self.network,
92 Page::Storage => &mut self.storage,
93 Page::System => &mut self.system_info,
94 Page::Timers => &mut self.timers,
95 Page::Notifications => &mut self.notifications,
96 }
97 }
98
99 pub fn get_current_page(&self) -> &dyn crate::pages::AppPage {
100 self.get_page(self.current_page)
101 }
102
103 pub fn get_current_page_mut(&mut self) -> &mut dyn crate::pages::AppPage {
104 self.get_page_mut(self.current_page)
105 }
106 }
107
108 #[derive(Debug, Clone)]
109 pub enum AppAction {
110 Exit,
111 /// A page worker sent a snapshot; the next tick drains it. Carries
112 /// nothing and rebuilds nothing by itself.
113 Wake,
114 Audio(audio::AudioMessage),
115 Browser(browser::BrowserMessage),
116 DefaultApps(default_apps::DefaultAppsMessage),
117 Network(network::NetworkMessage),
118 Bluetooth(bluetooth::BluetoothMessage),
119 Power(power::PowerMessage),
120 Processes(processes::ProcessesMessage),
121 Services(services::ServicesMessage),
122 SystemInfo(system_info::SystemMessage),
123 Timers(timers::TimersMessage),
124 Storage(storage::StorageMessage),
125 Accounts(accounts::AccountsMessage),
126 Packages(packages::PackagesMessage),
127 Notifications(notifications::NotificationsMessage),
128 }
129
130
131 /// A control carve bridged out of a widget's `paint` for the flat path — the
132 /// relief prims the toolkit's controls draw for themselves and the legacy
133 /// `all_quads` stream cannot carry. The page collects them and `display_list`
134 /// re-emits them as real prims into the window plate.
135 #[derive(Clone, Copy, Debug, PartialEq)]
136 pub enum ControlCarve {
137 /// `PaintCtx::inset_plate` — a flush inset face over a boundary seam
138 /// (Dropdown, Button). `color` fills the face; transparent leaves the
139 /// plate below.
140 Plate { x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, color: [f32; 4], tint: Option<[f32; 3]> },
141 /// A step carve straight from the toolkit (TextBox well, a Toggle's well
142 /// and glider) — it already carries its own rect, per-corner radii,
143 /// depth and wall mask, so nothing here re-derives them.
144 Step(cce_ui::layout::ReliefCarve),
145 }
146
147 impl ControlCarve {
148 /// This carve shifted vertically — the page-scroll adjustment the popover
149 /// collector applies when it lifts page carves to the popover layer.
150 pub fn shifted_y(self, dy: f32) -> Self {
151 match self {
152 Self::Plate { x, y, w, h, radius, depth, color, tint } => {
153 Self::Plate { x, y: y + dy, w, h, radius, depth, color, tint }
154 }
155 Self::Step(c) => Self::Step(c.shifted_y(dy)),
156 }
157 }
158 }
159
160 /// A bundled cce-icons glyph a page placed on its own (a sort chevron, a
161 /// row's in-use check) rather than as a button's face: the tinted upload's
162 /// image id, its rect and alpha, and the innermost clip at emission time —
163 /// all page coordinates, pre-scroll, like `texts`.
164 #[derive(Clone, Copy, Debug, PartialEq)]
165 pub struct PageIcon {
166 pub image: u32,
167 pub x: f32,
168 pub y: f32,
169 pub w: f32,
170 pub h: f32,
171 pub alpha: f32,
172 pub clip: Option<[f32; 4]>,
173 }
174
175 /// The width `text` takes at `size` in `font` (None = the default UI face,
176 /// what `PageContent::text` draws in), shaped as the renderer shapes it —
177 /// for placing a glyph right after a run of text.
178 pub fn text_width(text: &str, size: f32, font: Option<&str>) -> f32 {
179 cce_ui::geometry_font_system()
180 .lock()
181 .ok()
182 .and_then(|mut fs| {
183 cce_ui::backend::text::shaped_cluster_offsets(&mut fs, text, size, font)
184 .last()
185 .map(|&(_, total)| total)
186 })
187 .unwrap_or(0.0)
188 }
189
190 /// The y a label of `size` in `font` takes to sit centred in a band of
191 /// height `h` at `y` — the renderer's own centring of a button label, so a
192 /// run placed beside one shares its line.
193 pub fn label_y_in(y: f32, h: f32, size: f32, font: Option<&str>) -> f32 {
194 let lh = cce_ui::geometry_font_system()
195 .lock()
196 .map(|mut fs| cce_ui::backend::get_text_buffer(&mut fs, "Ag", size, font).metrics().line_height)
197 .unwrap_or(size * 1.2)
198 / cce_ui::scale::scale_factor();
199 y + (h - lh) / 2.0
200 }
201
202 pub struct PageContent {
203 pub rects: Vec<([f32; 4], f32, f32, f32, f32, f32, (bool, bool, bool, bool))>,
204 pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
205 /// (button, action, clip): clip is the innermost push_clip_rect at emission
206 /// time (page coords) — the renderer clamps the drawn quad, label bounds,
207 /// and the dispatch clone's hit rect to it.
208 pub buttons: Vec<(cce_ui::widget::Adapted<cce_ui::widget::Button>, AppAction, Option<[f32; 4]>)>,
209 /// Glyphs placed with [`PageContent::icon`] (and a widget's own, through
210 /// `RenderTarget::icon` — the expanded Dropdown's chevron).
211 pub icons: Vec<PageIcon>,
212 /// Section wells claimed via `RenderTarget::section_relief` — the body box
213 /// plus the title tab box, carved into the window plate by display_list as
214 /// recess prims (page coordinates, pre-scroll).
215 pub reliefs: Vec<((f32, f32, f32, f32), Option<(f32, f32, f32, f32)>)>,
216 /// Control carves claimed via the flat-path relief hooks
217 /// (`RenderTarget::inset_plate` for a Dropdown's flush inset chrome,
218 /// `RenderTarget::recess` for a TextBox's well) — (x, y, w, h, radius,
219 /// depth, carve), page coordinates, pre-scroll; display_list re-emits them
220 /// as real relief prims.
221 pub control_reliefs: Vec<ControlCarve>,
222 /// Per carve, `rects.len()` at the moment it was claimed — the carve's
223 /// place in the widget's own emission order. A Dropdown draws its
224 /// hovered-row highlight AFTER the inset plate it claims for the menu
225 /// face; replaying every rect and then every carve put the frosted plate
226 /// over the highlight. The popover layer interleaves on these marks.
227 pub control_relief_marks: Vec<usize>,
228 pub clip_stack: Vec<[f32; 4]>,
229 }
230
231 impl PageContent {
232 pub fn new() -> Self {
233 Self {
234 rects: Vec::new(),
235 texts: Vec::new(),
236 buttons: Vec::new(),
237 icons: Vec::new(),
238 reliefs: Vec::new(),
239 control_reliefs: Vec::new(),
240 control_relief_marks: Vec::new(),
241 clip_stack: Vec::new(),
242 }
243 }
244
245 fn get_clipped_rect(&self, x: f32, y: f32, w: f32, h: f32) -> Option<(f32, f32, f32, f32)> {
246 if let Some(&clip) = self.clip_stack.last() {
247 let cx = clip[0];
248 let cy = clip[1];
249 let cw = clip[2];
250 let ch = clip[3];
251 let rx1 = x.max(cx);
252 let ry1 = y.max(cy);
253 let rx2 = (x + w).min(cx + cw);
254 let ry2 = (y + h).min(cy + ch);
255 if rx1 < rx2 && ry1 < ry2 {
256 Some((rx1, ry1, rx2 - rx1, ry2 - ry1))
257 } else {
258 None
259 }
260 } else {
261 Some((x, y, w, h))
262 }
263 }
264
265 fn get_clipped_bounds(&self, bounds: Option<[f32; 4]>) -> Option<[f32; 4]> {
266 if let Some(&clip) = self.clip_stack.last() {
267 if let Some(b) = bounds {
268 let rx1 = b[0].max(clip[0]);
269 let ry1 = b[1].max(clip[1]);
270 let rx2 = b[2].min(clip[0] + clip[2]);
271 let ry2 = b[3].min(clip[1] + clip[3]);
272 Some([rx1, ry1, rx2.max(rx1), ry2.max(ry1)])
273 } else {
274 Some([clip[0], clip[1], clip[0] + clip[2], clip[1] + clip[3]])
275 }
276 } else {
277 bounds
278 }
279 }
280
281 pub fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
282 if let Some((cx, cy, cw, ch)) = self.get_clipped_rect(x, y, w, h) {
283 self.rects.push((color, cx, cy, cw, ch, 0.0, (true, true, true, true)));
284 }
285 }
286
287 pub fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
288 let cb = self.get_clipped_bounds(None);
289 self.texts.push((content.to_string(), size, x, y, color, None, cb));
290 }
291
292 pub fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
293 let cb = self.get_clipped_bounds(None);
294 self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), cb));
295 }
296
297 /// A bundled cce-icons glyph at (x, y, w, h), tinted `color` as a text
298 /// colour is (raw sRGB, alpha = the image's) — the ONE way a page draws a
299 /// symbol, never a character in whatever face the font falls back to.
300 /// Rasterized at twice the rect's longer side for a 2x output.
301 ///
302 /// `false` when the icon set lacks the glyph, so the caller can say it
303 /// in a WORD instead.
304 pub fn icon(&mut self, name: &str, x: f32, y: f32, w: f32, h: f32, color: [f32; 4]) -> bool {
305 let px = (w.max(h) * 2.0).ceil().max(1.0) as u32;
306 let Some((image, _, _)) = cce_ui::upload_icon_tinted(name, px, cce_ui::icon_tint(color)) else {
307 return false;
308 };
309 let clip = self.clip_stack.last().copied();
310 self.icons.push(PageIcon { image, x, y, w, h, alpha: color[3], clip });
311 true
312 }
313
314 pub fn button(&mut self, label: &str, x: f32, y: f32, w: f32, h: f32,
315 bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4],
316 action: AppAction) {
317 let btn = cce_ui::widget::Button::new(x, y, w, h)
318 .with_label(label)
319 .with_bg(bg)
320 .with_hover_bg(hover_bg)
321 .with_label_color(label_color);
322 let clip = self.clip_stack.last().copied();
323 self.buttons.push((btn, action, clip));
324 }
325
326 /// A button whose face is a bundled cce-icons glyph instead of a label.
327 ///
328 /// `label` stays as the FALLBACK: `upload_icon` returns `None` when the
329 /// icon set is missing or unparsable, and a control that silently loses
330 /// its face has no affordance left at all — so the button degrades to the
331 /// word rather than to an empty box. `alpha` dims the glyph for a disabled
332 /// control, which is the only state lever an icon has (images carry no
333 /// color).
334 pub fn button_icon(&mut self, icon: &str, label: &str, x: f32, y: f32, w: f32, h: f32,
335 bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4],
336 alpha: f32, action: AppAction) {
337 let mut btn = cce_ui::widget::Button::new(x, y, w, h)
338 .with_label(label)
339 .with_bg(bg)
340 .with_hover_bg(hover_bg)
341 .with_label_color(label_color);
342 // 32px on the longer side: the toolkit caches the upload per (name, px),
343 // so every row's Start button shares one texture.
344 if let Some((id, iw, ih)) = cce_ui::upload_icon(icon, 32) {
345 btn = btn.with_icon(id, iw as f32, ih as f32).with_icon_alpha(alpha);
346 }
347 let clip = self.clip_stack.last().copied();
348 self.buttons.push((btn, action, clip));
349 }
350
351 /// A row of a list, in the toolkit's list style (`Button::new_list_row`,
352 /// what cce-mail, cce-fonts and cce-calendar rows wear): no plate,
353 /// transparent until hovered, the shared row wash. A plain [`button`]
354 /// with a transparent face is NOT that — under `control_relief` it still
355 /// gets a control plate, edges only, so every idle row of a list wore a
356 /// carved ring. No colours to pass: the renderer asks the row for its own.
357 ///
358 /// [`button`]: PageContent::button
359 pub fn list_row(&mut self, x: f32, y: f32, w: f32, h: f32, action: AppAction) {
360 let btn = cce_ui::widget::Button::new_list_row(x, y, w, h);
361 let clip = self.clip_stack.last().copied();
362 self.buttons.push((btn, action, clip));
363 }
364
365 /// [`button_icon`](Self::button_icon) with the glyph tinted
366 /// `label_color` (its alpha the glyph's), where the face's colour is part
367 /// of what the control says — the process list's red Kill. The white
368 /// glyph `button_icon` uploads cannot carry a colour: an image has none.
369 pub fn button_icon_tinted(&mut self, icon: &str, label: &str, x: f32, y: f32, w: f32, h: f32,
370 bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4],
371 action: AppAction) {
372 let mut btn = cce_ui::widget::Button::new(x, y, w, h)
373 .with_label(label)
374 .with_bg(bg)
375 .with_hover_bg(hover_bg)
376 .with_label_color(label_color);
377 if let Some((id, iw, ih)) = cce_ui::upload_icon_tinted(icon, 32, cce_ui::icon_tint(label_color)) {
378 btn = btn.with_icon(id, iw as f32, ih as f32).with_icon_alpha(label_color[3]);
379 }
380 let clip = self.clip_stack.last().copied();
381 self.buttons.push((btn, action, clip));
382 }
383
384 pub fn button_left(&mut self, label: &str, x: f32, y: f32, w: f32, h: f32,
385 bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4],
386 action: AppAction) {
387 let btn = cce_ui::widget::Button::new(x, y, w, h)
388 .with_label(label)
389 .with_bg(bg)
390 .with_hover_bg(hover_bg)
391 .with_label_color(label_color)
392 .with_left_align(true);
393 let clip = self.clip_stack.last().copied();
394 self.buttons.push((btn, action, clip));
395 }
396 }
397
398 impl RenderTarget for PageContent {
399 fn icon(&mut self, name: &str, rect: cce_ui::scene::layout::Rect, color: [f32; 4]) {
400 PageContent::icon(self, name, rect.x, rect.y, rect.width, rect.height, color);
401 }
402
403 fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
404 if let Some((cx, cy, cw, ch)) = self.get_clipped_rect(x, y, w, h) {
405 self.rects.push((color, cx, cy, cw, ch, 0.0, (true, true, true, true)));
406 }
407 }
408
409 fn rect_with_radius(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32) {
410 if let Some((cx, cy, cw, ch)) = self.get_clipped_rect(x, y, w, h) {
411 self.rects.push((color, cx, cy, cw, ch, radius, (true, true, true, true)));
412 }
413 }
414
415 fn rect_with_radius_corners(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, corners: (bool, bool, bool, bool)) {
416 if let Some((cx, cy, cw, ch)) = self.get_clipped_rect(x, y, w, h) {
417 self.rects.push((color, cx, cy, cw, ch, radius, corners));
418 }
419 }
420
421 fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
422 let cb = self.get_clipped_bounds(None);
423 self.texts.push((content.to_string(), size, x, y, color, None, cb));
424 }
425
426 fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
427 let cb = self.get_clipped_bounds(None);
428 self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), cb));
429 }
430
431 fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
432 let cb = self.get_clipped_bounds(bounds);
433 self.texts.push((content.to_string(), size, x, y, color, None, cb));
434 }
435
436 fn text_with_font_and_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, bounds: Option<[f32; 4]>) {
437 let cb = self.get_clipped_bounds(bounds);
438 self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), cb));
439 }
440
441 fn section_relief_style(&self) -> bool {
442 cce_ui::layout::control_relief()
443 }
444
445 /// The flat-path bridge offers each Dropdown's flush inset trough here;
446 /// carve it for real (display_list turns these into inset plates).
447 fn inset_plate(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32) {
448 self.control_relief_marks.push(self.rects.len());
449 self.control_reliefs.push(ControlCarve::Plate { x, y, w, h, radius, depth, color, tint: None });
450 }
451 fn inset_plate_tinted(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, tint: [f32; 3]) {
452 self.control_reliefs.push(ControlCarve::Plate { x, y, w, h, radius, depth, color, tint: Some(tint) });
453 }
454
455 /// The same bridge for the step carves — a DIFFERENT shape, not an inset
456 /// plate at another rect: a trough is a seam about the boundary with the
457 /// face left level, a well drops the whole interior, a boss raises it.
458 /// Collapsing them would give this app controls no other relief host has.
459 fn relief_carve(&mut self, carve: &cce_ui::layout::ReliefCarve) {
460 self.control_relief_marks.push(self.rects.len());
461 self.control_reliefs.push(ControlCarve::Step(*carve));
462 }
463
464 fn section_relief(&mut self, f: &cce_ui::layout::SectionFrame) -> bool {
465 // Focused sections keep the legacy green outline (the ctrl-nav feedback);
466 // relief-off styling keeps the outline everywhere.
467 if f.focused || !cce_ui::layout::control_relief() {
468 return false;
469 }
470 self.reliefs.push(((f.x, f.y, f.w, f.h), f.tab));
471 true
472 }
473
474 fn push_clip_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
475 let clip = if let Some(&parent_clip) = self.clip_stack.last() {
476 let cx = x.max(parent_clip[0]);
477 let cy = y.max(parent_clip[1]);
478 let cw = (x + w).min(parent_clip[0] + parent_clip[2]) - cx;
479 let ch = (y + h).min(parent_clip[1] + parent_clip[3]) - cy;
480 [cx, cy, cw.max(0.0), ch.max(0.0)]
481 } else {
482 [x, y, w, h]
483 };
484 self.clip_stack.push(clip);
485 }
486
487 fn pop_clip_rect(&mut self) {
488 self.clip_stack.pop();
489 }
490 }
491
492
493 /// Width a label needs on a button plate. Feeds [`form_button_fit`], so a row of
494 /// buttons is divided by what is written on them rather than into equal
495 /// slices — "Reboot" and "Hibernate" are not the same size and a row that
496 /// pretends otherwise clips one and pads the other.
497 ///
498 /// Asks the Button itself (`intrinsic_size`: its label shaped in the button
499 /// font, plus its 8px inset each side) rather than measuring here. This used
500 /// `measure_text_width` in the control-label font, an inked extent that runs
501 /// ~20% short of the shaped run under Berkeley Mono, so the row handed every
502 /// button less than its label and "Hibernate" / "Power Off" were cut off.
503 pub fn button_need(label: &str) -> f32 {
504 cce_ui::widget::Button::new(0.0, 0.0, 0.0, 0.0)
505 .with_label(label)
506 .intrinsic_size()
507 .map_or(0.0, |s| s.width)
508 }
509
510 /// A page button (`PageContent::button`) as a form piece, a button's height: `w` wide, or
511 /// the width of its column (and, in a row, the row's slack) when `w` is 0. `colors` are the
512 /// face, the hover face and the label.
513 pub fn form_button<'w>(
514 g: &mut cce_ui::layout::FormGroup<'_, 'w, PageContent>,
515 label: impl Into<String>,
516 w: f32,
517 colors: ([f32; 4], [f32; 4], [f32; 4]),
518 action: AppAction,
519 ) {
520 let label = label.into();
521 g.draw(w, cce_ui::layout::button_height(), w == 0.0, move |pc, r, _| {
522 pc.button(&label, r.x, r.y, r.width, r.height, colors.0, colors.1, colors.2, action);
523 });
524 }
525
526 /// A page button as a row cell that asks for `need` (its label's own width) and shares the
527 /// row's slack with the other cells — a row of buttons sized to their labels.
528 pub fn form_button_fit<'w>(
529 g: &mut cce_ui::layout::FormGroup<'_, 'w, PageContent>,
530 label: impl Into<String>,
531 need: f32,
532 colors: ([f32; 4], [f32; 4], [f32; 4]),
533 action: AppAction,
534 ) {
535 let label = label.into();
536 g.draw(need, cce_ui::layout::button_height(), true, move |pc, r, _| {
537 pc.button(&label, r.x, r.y, r.width, r.height, colors.0, colors.1, colors.2, action);
538 });
539 }
540
541 /// `text` broken into lines no wider than `width` at `size`, measured as the
542 /// renderer shapes it (the default UI face, as `PageContent::text` draws).
543 /// Breaks after a space or before a `/`, so a path splits on its separators;
544 /// a run with neither is cut wherever it overflows. A section's text is
545 /// clipped to its content box, so anything that does not fit has to wrap or
546 /// it is simply lost — a GPU name, the CPU model, a pending install path.
547 pub fn wrap_to_width(text: &str, width: f32, size: f32) -> Vec<String> {
548 let offsets = cce_ui::geometry_font_system()
549 .lock()
550 .map(|mut fs| cce_ui::backend::text::shaped_cluster_offsets(&mut fs, text, size, None))
551 .unwrap_or_default();
552 if offsets.last().map_or(true, |&(_, total)| total <= width) {
553 return vec![text.to_string()];
554 }
555 let mut lines = Vec::new();
556 let (mut start, mut start_x) = (0usize, 0.0f32);
557 // Where the next line would begin if this one broke at the last chance.
558 let mut chance: Option<(usize, f32)> = None;
559 for pair in offsets.windows(2) {
560 let ((b, x), (next_b, next_x)) = (pair[0], pair[1]);
561 if next_x - start_x > width && b > start {
562 let (cut, cut_x) = chance.filter(|&(c, _)| c > start).unwrap_or((b, x));
563 lines.push(text[start..cut].trim_end().to_string());
564 (start, start_x, chance) = (cut, cut_x, None);
565 }
566 match &text[b..next_b] {
567 " " => chance = Some((next_b, next_x)),
568 "/" if b > start => chance = Some((b, x)),
569 _ => {}
570 }
571 }
572 lines.push(text[start..].trim_end().to_string());
573 lines.retain(|l| !l.is_empty());
574 lines
575 }
576
577 /// Label / value pairs as one block of text: the labels in a column as wide as the widest of
578 /// them, each value beside its label and wrapped to the room left of the content box, the
579 /// label on the value's first line. Each pair is (label, label colour, value, value colour).
580 pub fn form_pairs<'w>(
581 col: &mut cce_ui::layout::FormGroup<'_, 'w, PageContent>,
582 size: f32,
583 pairs: Vec<(String, [f32; 4], String, [f32; 4])>,
584 ) {
585 use cce_ui::scene::layout::{CrossAlign, Style};
586 let label_w = pairs.iter().map(|p| cce_ui::layout::form_text_width(&p.0, size)).fold(0.0, f32::max);
587 let line_h = cce_ui::layout::form_line_height(size);
588 let value_w = (col.form_width() - label_w - cce_ui::layout::control_gap()).max(1.0);
589 // Lines of text, not controls: they stand a line apart, with no control gap between.
590 col.group(Style::column().cross_align(CrossAlign::Stretch), false, |lines| {
591 for (label, label_color, value, value_color) in pairs {
592 lines.group(Style::controls_row(), true, |row| {
593 row.draw(label_w, line_h, false, move |pc, r, _| pc.text(&label, r.x, r.y, size, label_color));
594 row.lines(wrap_to_width(&value, value_w, size), size, value_color);
595 });
596 }
597 });
598 }
599
600 /// The hairline that parts a well's zones, as a form piece.
601 pub fn form_divider(g: &mut cce_ui::layout::FormGroup<'_, '_, PageContent>) {
602 g.rule([1.0, 1.0, 1.0, 0.06]);
603 }
604