system settings
git clone https://git.lucas.co/cce-system-interface.git
services: transport icons instead of Start/Stop/Restart labels
The three per-row buttons now wear cce-icons glyphs (play/stop/refresh), so
all three are the same square and the room "Restart" needed goes back to the
service description.
This app drew no images at all before: it is a flat host, `all_quads` carries
quads and the text list carries labels, and an icon is neither. So the page
now collects each button's icon rect from `Button::icon_rect` — the same
source `Button::paint` reads, so the glyph sits where the paint path would
put it — and `display_list` draws them under the page-viewport clip. That
clip is load-bearing: an image has no geometry to trim, so the clip is the
only thing that cuts a half-scrolled row's icon at the list edge.
Two things kept honest:
- The label survives as `button_icon`'s FALLBACK, because `upload_icon`
returns None when the icon set is missing and a control that loses its face
has no affordance left. The button widths follow the same test: at 24px a
fallback label doesn't truncate so much as invert — it centers, both ends
cut, and "Restart" reads "sta". Verified against an empty CCE_ICONS_DIR:
full words at their old widths.
- Disabled state dims the glyph (alpha 0.35) rather than graying it, since an
image carries no color to gray.
The dispatch clone still runs for an icon button — only the label block is
skipped — re-checked live by driving the System/User Services tabs, which
register through the same path.
src/app.rs | 26 ++++++++++++++++++++++++++
src/main.rs | 24 ++++++++++++++++++++++++
src/pages/services.rs | 48 +++++++++++++++++++++++++++++++++++++++---------
src/renderer.rs | 25 +++++++++++++++++++++++++
4 files changed, 114 insertions(+), 9 deletions(-)
diff --git a/src/app.rs b/src/app.rs
index 5624342..2165dd8 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -269,6 +269,32 @@ impl PageContent {
self.buttons.push((btn, action, clip));
}
+ /// A button whose face is a bundled cce-icons glyph instead of a label.
+ ///
+ /// `label` stays as the FALLBACK: `upload_icon` returns `None` when the
+ /// icon set is missing or unparsable, and a control that silently loses
+ /// its face has no affordance left at all — so the button degrades to the
+ /// word rather than to an empty box. `alpha` dims the glyph for a disabled
+ /// control, which is the only state lever an icon has (images carry no
+ /// color).
+ pub fn button_icon(&mut self, icon: &str, label: &str, x: f32, y: f32, w: f32, h: f32,
+ bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4],
+ alpha: f32, action: AppAction) {
+ if self.measure_only { return; }
+ let mut btn = cce_ui::widget::Button::new(x, y, w, h)
+ .with_label(label)
+ .with_bg(bg)
+ .with_hover_bg(hover_bg)
+ .with_label_color(label_color);
+ // 32px on the longer side: the toolkit caches the upload per (name, px),
+ // so every row's Start button shares one texture.
+ if let Some((id, iw, ih)) = cce_ui::upload_icon(icon, 32) {
+ btn = btn.with_icon(id, iw as f32, ih as f32).with_icon_alpha(alpha);
+ }
+ let clip = self.clip_stack.last().copied();
+ self.buttons.push((btn, action, clip));
+ }
+
pub fn button_left(&mut self, label: &str, x: f32, y: f32, w: f32, h: f32,
bg: [f32; 4], hover_bg: [f32; 4], label_color: [f32; 4],
action: AppAction) {
diff --git a/src/main.rs b/src/main.rs
index af72d5a..cc975a4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -109,6 +109,11 @@ struct SystemInterface {
/// Control troughs from `PageContent::control_reliefs` (page coords,
/// pre-scroll) — each carved as a flush inset plate after the section wells.
page_control_reliefs: Vec<ControlCarve>,
+ /// Icon faces for the page's buttons — `(image, x, y, w, h, alpha)`, page
+ /// coords already scroll-shifted by the renderer. A flat host draws no
+ /// images at all otherwise: `all_quads` carries quads and the text list
+ /// carries labels, and an icon is neither.
+ page_button_images: Vec<(u32, f32, f32, f32, f32, f32)>,
// Root Backplate + StatusBar DISSOLVED (Phase 6s): the window plate and the status
// bar are emitted as tuples in rebuild_layout.
sans_serif_family: String,
@@ -202,6 +207,7 @@ impl cce_ui::engine::Application for SystemInterface {
content_h: 0.0,
page_reliefs: Vec::new(),
page_control_reliefs: Vec::new(),
+ page_button_images: Vec::new(),
sans_serif_family: sans_family,
serif_family,
monospace_family,
@@ -431,6 +437,24 @@ impl cce_ui::engine::Application for SystemInterface {
});
}
+ // Button icon faces, over the page's quads and its carves — clipped to
+ // the page viewport, which is what cuts a half-scrolled list row's icon
+ // at the list edge (an image has no geometry to trim, only a clip).
+ if !self.page_button_images.is_empty() {
+ let view = Rect {
+ x: self.sidebar_width,
+ y: self.header_height,
+ width: width - self.sidebar_width,
+ height: (height - self.header_height - self.status_height
+ - if self.search_open { 42.0 } else { 0.0 }).max(0.0),
+ };
+ pc.clip(view, |pc| {
+ for &(image, x, y, w, h, alpha) in &self.page_button_images {
+ pc.image(image, Rect { x, y, width: w, height: h }, alpha);
+ }
+ });
+ }
+
cce_ui::widget::hover_animation::post_render_check();
if let Some((qx, qy, qw, qh, qc)) = cce_ui::widget::hover_animation::get_quad() {
pc.quad(Rect { x: qx, y: qy - self.scroll_y, width: qw, height: qh }, qc);
diff --git a/src/pages/services.rs b/src/pages/services.rs
index 0891ec6..787d84b 100644
--- a/src/pages/services.rs
+++ b/src/pages/services.rs
@@ -154,10 +154,23 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, _root
if let Some(draw_y) = state.list.get_item_draw_y(idx, 4.0) {
let is_active = service.active_state == "active" || service.sub_state == "running";
- // Control buttons: Start, Stop, Restart on the right
+ // Control buttons: Start, Stop, Restart on the right. With
+ // icon faces all three are the same square, and the extra
+ // room "Restart" needed goes back to the description.
+ //
+ // 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
+ // — 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 = if is_small { 24.0 } else { 46.0 };
- let r_btn_w = if is_small { 24.0 } else { 54.0 };
+ let (btn_w, r_btn_w) = match (icons_ok, is_small) {
+ (true, _) => (24.0, 24.0),
+ (false, true) => (24.0, 24.0),
+ (false, false) => (46.0, 54.0),
+ };
let btn_gap = if is_small { 4.0 } else { 6.0 };
let right_edge = list_box_x + list_box_w - 24.0 - 8.0;
@@ -199,12 +212,24 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, _root
let active_txt = [0.90, 0.90, 0.95, 1.0];
let disabled_txt = [0.40, 0.40, 0.45, 1.0];
- let start_lbl = if is_small { "▶" } else { "Start" };
- let stop_lbl = if is_small { "■" } else { "Stop" };
- let restart_lbl = if is_small { "⟳" } else { "Restart" };
+ // Icon faces (cce-icons `play`/`stop`/`refresh`); the words
+ // ride along as the fallback `button_icon` falls back to
+ // when the icon set is missing. A control that can't act
+ // dims its glyph rather than graying it — an image carries
+ // no color to gray.
+ let dim = 0.35;
+ // Fallback labels only — an icon face never draws them.
+ // Without the icons a narrow row is back to needing the
+ // one-glyph words it used before.
+ let (start_lbl, stop_lbl, restart_lbl) = if icons_ok || !is_small {
+ ("Start", "Stop", "Restart")
+ } else {
+ ("\u{25b6}", "\u{25a0}", "\u{27f3}")
+ };
// Start button
- sec.pc.button(
+ sec.pc.button_icon(
+ "play",
start_lbl,
start_x,
btn_y,
@@ -213,11 +238,13 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, _root
if !is_active { [0.16, 0.35, 0.18, 0.4] } else { [0.12, 0.12, 0.16, 0.1] },
[0.22, 0.45, 0.25, 0.6],
if !is_active { active_txt } else { disabled_txt },
+ if !is_active { 1.0 } else { dim },
crate::app::AppAction::Services(ServicesMessage::Start(service.name.clone(), service.is_system)),
);
// Stop button
- sec.pc.button(
+ sec.pc.button_icon(
+ "stop",
stop_lbl,
stop_x,
btn_y,
@@ -226,11 +253,13 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, _root
if is_active { [0.55, 0.16, 0.16, 0.3] } else { [0.12, 0.12, 0.16, 0.1] },
[0.70, 0.22, 0.22, 0.5],
if is_active { active_txt } else { disabled_txt },
+ if is_active { 1.0 } else { dim },
crate::app::AppAction::Services(ServicesMessage::Stop(service.name.clone(), service.is_system)),
);
// Restart button
- sec.pc.button(
+ sec.pc.button_icon(
+ "refresh",
restart_lbl,
restart_x,
btn_y,
@@ -239,6 +268,7 @@ pub fn view(state: &mut ServicesState, cx: f32, cy: f32, cw: f32, ch: f32, _root
[0.15, 0.28, 0.45, 0.3],
[0.20, 0.38, 0.58, 0.5],
active_txt,
+ 1.0,
crate::app::AppAction::Services(ServicesMessage::Restart(service.name.clone(), service.is_system)),
);
}
diff --git a/src/renderer.rs b/src/renderer.rs
index 6456898..c6231f4 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -105,6 +105,7 @@ impl SystemInterface {
let mut widgets = Vec::new();
let mut texts = Vec::new();
let mut page_buttons = Vec::new();
+ let mut page_button_images: Vec<(u32, f32, f32, f32, f32, f32)> = Vec::new();
cce_ui::widget::hover_animation::reset_frame_registration();
self.ui_context.clear_popovers();
@@ -504,6 +505,28 @@ impl SystemInterface {
radius: cce_ui::layout::button_corner_radius() * s,
corners: (true, true, true, true),
});
+ // An icon face replaces the label entirely (as it does in
+ // `Button::paint`). The rect comes from the button's own
+ // `icon_rect` so the glyph lands where the paint path would put
+ // it; display_list draws these under the page clip, which is what
+ // cuts a half-scrolled row's icon at the list edge.
+ let has_icon = if let Some((image, irect, alpha)) =
+ btn.icon_rect(cce_ui::scene::layout::Rect {
+ x: base.x,
+ y: base.y - scroll_offset_y,
+ width: base.w,
+ height: base.h,
+ })
+ {
+ page_button_images.push((image, irect.x, irect.y, irect.width, irect.height, alpha));
+ true
+ } else {
+ false
+ };
+
+ // The label is skipped for an icon face, but NOT the dispatch clone
+ // below it: an icon button still has to be clickable.
+ if !has_icon {
let label = base.label.as_deref().unwrap_or("");
let label_size = 12.0;
let buf = make_text_buffer_with_font(
@@ -564,6 +587,7 @@ impl SystemInterface {
btn.widget_font(),
button_bounds,
));
+ }
let mut btn_clone = btn.clone();
{
// The dispatch clone hit-tests at the CLAMPED rect, so clicks in
@@ -675,6 +699,7 @@ impl SystemInterface {
self.widgets = widgets;
self.texts = texts;
self.page_buttons = page_buttons;
+ self.page_button_images = page_button_images;
// The id-rooted router (`propagate_event(event, WidgetId)`) resolves roots
// through the registry, and `clear_hierarchy` above wiped it. The view pass