git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commitce5ae105f78ca1d55a6ba67dfcefa9e9f6a9a3d2
parentc8442d1f79
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-06 10:09
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,
             );
         }