system settings
git clone https://git.lucas.co/cce-system-interface.git
fix(controls): every button, tab and toggle at the toolkit's control height
Pages hand-set 20/22/26/28/32px buttons, 28px toggles and a 30px search
box; the toolkit gives every text-bearing control one height
(button_height / toggle_height / textbox_height, 24px by default and
configurable). Tab strips, form and action buttons, list-row actions and
the bluetooth/wifi toggles now ask for it, and glyph-only row buttons
(services, timers, processes' kill) are square at that height.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/pages/accounts.rs | 2 +-
src/pages/audio.rs | 7 ++++---
src/pages/bluetooth.rs | 9 +++++----
src/pages/browser.rs | 2 +-
src/pages/network.rs | 5 +++--
src/pages/notifications.rs | 2 +-
src/pages/packages.rs | 14 ++++++++------
src/pages/processes.rs | 7 ++++---
src/pages/services.rs | 10 +++++-----
src/pages/storage.rs | 2 +-
src/pages/timers.rs | 25 +++++++++++++------------
src/renderer.rs | 6 ++++--
12 files changed, 50 insertions(+), 41 deletions(-)
diff --git a/src/pages/accounts.rs b/src/pages/accounts.rs
index cb8f662..2cec31a 100644
--- a/src/pages/accounts.rs
+++ b/src/pages/accounts.rs
@@ -631,7 +631,7 @@ pub fn view(state: &mut AccountsState, cx: f32, cy: f32, cw: f32, ch: f32, sec_f
let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(1);
let widget_h = cce_ui::layout::spinbox_height();
- let btn_h = 26.0;
+ let btn_h = cce_ui::layout::button_height();
let m = crate::app::section_margin();
let gap = cce_ui::layout::plate_gap();
diff --git a/src/pages/audio.rs b/src/pages/audio.rs
index fe47b0a..a3dae0d 100644
--- a/src/pages/audio.rs
+++ b/src/pages/audio.rs
@@ -258,7 +258,8 @@ fn device_row(
let sb_h = cce_ui::layout::spinbox_height();
let sl_h = cce_ui::layout::slider_height();
- let row_h = sb_h.max(sl_h).max(22.0);
+ let btn_h = cce_ui::layout::button_height();
+ let row_h = sb_h.max(sl_h).max(btn_h);
let (mute_label, colors, mute_text) = if muted {
("Unmute", BTN_DANGER, TEXT_DANGER)
} else {
@@ -281,9 +282,9 @@ fn device_row(
c.button(
mute_label,
spin_x + spin_w + gap,
- y + (row_h - 22.0) / 2.0,
+ y + (row_h - btn_h) / 2.0,
mute_w,
- 22.0,
+ btn_h,
colors.0,
colors.1,
mute_text,
diff --git a/src/pages/bluetooth.rs b/src/pages/bluetooth.rs
index 4d2e772..933db27 100644
--- a/src/pages/bluetooth.rs
+++ b/src/pages/bluetooth.rs
@@ -163,7 +163,7 @@ pub fn view(state: &mut BluetoothState, cx: f32, cy: f32, cw: f32, ch: f32, sec_
let row_gap = cce_ui::layout::label_margin();
let margin = padding.max(12.0);
let font_size = 12.0;
- let btn_h = 28.0;
+ let btn_h = cce_ui::layout::button_height();
if !state.loaded {
sec.text("Loading Bluetooth status...", margin, 0.0, font_size, TEXT_DIM);
@@ -194,11 +194,12 @@ pub fn view(state: &mut BluetoothState, cx: f32, cy: f32, cw: f32, ch: f32, sec_
// Hand-placed: sec.widget grid-places at full column width, which
// would sit the toggle under the Scan button.
let tx = sec.ax(margin);
- render_widget(sec.pc, &mut state.toggle, tx, yt, bt_btn_w, btn_h, ctx);
+ let toggle_h = cce_ui::layout::toggle_height();
+ render_widget(sec.pc, &mut state.toggle, tx, yt, bt_btn_w, toggle_h, ctx);
sec.button("Scan", sec.ax(scan_btn_x), yt, scan_btn_w, btn_h,
TOGGLE_OFF, BTN_HOVER, WHITE,
AppAction::Bluetooth(BluetoothMessage::Scan));
- sec.content_y = yt + btn_h + row_gap;
+ sec.content_y = yt + btn_h.max(toggle_h) + row_gap;
if state.devices.is_empty() {
if state.enabled {
@@ -206,7 +207,7 @@ pub fn view(state: &mut BluetoothState, cx: f32, cy: f32, cw: f32, ch: f32, sec_
sec.text(no_devices_msg, margin, 0.0, font_size, TEXT_DIM);
}
} else {
- let item_h = 22.0;
+ let item_h = btn_h;
for dev in &state.devices {
let btn_w = if bt_sec_w < 250.0 { 42.0 } else { 70.0 };
let action_label = if dev.connected {
diff --git a/src/pages/browser.rs b/src/pages/browser.rs
index a5ddf66..affa241 100644
--- a/src/pages/browser.rs
+++ b/src/pages/browser.rs
@@ -314,7 +314,7 @@ impl AppPage for BrowserState {
self.vi_mode_toggle.set_toggled(self.vi_mode);
stack.add_widget(&mut self.vi_mode_toggle, row_w, cce_ui::layout::toggle_height(), ctx);
- let btn_h = 32.0;
+ let btn_h = cce_ui::layout::button_height();
stack.add_row(1, 0.0, btn_h, |ctx, _, x, w| {
ctx.button(
"Apply",
diff --git a/src/pages/network.rs b/src/pages/network.rs
index 1a73156..59c5266 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -184,8 +184,9 @@ pub fn view(state: &mut NetworkState, cx: f32, cy: f32, cw: f32, ch: f32, root_f
// column width (the old wide "ON" plate).
let yt = sec.ay();
let tx = sec.ax(margin);
- cce_ui::layout::render_widget(sec.pc, &mut state.wifi_toggle, tx, yt, wifi_btn_w, 28.0, ctx);
- sec.content_y = yt + 28.0 + row_gap;
+ let toggle_h = cce_ui::layout::toggle_height();
+ cce_ui::layout::render_widget(sec.pc, &mut state.wifi_toggle, tx, yt, wifi_btn_w, toggle_h, ctx);
+ sec.content_y = yt + toggle_h + row_gap;
if state.wifi_enabled {
// Flowing text rows — sec.text advances content_y itself.
diff --git a/src/pages/notifications.rs b/src/pages/notifications.rs
index 1e1dfa0..72fd50a 100644
--- a/src/pages/notifications.rs
+++ b/src/pages/notifications.rs
@@ -233,7 +233,7 @@ impl AppPage for NotificationsState {
self.duration_spinbox.set_row_rect(stack.context.left + 14.0, sec_w - 28.0);
stack.add_widget(&mut self.duration_spinbox, sec_w - 28.0, 44.0, ctx);
- let btn_h = 32.0;
+ let btn_h = cce_ui::layout::button_height();
let white_color = [1.0, 1.0, 1.0, 1.0];
let btn_bg = [0.20, 0.40, 0.65, 1.0];
let btn_hover = [0.28, 0.50, 0.78, 1.0];
diff --git a/src/pages/packages.rs b/src/pages/packages.rs
index 3be9936..eabb074 100644
--- a/src/pages/packages.rs
+++ b/src/pages/packages.rs
@@ -339,7 +339,7 @@ pub fn view(
// ── Tabs (with counts), filter, compact update row ──
let mut stack = sec.vstack(cce_ui::layout::plate_gap());
- let tab_h = 28.0;
+ let tab_h = cce_ui::layout::button_height();
let active_bg = [0.20, 0.40, 0.65, 0.4];
let inactive_bg = [0.10, 0.10, 0.16, 0.3];
let hover_bg = [0.20, 0.20, 0.25, 0.15];
@@ -397,13 +397,13 @@ pub fn view(
} else {
("Update System", [0.13, 0.18, 0.14, 1.0], [0.25, 0.30, 0.26, 1.0])
};
- stack.add_row(3, cce_ui::layout::plate_gap(), 26.0, |c, i, x, w| {
+ stack.add_row(3, cce_ui::layout::plate_gap(), tab_h, |c, i, x, w| {
if i == 0 {
- c.button(btn_lbl, x, c.ay(), w, 26.0, bg, hover, [0.90, 0.90, 0.95, 1.0],
+ c.button(btn_lbl, x, c.ay(), w, tab_h, bg, hover, [0.90, 0.90, 0.95, 1.0],
AppAction::Packages(PackagesMessage::StartUpdate));
} else if i == 1 {
let y = c.ay();
- c.pc.text(&status_line, x, y + 6.0, 12.0, status_color);
+ c.pc.text(&status_line, x, y + (tab_h - 14.0) / 2.0, 12.0, status_color);
}
});
@@ -452,7 +452,9 @@ pub fn view(
} else {
("Uninstall", [0.25, 0.14, 0.14, 1.0], [0.40, 0.20, 0.20, 1.0], [0.95, 0.55, 0.55, 1.0])
};
- sc.button(btn_lbl, bx, y - 2.0, bw, 22.0, bg, hover, text_col,
+ // Centred on the 14px name's line.
+ let bh = cce_ui::layout::button_height();
+ sc.button(btn_lbl, bx, y + (17.0 - bh) / 2.0, bw, bh, bg, hover, text_col,
AppAction::Packages(PackagesMessage::StartUninstall(pkg_name.clone())));
}
}
@@ -497,7 +499,7 @@ pub fn view(
let reqs: Vec<&str> = parsed.required_by.split_whitespace().collect();
let cols_count = 4;
let gap = 4.0;
- let btn_h = 20.0;
+ let btn_h = cce_ui::layout::button_height();
for chunk in reqs.chunks(cols_count) {
let btn_y = stack.context.ay();
diff --git a/src/pages/processes.rs b/src/pages/processes.rs
index cb56bee..337523f 100644
--- a/src/pages/processes.rs
+++ b/src/pages/processes.rs
@@ -398,13 +398,14 @@ pub fn view(state: &mut ProcessesState, cx: f32, cy: f32, cw: f32, ch: f32, root
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();
sec.pc.button_icon_tinted(
"x",
"Kill",
list_box_x + COL_KILL - ox,
- draw_y + 3.0,
- 20.0,
- row_h - 6.0,
+ 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],
diff --git a/src/pages/services.rs b/src/pages/services.rs
index cf321dd..3db63c6 100644
--- a/src/pages/services.rs
+++ b/src/pages/services.rs
@@ -71,7 +71,7 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, _root
} else {
// Tab header buttons: System Services, User Services
let mut stack = sec.vstack(cce_ui::layout::plate_gap());
- let tab_h = 28.0;
+ let tab_h = cce_ui::layout::button_height();
let active_bg = [0.20, 0.40, 0.65, 0.4];
let inactive_bg = [0.10, 0.10, 0.16, 0.3];
let hover_bg = [0.20, 0.20, 0.25, 0.15];
@@ -163,13 +163,14 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, _root
//
// Sized on whether the icon set is actually THERE:
// `button_icon` falls back to the labels when it isn't, and
- // a 24px button doesn't clip a label so much as replace it
+ // a square button doesn't clip a label so much as replace it
// — the text centers, so both ends cut and "Restart" reads
// "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_w, r_btn_w) = if icons_ok { (24.0, 24.0) } else { (46.0, 54.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
@@ -182,8 +183,7 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, _root
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 - 22.0) / 2.0;
- let btn_h = 22.0;
+ let btn_y = draw_y + (item_h - btn_h) / 2.0;
// Service description, truncated to the room the row's text
// column actually has (the item insets its labels by 8px).
diff --git a/src/pages/storage.rs b/src/pages/storage.rs
index d091010..7527c3b 100644
--- a/src/pages/storage.rs
+++ b/src/pages/storage.rs
@@ -317,7 +317,7 @@ pub fn view(state: &mut StorageState, cx: f32, cy: f32, cw: f32, ch: f32, sec_fo
// Action Button
let mut stack = sec.vstack(cce_ui::layout::plate_gap());
- let btn_h = 32.0;
+ let btn_h = cce_ui::layout::button_height();
// Retained widget rather than an immediate `sec.button`, so it can hold
// keyboard focus. Its label/colours are re-synced each frame from the
diff --git a/src/pages/timers.rs b/src/pages/timers.rs
index eac299c..aaa2c11 100644
--- a/src/pages/timers.rs
+++ b/src/pages/timers.rs
@@ -413,7 +413,7 @@ pub fn view(state: &mut TimersState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
} else {
// Tab header buttons: System Timers, User Timers
let mut stack = sec.vstack(cce_ui::layout::plate_gap());
- let tab_h = 28.0;
+ let tab_h = cce_ui::layout::button_height();
let active_bg = [0.20, 0.40, 0.65, 0.4];
let inactive_bg = [0.10, 0.10, 0.16, 0.3];
let hover_bg = [0.20, 0.20, 0.25, 0.15];
@@ -453,9 +453,9 @@ pub fn view(state: &mut TimersState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
// New Timer (user scope) — compact button; the form unfolds below.
let new_bg = if state.creating { active_bg } else { [0.13, 0.18, 0.14, 1.0] };
- stack.add_row(3, cce_ui::layout::plate_gap(), 26.0, |c, i, x, w| {
+ stack.add_row(3, cce_ui::layout::plate_gap(), tab_h, |c, i, x, w| {
if i == 0 {
- c.button("New Timer", x, c.ay(), w, 26.0,
+ c.button("New Timer", x, c.ay(), w, tab_h,
new_bg, [0.25, 0.30, 0.26, 1.0], [0.90, 0.90, 0.95, 1.0],
crate::app::AppAction::Timers(TimersMessage::CreateStart));
}
@@ -483,12 +483,12 @@ pub fn view(state: &mut TimersState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
stack.context.spacing(4.0);
let save_label = if state.editing.is_some() { "Save" } else { "Create" };
- stack.add_row(3, cce_ui::layout::plate_gap(), 26.0, |c, i, x, w| {
+ stack.add_row(3, cce_ui::layout::plate_gap(), tab_h, |c, i, x, w| {
match i {
- 0 => c.button(save_label, x, c.ay(), w, 26.0,
+ 0 => c.button(save_label, x, c.ay(), w, tab_h,
[0.13, 0.18, 0.14, 1.0], [0.25, 0.30, 0.26, 1.0], [0.90, 0.90, 0.95, 1.0],
crate::app::AppAction::Timers(TimersMessage::CreateSave)),
- 1 => c.button("Cancel", x, c.ay(), w, 26.0,
+ 1 => c.button("Cancel", x, c.ay(), w, tab_h,
[0.15, 0.15, 0.20, 1.0], [0.22, 0.22, 0.28, 1.0], [0.90, 0.90, 0.95, 1.0],
crate::app::AppAction::Timers(TimersMessage::CreateCancel)),
_ => {}
@@ -538,9 +538,11 @@ pub fn view(state: &mut TimersState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
// needs the wide button. `upload_icon` caches per
// (name, px), so asking every row is one hash lookup.
let compact = is_small && cce_ui::upload_icon("play", 32).is_some();
- let run_w = if compact { 40.0 } else { 76.0 };
- let en_w = if compact { 40.0 } else { 66.0 };
- let edit_w = if compact { 36.0 } else { 50.0 };
+ // 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.
@@ -550,8 +552,7 @@ pub fn view(state: &mut TimersState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
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 - 22.0) / 2.0;
- let btn_h = 22.0;
+ 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 };
@@ -648,7 +649,7 @@ pub fn view(state: &mut TimersState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
);
}
_ => {
- sec.pc.text("static", en_x + 8.0, btn_y + 5.0, 11.0, TEXT_DIM);
+ sec.pc.text("static", en_x + 8.0, btn_y + (btn_h - 12.0) / 2.0, 11.0, TEXT_DIM);
}
}
}
diff --git a/src/renderer.rs b/src/renderer.rs
index e42fbde..d50edc6 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -262,14 +262,16 @@ impl SystemInterface {
(false, false, false, false),
));
// The search box stands on the root plate: the window-edge inset.
+ // The toolkit's textbox height, centred in the 42px band.
let inset = cce_ui::layout::root_plate_inset();
+ let box_h = cce_ui::layout::textbox_height();
cce_ui::layout::render_widget(
&mut search_pc,
&mut self.search_box,
self.sidebar_width + inset,
- sh - 36.0,
+ sh - 42.0 + (42.0 - box_h) / 2.0,
sw - self.sidebar_width - 2.0 * inset,
- 30.0,
+ box_h,
&mut self.ui_context,
);
}