system settings
git clone https://git.lucas.co/cce-system-interface.git
refactor: list rows and the process table lay out by list_gap
Every scrolling list's row interior goes through cce-ui's lay_row on the new
list_gap rung instead of its own insets and gaps: services' controls and
name, timers' dot, name and button run, the packages / accounts / wifi row
items, accounts' marks. Word buttons are as wide as their words. A timer's
item is cut at its cell, so a long schedule no longer runs under Run Now.
The process table's columns are as wide as their header or widest likely
value, with COMMAND taking the slack: a wide window shows the whole table,
a narrow one scrolls it. Header sort targets are their whole column; text is
centred in its row. section_margin went with its last user.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/app.rs | 12 --
src/pages/accounts.rs | 27 ++---
src/pages/network.rs | 12 +-
src/pages/packages.rs | 9 +-
src/pages/processes.rs | 311 ++++++++++++++++++++++++++-----------------------
src/pages/services.rs | 35 +++---
src/pages/timers.rs | 56 +++++----
7 files changed, 241 insertions(+), 221 deletions(-)
diff --git a/src/app.rs b/src/app.rs
index 503008d..248a1d5 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -490,18 +490,6 @@ impl RenderTarget for PageContent {
}
-/// The inset from a section well's rim to the app's own content, on top of
-/// `section_padding()`. cce-ui's `SectionContext` lays its title tab, its
-/// grid and a VStack's widgets out `2 * padding + DEFAULT_MARGIN_X` in, and
-/// `finish()` lands the well's wall `padding + DEFAULT_MARGIN_X` past the
-/// content — a rect the app places inside a well has to match that number or
-/// sit off the toolkit's own geometry. A well is this app's pane, so this is
-/// where the pane rung belongs; TODO(style): become `plate_padding()` once
-/// SectionContext reads the rung instead of its literal.
-pub fn section_margin() -> f32 {
- cce_ui::layout::SectionContext::<PageContent>::DEFAULT_MARGIN_X
-}
-
/// Width a label needs on a button plate. Feeds [`form_button_fit`], so a row of
/// buttons is divided by what is written on them rather than into equal
/// slices — "Reboot" and "Hibernate" are not the same size and a row that
diff --git a/src/pages/accounts.rs b/src/pages/accounts.rs
index f4a8a1d..fbf458a 100644
--- a/src/pages/accounts.rs
+++ b/src/pages/accounts.rs
@@ -1,6 +1,7 @@
use crate::app::{form_button, form_divider, form_pairs, AppAction, PageContent};
use cce_ui::widget::Owned;
-use cce_ui::layout::{PageLayoutBuilder, PageFlow, RenderTarget};
+use cce_ui::layout::{lay_row, Cell, PageLayoutBuilder, PageFlow, RenderTarget};
+use cce_ui::scene::layout::Rect;
use cce_ui::widget::ScrollRegion;
use cce_ui::widget::TextBox;
@@ -616,8 +617,8 @@ const TEXT_DANGER: [f32; 4] = [0.95, 0.55, 0.55, 1.0];
const TEXT_WARN: [f32; 4] = [0.90, 0.75, 0.40, 1.0];
/// Gap between account rows. style: deliberate — list rows pack tighter
-/// than the pane gap, like every list on this app's pages (the inset from
-/// the region's edges is the well margin, `section_margin()`).
+/// than the pane gap, like every list on this app's pages (what stands
+/// inside a row is `list_gap()` apart and in from the region's edges).
const LIST_GAP: f32 = 4.0;
/// Rows shown before the region starts scrolling. The list sits ABOVE the
/// actions and the edit form, so it cannot fill the page the way services'
@@ -662,8 +663,6 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, sec_f
let item_h = state.list.item_height;
let rows_shown = state.accounts.len().min(LIST_MAX_ROWS);
let list_h = rows_shown as f32 * (item_h + LIST_GAP) + 8.0;
- // The list's own inset of its rows inside its frame.
- let m = crate::app::section_margin();
let list = &mut state.list;
let accounts = &state.accounts;
let keyring = &state.keyring;
@@ -674,7 +673,6 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, sec_f
list.set_rect(list_x, list_y, list_w, list_h);
list.update_bounds(accounts.len(), list_y, list_h);
list.push_prims(pc);
- let btn_w = list_w - 2.0 * m;
pc.push_clip_rect(list_x, list_y, list_w, list_h);
for (idx, acc) in accounts.iter().enumerate() {
// Same predicate the region virtualizes on — a row scrolled out
@@ -690,12 +688,13 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, sec_f
} else {
([1.0, 1.0, 1.0, 0.04], [1.0, 1.0, 1.0, 0.10])
};
+ let cell = lay_row(Rect { x: list_x, y: draw_y, width: list_w, height: item_h }, &[Cell::grow(item_h)])[0];
pc.button_left(
&acc.email,
- list_x + m,
- draw_y,
- btn_w,
- item_h,
+ cell.x,
+ cell.y,
+ cell.width,
+ cell.height,
bg,
hover,
TEXT_BTN,
@@ -717,12 +716,14 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, sec_f
let size = 12.0;
let ty = crate::app::label_y_in(draw_y, item_h, size, Some(&font));
let g = 11.0;
- let mut mx = list_x + m + cce_ui::layout::CONTROL_TEXT_INSET
+ // A list gap before each mark; a glyph and its word, half that.
+ let gap = cce_ui::layout::list_gap();
+ let mut mx = cell.x + cce_ui::layout::CONTROL_TEXT_INSET
+ crate::app::text_width(&acc.email, size, Some(&font));
for (icon, word, color) in marks {
- mx += 18.0;
+ mx += gap;
if pc.icon(icon, mx, draw_y + (item_h - g) / 2.0, g, g, *color) {
- mx += g + 4.0;
+ mx += g + gap / 2.0;
}
pc.text_with_font(word, mx, ty, size, TEXT_BTN, &font);
mx += crate::app::text_width(word, size, Some(&font));
diff --git a/src/pages/network.rs b/src/pages/network.rs
index 2ce21b2..27c0f15 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -1,7 +1,8 @@
use crate::app::{AppAction, PageContent};
use cce_ui::widget::Owned;
use cce_ui::widget::ScrollRegion;
-use cce_ui::layout::{PageLayoutBuilder, PageFlow, RenderTarget};
+use cce_ui::layout::{lay_row, Cell, PageLayoutBuilder, PageFlow, RenderTarget};
+use cce_ui::scene::layout::Rect;
use cce_ui::widget::{Adapted, Toggle};
#[derive(Debug, Clone)]
@@ -172,8 +173,6 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
// ── WiFi (label-less well) ──
builder.add_section_spanned(&mut final_pc, "", 1, root_focused, |sec| {
let sec_w = sec.cw;
- // The list's own inset of its rows inside its frame.
- let margin = sec.padding().max(12.0);
let mut form = sec.form();
let mut col = form.column();
@@ -219,7 +218,8 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
list.update_bounds(available.len(), r.y, r.height);
list.push_prims(pc);
- let btn_w = r.width - 2.0 * margin;
+ let row_h = list.item_height;
+ let btn_w = r.width - 2.0 * cce_ui::layout::list_gap();
let max_chars = ((btn_w / 6.5) as usize).saturating_sub(10).max(5);
pc.push_clip_rect(r.x, r.y, r.width, r.height);
@@ -232,8 +232,8 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
};
let label = format!("{} ({}%)", ssid_truncated, net.signal);
let active = net.in_use;
- let row_x = r.x + margin;
- let row_h = 26.0;
+ let cell = lay_row(Rect { x: r.x, y: draw_y, width: r.width, height: row_h }, &[Cell::grow(row_h)])[0];
+ let row_x = cell.x;
pc.button(&label, row_x, draw_y, btn_w, row_h,
if active { ACT_BTN } else { NET_BTN }, BTN_HOVER,
if active { ACCENT } else { TEXT_FG },
diff --git a/src/pages/packages.rs b/src/pages/packages.rs
index 8208c1b..a94b991 100644
--- a/src/pages/packages.rs
+++ b/src/pages/packages.rs
@@ -1,7 +1,8 @@
use crate::app::{button_need, form_button, form_divider, wrap_to_width, AppAction, PageContent};
use cce_ui::widget::Owned;
use cce_ui::widget::ScrollRegion;
-use cce_ui::layout::{render_widget, PageLayoutBuilder, PageFlow, RenderTarget};
+use cce_ui::layout::{lay_row, render_widget, Cell, PageLayoutBuilder, PageFlow, RenderTarget};
+use cce_ui::scene::layout::Rect;
use cce_ui::widget::{TextBox, InteractiveListItem};
#[derive(Debug, Clone, Default)]
@@ -872,7 +873,8 @@ pub fn view(
} else {
Some(&pkg.name) == selected.as_ref()
};
- render_widget(pc, item, list_box_x + 24.0, draw_y, list_box_w - 44.0, item_h, ctx);
+ let cell = lay_row(Rect { x: list_box_x, y: draw_y, width: list_box_w, height: item_h }, &[Cell::grow(item_h)])[0];
+ render_widget(pc, item, cell.x, cell.y, cell.width, cell.height, ctx);
}
}
pc.pop_clip_rect();
@@ -897,7 +899,8 @@ pub fn view(
item.title = pkg.name.clone();
item.subtitle = Some(format!("{} -> {}", pkg.old_version, pkg.new_version));
item.selected = Some(&pkg.name) == selected.as_ref();
- render_widget(pc, item, list_box_x + 24.0, draw_y, list_box_w - 44.0, item_h, ctx);
+ let cell = lay_row(Rect { x: list_box_x, y: draw_y, width: list_box_w, height: item_h }, &[Cell::grow(item_h)])[0];
+ render_widget(pc, item, cell.x, cell.y, cell.width, cell.height, ctx);
}
}
pc.pop_clip_rect();
diff --git a/src/pages/processes.rs b/src/pages/processes.rs
index 20c1199..c06a341 100644
--- a/src/pages/processes.rs
+++ b/src/pages/processes.rs
@@ -1,7 +1,8 @@
use crate::app::{AppAction, PageContent};
use crate::power_meter::{self, Meter, Mode};
use cce_ui::widget::ScrollRegion;
-use cce_ui::layout::{PageLayoutBuilder, PageFlow, RenderTarget};
+use cce_ui::layout::{lay_row, Cell, PageLayoutBuilder, PageFlow, RenderTarget};
+use cce_ui::scene::layout::Rect;
use std::sync::Mutex;
#[derive(Debug, Clone)]
@@ -247,6 +248,44 @@ const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
/// The least room the table keeps on a short window.
const LIST_MIN_H: f32 = 120.0;
+/// The table's header and row text sizes, and the sort chevron's.
+const HDR_SIZE: f32 = 11.0;
+const ROW_SIZE: f32 = 12.0;
+const CHEVRON: f32 = 9.0;
+/// The least room COMMAND keeps, in characters, before the table scrolls sideways.
+const COMMAND_MIN_CHARS: usize = 24;
+
+/// The table's columns in content space — PID, COMMAND, MEM, MEM %, CPU %, W, WAKE/s and
+/// Kill — and the content width they need. Each column is as wide as its header (and the
+/// sort chevron) or its widest likely value, whichever is wider; COMMAND takes the slack of
+/// a box wider than that, so a wide window shows the whole table and a narrow one scrolls
+/// it. They stand `list_gap` apart and in from the edges (`lay_row`).
+fn table_columns(box_w: f32, row_h: f32, kill_w: f32) -> ([Rect; 8], f32) {
+ let gap = cce_ui::layout::list_gap();
+ let w = |s: &str| crate::app::text_width(s, ROW_SIZE, None);
+ let head = |s: &str, sortable: bool| {
+ crate::app::text_width(s, HDR_SIZE, None) + if sortable { gap / 2.0 + CHEVRON } else { 0.0 }
+ };
+ let widths = [
+ w("4194304").max(head("PID", false)),
+ w(&"x".repeat(COMMAND_MIN_CHARS)),
+ w("999.9 GB").max(head("MEM", true)),
+ w("100.0%").max(head("MEM %", false)),
+ w("1000.0%").max(head("CPU %", true)),
+ w("99.9").max(head("W", true)),
+ w("99999").max(head("WAKE/s", true)),
+ kill_w,
+ ];
+ let needed = widths.iter().sum::<f32>() + gap * (widths.len() + 1) as f32;
+ let content_w = needed.max(box_w);
+ let cells: Vec<Cell> = widths
+ .iter()
+ .enumerate()
+ .map(|(i, &wd)| Cell { width: wd, height: row_h, grow: i == 1 })
+ .collect();
+ let rects = lay_row(Rect { x: 0.0, y: 0.0, width: content_w, height: row_h }, &cells);
+ (std::array::from_fn(|i| rects[i]), content_w)
+}
pub fn view(state: &mut ProcessesState, cx: f32, cy: f32, cw: f32, ch: f32, root_focused: bool, sec_focused: &[bool], layout: &mut PageFlow, ctx: &mut cce_ui::context::UiContext) -> PageContent {
let mut final_pc = PageContent::new();
@@ -263,7 +302,6 @@ pub fn view(state: &mut ProcessesState, cx: f32, cy: f32, cw: f32, ch: f32, root
}
// The table fills the page: the well's floor lands at the page's bottom.
form.fill_height((cy + ch) - form.top() - sec.bottom_inset());
- let inset = crate::app::section_margin();
let cpu_list = &mut state.cpu_list;
let processes = &state.processes;
let killing = &state.killing;
@@ -273,155 +311,132 @@ pub fn view(state: &mut ProcessesState, cx: f32, cy: f32, cw: f32, ch: f32, root
col.text(summary_line(&state.power), 11.0, TEXT_DIM);
col.fill(LIST_MIN_H, move |pc, r, _| {
let (list_box_x, list_box_y, list_box_w, list_box_h) = (r.x, r.y, r.width, r.height);
- // Dissolved List (Phase 6v): scroll state + frame prims are app-owned. The
- // scrollable viewport starts below the header.
- //
- // Columns live in CONTENT space at fixed offsets; every draw
- // subtracts scroll_x. CONTENT_W > box width = the h-bar appears.
- // Sized so the default 820px window shows the whole table without
- // the h-bar: COMMAND gives up the 20px the content box's inset
- // took.
- const COL_PID: f32 = 12.0;
- const COL_COMMAND: f32 = 80.0;
- const COL_RSS: f32 = 380.0;
- const COL_MEM: f32 = 460.0;
- const COL_CPU: f32 = 525.0;
- const COL_WATTS: f32 = 585.0;
- const COL_WAKE: f32 = 635.0;
- const COL_KILL: f32 = 700.0;
- const CONTENT_W: f32 = 725.0;
-
- // TODO(style): the column offsets, header hit-target nudges and
- // in-row text centring below are this table's own layout.
- let header_h = 22.0;
- cpu_list.set_rect(list_box_x, list_box_y, list_box_w, list_box_h);
- cpu_list.set_content_w(CONTENT_W);
- // No band for the horizontal scrollbar: it rides the rows'
- // centre line, crossing the vertical one, behind the plate until
- // a scroll raises it.
- let rows_h = list_box_h - header_h - 6.0;
- cpu_list.update_bounds(processes.len(), list_box_y + header_h, rows_h);
- cpu_list.push_prims(pc);
-
- let ox = cpu_list.scroll_x;
-
- // Header row is background-less (the well shows through); only the
- // divider separates it from the rows.
- pc.rect([0.18, 0.18, 0.24, 1.0], list_box_x + 1.0, list_box_y + header_h, list_box_w - 2.0, 1.0); // Divider
-
- // Header labels pan with the columns, clipped to the box. The
- // sortable ones (MEM, MEM %, CPU %) are buttons: the active key
- // shows brighter with a `chevron-down` glyph after it (the sort
- // is always descending). Both memory headers toggle the same
- // Mem sort — one bigger target, no distinction to learn.
- let active = |k: ProcSort| sort == k;
- let hdr = |on: bool| if on { [0.78, 0.78, 0.85, 1.0] } else { TEXT_DIM };
- const HDR_SIZE: f32 = 11.0;
- const HDR_Y: f32 = 5.0;
- const CHEVRON: f32 = 9.0;
- pc.push_clip_rect(list_box_x, list_box_y, list_box_w, header_h);
- let header = |pc: &mut PageContent, label: &str, col: f32, on: bool| {
- let x = list_box_x + col - ox;
- pc.text(label, x, list_box_y + HDR_Y, HDR_SIZE, hdr(on));
- if on {
- let gx = x + crate::app::text_width(label, HDR_SIZE, None) + 3.0;
- let gy = list_box_y + (header_h - CHEVRON) / 2.0;
- pc.icon("chevron-down", gx, gy, CHEVRON, CHEVRON, hdr(on));
- }
- };
- pc.text("PID", list_box_x + COL_PID - ox, list_box_y + HDR_Y, HDR_SIZE, TEXT_DIM);
- pc.text("COMMAND", list_box_x + COL_COMMAND - ox, list_box_y + HDR_Y, HDR_SIZE, TEXT_DIM);
- header(pc, "MEM", COL_RSS, active(ProcSort::Mem));
- pc.text("MEM %", list_box_x + COL_MEM - ox, list_box_y + HDR_Y, HDR_SIZE, hdr(active(ProcSort::Mem)));
- header(pc, "CPU %", COL_CPU, active(ProcSort::Cpu));
- header(pc, "W", COL_WATTS, active(ProcSort::Power));
- header(pc, "WAKE/s", COL_WAKE, active(ProcSort::Wakeups));
-
- // Header hit targets, list-row style like the rows (plateless,
- // washed on hover), inside the header clip so they pan and cut
- // with the labels. They share no rect with the row buttons, so
- // emission order is free here.
- pc.list_row(list_box_x + COL_RSS - ox - 4.0, list_box_y, 52.0, header_h - 2.0,
- AppAction::Processes(ProcessesMessage::SortBy(ProcSort::Mem)));
- pc.list_row(list_box_x + COL_MEM - ox - 4.0, list_box_y, 58.0, header_h - 2.0,
- AppAction::Processes(ProcessesMessage::SortBy(ProcSort::Mem)));
- pc.list_row(list_box_x + COL_CPU - ox - 4.0, list_box_y, 58.0, header_h - 2.0,
- AppAction::Processes(ProcessesMessage::SortBy(ProcSort::Cpu)));
- pc.list_row(list_box_x + COL_WATTS - ox - 4.0, list_box_y, 48.0, header_h - 2.0,
- AppAction::Processes(ProcessesMessage::SortBy(ProcSort::Power)));
- pc.list_row(list_box_x + COL_WAKE - ox - 4.0, list_box_y, 62.0, header_h - 2.0,
- AppAction::Processes(ProcessesMessage::SortBy(ProcSort::Wakeups)));
- pc.pop_clip_rect();
-
- let row_h = 24.0;
-
- // Visible process rows, clipped to the rows' viewport — not on
- // into the reserve band below it, where the scroll region still
- // hands back a row starting there and its Kill button stood alone
- // under the list.
- pc.push_clip_rect(list_box_x, list_box_y + header_h, list_box_w, rows_h);
- for (idx, p) in processes.iter().enumerate() {
- if let Some(draw_y) = cpu_list.get_item_draw_y(idx, 4.0) {
- // The row itself, in the DE's list style: plateless,
- // washed on hover. A transparent plain button here wore a
- // control plate per row — the whole list read as a stack
- // of carved rings. Viewport-fixed on purpose: the hover
- // band spans the visible row whatever the horizontal pan.
- pc.list_row(
- list_box_x + 2.0,
- draw_y,
- list_box_w - 16.0,
- row_h,
- AppAction::Processes(ProcessesMessage::None),
+ let gap = cce_ui::layout::list_gap();
+ // A row, and the header, are as tall as the region virtualizes rows at.
+ let row_h = cpu_list.item_height;
+ let header_h = row_h;
+ let kill_h = cce_ui::layout::button_height();
+
+ // Dissolved List (Phase 6v): scroll state + frame prims are app-owned. The
+ // scrollable viewport starts below the header. Columns live in CONTENT space;
+ // every draw subtracts scroll_x, and a table wider than the box scrolls.
+ let (cols, content_w) = table_columns(list_box_w, row_h, kill_h);
+ let [c_pid, c_cmd, c_rss, c_mem, c_cpu, c_watts, c_wake, c_kill] = cols;
+ cpu_list.set_rect(list_box_x, list_box_y, list_box_w, list_box_h);
+ cpu_list.set_content_w(content_w);
+ // No band for the horizontal scrollbar: it rides the rows'
+ // centre line, crossing the vertical one, behind the plate until
+ // a scroll raises it.
+ let rows_h = list_box_h - header_h - gap;
+ cpu_list.update_bounds(processes.len(), list_box_y + header_h, rows_h);
+ cpu_list.push_prims(pc);
+
+ let ox = cpu_list.scroll_x;
+ let at = |c: Rect| list_box_x + c.x - ox;
+
+ // Header row is background-less (the well shows through); only the
+ // divider separates it from the rows.
+ pc.rect([0.18, 0.18, 0.24, 1.0], list_box_x, list_box_y + header_h, list_box_w, 1.0);
+
+ // Header labels pan with the columns, clipped to the box. The
+ // sortable ones (MEM, MEM %, CPU %, W, WAKE/s) are hit targets the
+ // width of their column: the active key shows brighter with a
+ // `chevron-down` glyph after it (the sort is always descending).
+ // Both memory headers toggle the same Mem sort — one bigger
+ // target, no distinction to learn.
+ let hdr = |on: bool| if on { [0.78, 0.78, 0.85, 1.0] } else { TEXT_DIM };
+ let hdr_y = crate::app::label_y_in(list_box_y, header_h, HDR_SIZE, None);
+ pc.push_clip_rect(list_box_x, list_box_y, list_box_w, header_h);
+ let header = |pc: &mut PageContent, label: &str, c: Rect, on: bool, chevron: bool| {
+ let x = at(c);
+ pc.text(label, x, hdr_y, HDR_SIZE, hdr(on));
+ if on && chevron {
+ let gx = x + crate::app::text_width(label, HDR_SIZE, None) + gap / 2.0;
+ let gy = list_box_y + (header_h - CHEVRON) / 2.0;
+ pc.icon("chevron-down", gx, gy, CHEVRON, CHEVRON, hdr(on));
+ }
+ };
+ header(pc, "PID", c_pid, false, false);
+ header(pc, "COMMAND", c_cmd, false, false);
+ header(pc, "MEM", c_rss, sort == ProcSort::Mem, true);
+ header(pc, "MEM %", c_mem, sort == ProcSort::Mem, false);
+ header(pc, "CPU %", c_cpu, sort == ProcSort::Cpu, true);
+ header(pc, "W", c_watts, sort == ProcSort::Power, true);
+ header(pc, "WAKE/s", c_wake, sort == ProcSort::Wakeups, true);
+ // The hit targets, list-row style like the rows (plateless, washed on
+ // hover), inside the header clip so they pan and cut with the labels.
+ // They share no rect with the row buttons, so emission order is free.
+ for (c, key) in [(c_rss, ProcSort::Mem), (c_mem, ProcSort::Mem), (c_cpu, ProcSort::Cpu), (c_watts, ProcSort::Power), (c_wake, ProcSort::Wakeups)] {
+ pc.list_row(at(c), list_box_y, c.width, header_h, AppAction::Processes(ProcessesMessage::SortBy(key)));
+ }
+ pc.pop_clip_rect();
+
+ // Visible process rows, clipped to the rows' viewport — not on
+ // into the reserve band below it, where the scroll region still
+ // hands back a row starting there and its Kill button stood alone
+ // under the list.
+ pc.push_clip_rect(list_box_x, list_box_y + header_h, list_box_w, rows_h);
+ for (idx, p) in processes.iter().enumerate() {
+ if let Some(draw_y) = cpu_list.get_item_draw_y(idx, 4.0) {
+ // The row itself, in the DE's list style: plateless,
+ // washed on hover. A transparent plain button here wore a
+ // control plate per row — the whole list read as a stack
+ // of carved rings. Viewport-fixed on purpose: the hover
+ // band spans the visible row whatever the horizontal pan.
+ let wash = lay_row(Rect { x: list_box_x, y: draw_y, width: list_box_w, height: row_h }, &[Cell::grow(row_h)])[0];
+ pc.list_row(wash.x, wash.y, wash.width, wash.height, AppAction::Processes(ProcessesMessage::None));
+
+ // A pending kill dims the row until the next refresh
+ // settles it (gone, or alive again = the kill didn't take).
+ let dim = killing.contains(&p.pid);
+ let fg = if dim { [0.45, 0.45, 0.50, 1.0] } else { [0.80, 0.80, 0.85, 1.0] };
+ let mem_fg = if dim { [0.45, 0.45, 0.50, 1.0] } else { [0.62, 0.72, 0.88, 1.0] };
+ let cpu_fg = if dim { [0.45, 0.45, 0.50, 1.0] } else { [0.56, 0.83, 0.56, 1.0] };
+ let watt_fg = if dim { [0.45, 0.45, 0.50, 1.0] } else { [0.90, 0.75, 0.45, 1.0] };
+
+ let ty = crate::app::label_y_in(draw_y, row_h, ROW_SIZE, None);
+ let cell = |pc: &mut PageContent, text: &str, c: Rect, color: [f32; 4]| {
+ let x = at(c);
+ pc.text_with_bounds(text, x, ty, ROW_SIZE, color, Some([x, ty - ROW_SIZE, x + c.width, ty + 2.0 * ROW_SIZE]));
+ };
+ cell(pc, &p.pid, c_pid, fg);
+ cell(pc, &p.command, c_cmd, fg);
+ cell(pc, &format_rss(p.rss_kb), c_rss, mem_fg);
+ cell(pc, &format!("{}%", p.mem_pct), c_mem, mem_fg);
+ cell(pc, &format!("{}%", p.cpu), c_cpu, cpu_fg);
+ cell(pc, &format_watts(p.watts), c_watts, watt_fg);
+ cell(pc, &format_wakeups(p.wakeups), c_wake, fg);
+
+ // Kill button, in content space like the columns. Emitted
+ // AFTER the row button on purpose: overlapping page
+ // buttons all see the click and the LAST take_click wins
+ // the dispatched action (input_handler's collect loop), so
+ // the Kill button beats the row's no-op exactly because it comes later.
+ if !dim {
+ // The `x` glyph in the danger tint; "Kill" only if
+ // the icon set is missing.
+ pc.button_icon_tinted(
+ "x",
+ "Kill",
+ at(c_kill),
+ draw_y + (row_h - kill_h) / 2.0,
+ c_kill.width,
+ kill_h,
+ [0.0, 0.0, 0.0, 0.0],
+ [0.75, 0.30, 0.30, 0.45],
+ [0.85, 0.55, 0.55, 1.0],
+ AppAction::Processes(ProcessesMessage::Kill(p.pid.clone())),
);
-
- // A pending kill dims the row until the next refresh
- // settles it (gone, or alive again = the kill didn't take).
- let dim = killing.contains(&p.pid);
- let fg = if dim { [0.45, 0.45, 0.50, 1.0] } else { [0.80, 0.80, 0.85, 1.0] };
- let mem_fg = if dim { [0.45, 0.45, 0.50, 1.0] } else { [0.62, 0.72, 0.88, 1.0] };
- let cpu_fg = if dim { [0.45, 0.45, 0.50, 1.0] } else { [0.56, 0.83, 0.56, 1.0] };
- let watt_fg = if dim { [0.45, 0.45, 0.50, 1.0] } else { [0.90, 0.75, 0.45, 1.0] };
-
- pc.text(&p.pid, list_box_x + COL_PID - ox, draw_y + 6.0, 12.0, fg);
- pc.text(&p.command, list_box_x + COL_COMMAND - ox, draw_y + 6.0, 12.0, fg);
- pc.text(&format_rss(p.rss_kb), list_box_x + COL_RSS - ox, draw_y + 6.0, 12.0, mem_fg);
- pc.text(&format!("{}%", p.mem_pct), list_box_x + COL_MEM - ox, draw_y + 6.0, 12.0, mem_fg);
- pc.text(&format!("{}%", p.cpu), list_box_x + COL_CPU - ox, draw_y + 6.0, 12.0, cpu_fg);
- pc.text(&format_watts(p.watts), list_box_x + COL_WATTS - ox, draw_y + 6.0, 12.0, watt_fg);
- pc.text(&format_wakeups(p.wakeups), list_box_x + COL_WAKE - ox, draw_y + 6.0, 12.0, fg);
-
- // Kill button, in content space like the columns. Emitted
- // AFTER the row button on purpose: overlapping page
- // buttons all see the click and the LAST take_click wins
- // the dispatched action (input_handler's collect loop), so
- // the Kill button beats the row's no-op exactly because it comes later.
- if !dim {
- // The `x` glyph in the danger tint; "Kill" only if
- // the icon set is missing.
- let kill_h = cce_ui::layout::button_height();
- pc.button_icon_tinted(
- "x",
- "Kill",
- list_box_x + COL_KILL - ox,
- draw_y + (row_h - kill_h) / 2.0,
- kill_h,
- kill_h,
- [0.0, 0.0, 0.0, 0.0],
- [0.75, 0.30, 0.30, 0.45],
- [0.85, 0.55, 0.55, 1.0],
- AppAction::Processes(ProcessesMessage::Kill(p.pid.clone())),
- );
- }
}
}
- pc.pop_clip_rect();
- // The scrollbar's fore copy, over the rows at the raise's fade.
- cpu_list.push_scrollbar_fore(pc);
-
- if processes.is_empty() {
- pc.text("No active processes", list_box_x + inset, list_box_y + header_h + 16.0, 12.0, TEXT_DIM);
- }
+ }
+ pc.pop_clip_rect();
+ // The scrollbar's fore copy, over the rows at the raise's fade.
+ cpu_list.push_scrollbar_fore(pc);
+
+ if processes.is_empty() {
+ pc.text("No active processes", list_box_x + gap, list_box_y + header_h + gap, 12.0, TEXT_DIM);
+ }
});
sec.place(form, ctx);
});
diff --git a/src/pages/services.rs b/src/pages/services.rs
index 328a02a..22fcfe7 100644
--- a/src/pages/services.rs
+++ b/src/pages/services.rs
@@ -1,7 +1,8 @@
use crate::app::{form_button, AppAction, PageContent};
use cce_ui::widget::Owned;
use cce_ui::widget::ScrollRegion;
-use cce_ui::layout::{render_widget, PageLayoutBuilder, PageFlow, RenderTarget};
+use cce_ui::layout::{lay_row, render_widget, Cell, PageLayoutBuilder, PageFlow, RenderTarget};
+use cce_ui::scene::layout::Rect;
use cce_ui::widget::{TextBox, InteractiveListItem};
#[derive(Debug, Clone)]
@@ -135,26 +136,24 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, _root
// "sta". `upload_icon` caches per (name, px), so asking
// every row costs one hash lookup.
let icons_ok = cce_ui::upload_icon("play", 32).is_some();
- let is_small = sec_w < 350.0;
let btn_h = cce_ui::layout::button_height();
- let (btn_w, r_btn_w) = if icons_ok { (btn_h, btn_h) } else { (46.0, 54.0) };
- let btn_gap = if is_small { 4.0 } else { 6.0 };
-
- // TODO(style): the row's control run and text column
- // below are this list row's own layout.
- let toggle_x = list_box_x + 10.0;
- let restart_x = toggle_x + btn_w + btn_gap;
- // The row's text starts after the controls and ends the
- // controls' inset short of the right wall: no gutter for
- // the scrollbar, which rides the list's centre line.
- let item_x = restart_x + r_btn_w + 10.0;
- let item_w = (list_box_x + list_box_w - 10.0) - item_x;
-
- let btn_y = draw_y + (item_h - btn_h) / 2.0;
+ // Without the icon set the controls are word buttons, as wide as
+ // their words.
+ let (btn_w, r_btn_w) = if icons_ok {
+ (btn_h, btn_h)
+ } else {
+ (crate::app::button_need("Start").max(crate::app::button_need("Stop")), crate::app::button_need("Restart"))
+ };
+ // The controls, then the name and description to the row's end
+ // (no gutter for the scrollbar, which rides the list's centre line).
+ let row = Rect { x: list_box_x, y: draw_y, width: list_box_w, height: item_h };
+ let cells = lay_row(row, &[Cell::fixed(btn_w, btn_h), Cell::fixed(r_btn_w, btn_h), Cell::grow(item_h)]);
+ let (toggle_x, restart_x, btn_y) = (cells[0].x, cells[1].x, cells[0].y);
+ let (item_x, item_w) = (cells[2].x, cells[2].width);
// Service description, truncated to the room the row's text
- // column actually has (the item insets its labels by 8px).
- let text_max_w = item_w - 16.0;
+ // column actually has (the item insets its labels from its walls).
+ let text_max_w = item_w - 2.0 * cce_ui::layout::CONTROL_TEXT_INSET;
let max_chars = ((text_max_w / 6.0) as usize).max(10);
// `failed` was the status dot's third state and has nowhere
// else to show: a failed unit offers the same play button a
diff --git a/src/pages/timers.rs b/src/pages/timers.rs
index b91fa50..211929f 100644
--- a/src/pages/timers.rs
+++ b/src/pages/timers.rs
@@ -5,7 +5,8 @@
use crate::app::{button_need, form_button, AppAction, PageContent};
use cce_ui::widget::Owned;
use cce_ui::widget::ScrollRegion;
-use cce_ui::layout::{render_widget, PageLayoutBuilder, PageFlow, RenderTarget};
+use cce_ui::layout::{lay_row, render_widget, Cell, PageLayoutBuilder, PageFlow, RenderTarget};
+use cce_ui::scene::layout::Rect;
use cce_ui::widget::{StatusDot, DotStatus, InteractiveListItem, TextBox};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -84,6 +85,8 @@ pub enum TimersMessage {
const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
/// The least room the list keeps on a short window.
const LIST_MIN_H: f32 = 120.0;
+/// A row's status dot: a small LED, not a control.
+const DOT: f32 = 10.0;
/// "46min" / "3h" / "5d 3h" — coarse two-unit humanization.
fn humanize(secs: u64) -> String {
@@ -501,23 +504,31 @@ pub fn view(state: &mut TimersState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
let compact = is_small && cce_ui::upload_icon("play", 32).is_some();
// Glyph buttons are square at the control height.
let btn_h = cce_ui::layout::button_height();
- let run_w = if compact { btn_h } else { 76.0 };
- let en_w = if compact { btn_h } else { 66.0 };
- let edit_w = if compact { btn_h } else { 50.0 };
- let btn_gap = if is_small { 4.0 } else { 6.0 };
- // TODO(style): the row's button run, dot and text
- // column below are this list row's own layout.
- let right_edge = list_box_x + list_box_w - 24.0 - 8.0;
-
- let en_x = right_edge - en_w;
- let run_x = en_x - btn_gap - run_w;
- let edit_x = run_x - btn_gap - edit_w;
-
- let btn_y = draw_y + (item_h - btn_h) / 2.0;
-
- // Title + schedule subtitle (truncated to the space before the buttons).
- let text_left_edge = if timer.editable { edit_x } else { run_x };
- let text_max_w = (text_left_edge - 8.0) - (list_box_x + 32.0);
+ // Word buttons are as wide as their words.
+ let run_label = if is_small { "Run" } else { "Run Now" };
+ let word_w = |words: &[&str]| words.iter().map(|w| crate::app::button_need(w)).fold(0.0, f32::max);
+ let run_w = if compact { btn_h } else { word_w(&[run_label]) };
+ let en_w = if compact { btn_h } else { word_w(&["Disable", "Enable", "static"]) };
+ let edit_w = if compact { btn_h } else { word_w(&["Edit"]) };
+
+ // The status dot, the name and schedule, then the buttons at the
+ // row's end (Edit only for an editable unit).
+ let row = Rect { x: list_box_x, y: draw_y, width: list_box_w, height: item_h };
+ let mut layout = vec![Cell::fixed(DOT, DOT), Cell::grow(item_h)];
+ if timer.editable {
+ layout.push(Cell::fixed(edit_w, btn_h));
+ }
+ layout.push(Cell::fixed(run_w, btn_h));
+ layout.push(Cell::fixed(en_w, btn_h));
+ let cells = lay_row(row, &layout);
+ let (dot_rect, item_rect) = (cells[0], cells[1]);
+ let en_x = cells[cells.len() - 1].x;
+ let run_x = cells[cells.len() - 2].x;
+ let edit_x = if timer.editable { cells[2].x } else { run_x };
+ let btn_y = cells[cells.len() - 1].y;
+
+ // Title + schedule subtitle, truncated to the item's text room.
+ let text_max_w = item_rect.width - 2.0 * cce_ui::layout::CONTROL_TEXT_INSET;
let max_chars = ((text_max_w / 6.0) as usize).max(10);
let subtitle_full = schedule_line(timer, now);
let subtitle = if subtitle_full.len() > max_chars {
@@ -529,11 +540,14 @@ pub fn view(state: &mut TimersState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
let item_btn = &mut items[idx];
item_btn.title = timer.unit.clone();
item_btn.subtitle = Some(subtitle);
- render_widget(pc, item_btn, list_box_x + 24.0, draw_y, list_box_w - 44.0, item_h, ctx);
+ // Cut at its cell, so a long schedule stops short of the buttons.
+ pc.push_clip_rect(item_rect.x, item_rect.y, item_rect.width, item_rect.height);
+ render_widget(pc, item_btn, item_rect.x, item_rect.y, item_rect.width, item_rect.height, ctx);
+ pc.pop_clip_rect();
let dot_state = if timer.active { DotStatus::Active } else { DotStatus::Inactive };
let mut dot = Owned::new(StatusDot::new(dot_state));
- render_widget(pc, &mut dot, list_box_x + 10.0, draw_y + (item_h - 10.0) / 2.0, 10.0, 10.0, ctx);
+ render_widget(pc, &mut dot, dot_rect.x, dot_rect.y, dot_rect.width, dot_rect.height, ctx);
let active_txt = [0.90, 0.90, 0.95, 1.0];
@@ -610,7 +624,7 @@ pub fn view(state: &mut TimersState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
);
}
_ => {
- pc.text("static", en_x + 8.0, btn_y + (btn_h - 12.0) / 2.0, 11.0, TEXT_DIM);
+ pc.text("static", en_x + cce_ui::layout::CONTROL_TEXT_INSET, crate::app::label_y_in(btn_y, btn_h, 11.0, None), 11.0, TEXT_DIM);
}
}
}