status bar
git clone https://git.lucas.co/cce-status-interface.git
feat(stats): numbers beside the glyphs, all readouts in one segment
The readout is now glyph then number, module { icon_gap } apart, at the
module font and regular weight — the superimposed form's ghosting, bold
weight and digit pocket go (icon_alpha now defaults to 1, icon_weight is
opt-in, icon_pocket is removed). The launcher runs one combined segment,
`stats`, holding cpu, memory, brightness, volume and battery in a single
bubble, module { icon_spacing } apart, in the compositor's old right-side
order; the five single names stay valid --module values. The compositor
orders `stats` between tray and clock (cce-window-manager@bead0c3).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
CLAUDE.md | 70 ++++++++-------
src/config.rs | 46 +++++-----
src/main.rs | 45 ++--------
src/modules.rs | 268 ++++++++++++++++++++++++++++++++++++++-------------------
4 files changed, 251 insertions(+), 178 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 90c1ee2..a8f1e40 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -9,7 +9,7 @@ is one crate in the multi-repo `cce` workspace — see `../cce-compositor/WORKSP
the workspace layout, the multi-repo git rules (commit here, never `git init` at the root),
and the `cce-ui` toolkit this app is built on. This crate is deliberately small:
`src/main.rs` (the `StatusApp` application, layout/input, launcher daemon),
-`src/modules.rs` (the `StatusModule` trait and its nine implementations),
+`src/modules.rs` (the `StatusModule` trait and its ten implementations),
`src/config.rs` (pointer-first config readers), `src/tray.rs` (SNI host),
`src/cloud.rs` (menu page building), `src/stats.rs` (system stat readers —
numbers, not strings; the modules do the formatting), `src/icons.rs` (tinted
@@ -36,7 +36,8 @@ One binary, three modes, selected by CLI args in `main()`:
resets it). This is the normal production mode: each module is its own process and
its own Wayland surface.
- **`--module <name>`** — a single-module bar segment. Valid names: `window`, `tray`,
- `cpu`, `memory`, `brightness`, `volume`, `battery`, `clock`, `light_source`.
+ `stats`, `cpu`, `memory`, `brightness`, `volume`, `battery`, `clock`, `light_source`
+ (the daemon launches `stats`, not the five it combines).
- **`--trigger-switcher`** — one-shot: writes `trigger` to the switcher socket of the
running instance and exits (used as a keybinding target).
@@ -86,27 +87,31 @@ Orientation is dynamic: `is_vertical()` compares the surface size against the
configured bar thickness; every module renders along one axis using `bar_h`/`coord`
accordingly.
-**The stat modules read out as a glyph, not a label.** `cpu`, `memory`,
-`brightness`, `volume` and `battery` are `IconStat` implementations (a
-blanket `impl<T: IconStat> StatusModule for T` in `modules.rs` does the shared
-layout): each names a cce-icons glyph, the bare number and a color, and
-`IconReadout` draws the glyph ghosted at `module { icon_alpha }` with the
-number centered on it — no unit symbol, since the glyph IS the unit ("87" on
-the battery, not "Bat 87%"). The number is bold (`module { icon_weight }`,
-the one run in the bar that sets `TextPrim`'s weight field) and sits in a
-POCKET: a feathered `Prim::Glow` pool in the number's contrast color, hugging
-the digits and drawn between the glyph and the number (`IconPrim::pocket`,
-`module { icon_pocket }`). Same-hue was the problem — white digits on a white
-ghost had only the alpha gap for contrast — and the pocket is the bubble
-scrim's answer applied locally: darken the ground under the digits rather
-than outline the letterforms. The muted sink swaps to `volume-muted`, the
-charging battery to `battery-charging` (the bolt runs the glyph's full
-height so its tips clear the digit pocket); the battery also keeps its
-accent color while charging or under 10%. A module whose
-reader has nothing (no battery, no backlight, no pactl) returns `None` and
-has width 0, i.e. it is hidden rather than an empty bubble; a reader that
-answers without a number (cpu with no /proc/stat, a sink with no level)
-draws the glyph alone.
+**The stat modules read out as a glyph with the number beside it, not a
+label.** `cpu`, `memory`, `brightness`, `volume` and `battery` are
+`IconStat` implementations: each names a cce-icons glyph, the bare number
+and a color, and `IconReadout` draws the glyph (tinted that color, at
+`module { icon_alpha }`) with the number `module { icon_gap }` to its right
+— no unit symbol, since the glyph IS the unit ("87" beside the battery, not
+"Bat 87%"). **The launcher runs them as ONE segment**, `stats`
+(`StatsModule`): every readout in a single bubble, `module { icon_spacing }`
+apart, in the order cpu, memory, brightness, volume, battery — the order the
+compositor's `RIGHT_ORDER` gave the five separate segments, and `stats` has
+its own slot there between `tray` and `clock` (cce-window-manager
+2026-09-16). The five single names stay valid `--module` values for a bar
+that wants them apart; a blanket `impl<T: IconStat> StatusModule for T`
+lays a lone readout out through the same `readouts_width` /
+`render_readouts` the combined segment uses. (Superimposing the number on a
+ghosted glyph, with a bold weight and a dark pocket under the digits, was
+tried first on 2026-09-16 and replaced the same day by the side-by-side
+form; `icon_weight` survives as an opt-in, the pocket is gone.) The muted
+sink swaps to `volume-muted`, the charging battery to `battery-charging`;
+the battery also keeps its accent color while charging or under 10%. A
+reader with nothing (no battery, no backlight, no pactl) returns `None`
+and drops out of the row — a lone module with nothing has width 0, i.e. it
+is hidden rather than an empty bubble; a reader that answers without a
+number (cpu with no /proc/stat, a sink with no level) draws the glyph
+alone.
The glyphs come from the **cce-icons** crate via `cce_ui::icons_dir()`
(`$CCE_ICONS_DIR`, else `~/projects/cce/cce-icons/svg`) — but NOT through
@@ -118,9 +123,9 @@ glyph that fails to load falls back to the old text readout ("Cpu 45%"), so
a bar started without the icon set is still attributable; **a shadow session
needs `CCE_ICONS_DIR` exported into the spawn**, its HOME being elsewhere,
exactly as it needs `CCE_FONTS_DIR`. Slot stability holds as before: the
-stable width is the wider of the glyph and the "100" template, and the glyph
-normally wins, so a value crossing a digit boundary never resizes the
-surface. `memory` reads as a percentage of the total in use (used = total
+stable width sizes every number at the "100" template, so a value crossing
+a digit boundary never resizes the surface, and the bubble eases to the
+live row. `memory` reads as a percentage of the total in use (used = total
less free, buffers and page cache) since 2026-09-16 — the "Mem 10/62G"
gigabyte form went with the label.
@@ -237,12 +242,13 @@ silhouette's AA feather plus `DropletSpec::shadow_gap()` for the shadow's
falloff (`main.rs`, three call sites — left, right, and the expanded menu
box, which becomes the drop growing))
and `module { icon_size }` (glyph height for the icon readouts, logical px,
-default bar height − 6) and `module { icon_font_size }` (the number on the
-glyph, default ¾ of `module { font_size }` — "100" at the full size
-overhangs a bar-height glyph) and `module { icon_alpha }` (the glyph's
-ghosting under the number, 0-1, default 0.4) and `module { icon_weight }`
-(OpenType weight of that number, default 700) and `module { icon_pocket }`
-(opacity 0-1 of the dark pocket under the digits, default 0.6, 0 = off) and `module { text_raise }` (lifts module text above vertical center, logical
+default bar height − 6) and `module { icon_font_size }` (the number beside
+the glyph, default `module { font_size }`) and `module { icon_gap }` (glyph
+to number, logical px, default 4) and `module { icon_spacing }` (between
+readouts in the `stats` bubble, default `module { spacing }`) and
+`module { icon_alpha }` (glyph opacity 0-1, default 1) and
+`module { icon_weight }` (OpenType weight of the number, unset = regular)
+and `module { text_raise }` (lifts module text above vertical center, logical
px, bar-side only — every module funnels through `centered_text_y`) and
`module { text_scrim }` (0-1 resting opacity of a
feathered pool filling each module box, the DE's one text-contrast treatment)
diff --git a/src/config.rs b/src/config.rs
index 943a63b..fc6ffe9 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -201,36 +201,38 @@ pub(crate) fn read_icon_size_from_config() -> f32 {
.max(4.0)
}
-/// `module { icon_font_size }` — the size of the number superimposed on a
-/// glyph. Defaults to three quarters of the module font size: "100" at the
-/// full size overhangs a bar-height glyph on both sides, and the glyph is
-/// what carries the unit now, so the number can afford to be smaller.
+/// `module { icon_font_size }` — the size of the number beside a glyph.
+/// Defaults to the module font size, so the readouts match the clock.
pub(crate) fn read_icon_font_size_from_config(font_size: f32) -> f32 {
- cfg_f32("/module/icon_font_size").unwrap_or(font_size * 0.75).max(1.0)
+ cfg_f32("/module/icon_font_size").unwrap_or(font_size).max(1.0)
}
-/// `module { icon_weight }` — OpenType weight of the number on a glyph
-/// (default 700, bold; 400 = the face's regular). Stroke width is what a
-/// digit of that size has to spare, so the number is heavier than the
-/// labels around it.
-pub(crate) fn read_icon_weight_from_config() -> Option<u16> {
- let w = cfg_f32("/module/icon_weight").unwrap_or(700.0).clamp(1.0, 1000.0) as u16;
- Some(w)
+/// `module { icon_gap }` — logical px between a glyph and its number
+/// (default 4). Narrower than `icon_spacing` so each pair reads as one.
+pub(crate) fn read_icon_gap_from_config() -> f32 {
+ cfg_f32("/module/icon_gap").unwrap_or(4.0).max(0.0)
+}
+
+/// `module { icon_spacing }` — logical px between readouts sharing a bubble
+/// (the `stats` segment). Defaults to the gap between segments, so the
+/// combined bubble is spaced like the strip it replaced.
+pub(crate) fn read_icon_spacing_from_config() -> f32 {
+ cfg_f32("/module/icon_spacing")
+ .unwrap_or_else(read_status_module_spacing_from_config)
+ .max(0.0)
}
-/// `module { icon_pocket }` — opacity 0-1 (default 0.6) of the feathered
-/// pool under the digits, in the number's contrast color: the notch the
-/// number sits in, cut into the glyph so the digits are never read against
-/// the glyph's own tint. 0 disables it.
-pub(crate) fn read_icon_pocket_from_config() -> f32 {
- cfg_f32("/module/icon_pocket").unwrap_or(0.6).clamp(0.0, 1.0)
+/// `module { icon_weight }` — OpenType weight of the number beside a glyph
+/// (700 = bold); unset leaves the face's regular, like every other label.
+pub(crate) fn read_icon_weight_from_config() -> Option<u16> {
+ cfg_f32("/module/icon_weight").map(|w| w.clamp(1.0, 1000.0) as u16)
}
-/// `module { icon_alpha }` — opacity 0-1 of the glyph under the number
-/// (default 0.4). The glyph is tinted the number's color, so it is this
-/// ghosting alone that keeps the digits legible on it.
+/// `module { icon_alpha }` — opacity 0-1 of the glyph (default 1). The
+/// glyph is tinted the number's color, so this is the one lever for making
+/// it read lighter than the digits.
pub(crate) fn read_icon_alpha_from_config() -> f32 {
- cfg_f32("/module/icon_alpha").unwrap_or(0.4).clamp(0.0, 1.0)
+ cfg_f32("/module/icon_alpha").unwrap_or(1.0).clamp(0.0, 1.0)
}
/// `module { text_contrast }` — adaptive contrast strength (0 = off, the
diff --git a/src/main.rs b/src/main.rs
index f7490a5..5dd3a80 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -14,7 +14,7 @@ pub(crate) use listeners::*;
pub(crate) use stats::*;
pub(crate) use tray::*;
-use modules::{StatusModule, WindowModule, ClockModule, BatteryModule, VolumeModule, BrightnessModule, MemoryModule, CpuModule, TrayModule, LightSourceModule};
+use modules::{StatusModule, WindowModule, ClockModule, BatteryModule, VolumeModule, BrightnessModule, MemoryModule, CpuModule, StatsModule, TrayModule, LightSourceModule};
use std::collections::HashMap;
use cce_ui::cosmic_text::{
@@ -86,23 +86,6 @@ pub struct IconPrim {
pub w: f32,
pub h: f32,
pub alpha: f32,
- /// The dark pocket the number sits in, cut into the glyph — see `Pocket`.
- pub pocket: Option<Pocket>,
-}
-
-/// A feathered pool under a readout's digits (`cce_ui`'s `Prim::Glow`, the
-/// same ground treatment the bubble scrim is): solid through the core rect,
-/// fading to nothing across `feather` px outside it. Drawn between the glyph
-/// and the number, so the digits sit in a notch of the number's contrast
-/// color rather than on the glyph's own tint. Logical px.
-#[derive(Debug, Clone, Copy)]
-pub struct Pocket {
- pub x: f32,
- pub y: f32,
- pub w: f32,
- pub h: f32,
- pub feather: f32,
- pub color: [f32; 4],
}
/// The subset of [`SystemStats`] a given module actually paints, as a comparable
@@ -121,6 +104,7 @@ fn stats_signature(module: Option<&str>, s: &SystemStats) -> Option<String> {
Some("volume") => Some(format!("{:?}", s.volume)),
Some("brightness") => Some(format!("{:?}", s.brightness)),
Some("window") | Some("tray") | Some("light_source") => None,
+ // `stats` paints every reader's value; so does an unknown name.
_ => Some(format!(
"{}|{:?}|{:?}|{:?}|{:?}|{:?}",
s.clock, s.memory, s.cpu_pct, s.battery, s.volume, s.brightness
@@ -1195,13 +1179,6 @@ impl StatusApp {
ip.y = old_x;
ip.w = old_h;
ip.h = old_w;
- if let Some(pk) = &mut ip.pocket {
- let (old_x, old_y, old_w, old_h) = (pk.x, pk.y, pk.w, pk.h);
- pk.x = old_y;
- pk.y = old_x;
- pk.w = old_h;
- pk.h = old_w;
- }
}
// Rotate tray_item_bounds
for tib in &mut self.tray_item_bounds {
@@ -1468,6 +1445,7 @@ impl cce_ui::engine::Application for StatusApp {
"brightness" => Box::new(BrightnessModule),
"volume" => Box::new(VolumeModule),
"battery" => Box::new(BatteryModule),
+ "stats" => Box::new(StatsModule),
"clock" => Box::new(ClockModule),
"light_source" => Box::new(LightSourceModule),
_ => panic!("Unknown module: {}", name),
@@ -1502,7 +1480,7 @@ impl cce_ui::engine::Application for StatusApp {
tokio::spawn(spawn_status_tray(sender.clone()));
}
let has_stats = selected_module.as_ref().map_or(true, |(name, _)| {
- name == "cpu" || name == "memory" || name == "brightness" || name == "volume" || name == "battery" || name == "clock"
+ name == "stats" || name == "cpu" || name == "memory" || name == "brightness" || name == "volume" || name == "battery" || name == "clock"
});
if has_stats {
tokio::spawn(spawn_system_stats(sender.clone()));
@@ -1955,14 +1933,9 @@ impl cce_ui::engine::Application for StatusApp {
);
}
- // Readout glyphs: post-scrim so the pool grounds them like any run,
- // pre-text so the number lands on the glyph.
+ // Readout glyphs: post-scrim so the pool grounds them like any run.
for ip in &self.icon_prims {
pc.image(ip.image, Rect { x: ip.x, y: ip.y, width: ip.w, height: ip.h }, ip.alpha);
- if let Some(pk) = ip.pocket {
- let core = Rect { x: pk.x, y: pk.y, width: pk.w, height: pk.h };
- pc.glow(core, pk.h.min(pk.w) / 2.0, pk.feather, pk.color);
- }
}
for (text, tsize, x, y, color, font, bounds, layout, _run_w, weight) in &self.text_prims {
@@ -2367,10 +2340,10 @@ fn main() {
let mut sigint = signal(SignalKind::interrupt()).expect("SIGINT");
let mut sigterm = signal(SignalKind::terminate()).expect("SIGTERM");
- let modules = vec![
- "window", "tray", "cpu", "memory", "brightness",
- "volume", "battery", "clock", "light_source"
- ];
+ // `stats` is the combined readout segment (cpu, memory,
+ // brightness, volume, battery in one bubble); the five single
+ // names remain valid `--module` values but are not launched.
+ let modules = vec!["window", "tray", "stats", "clock", "light_source"];
let current_exe = std::env::current_exe().unwrap_or_else(|_| {
std::path::PathBuf::from(std::env::var("HOME").unwrap_or_default())
.join(".local/bin/cce-status-interface")
diff --git a/src/modules.rs b/src/modules.rs
index a84b53a..7216627 100644
--- a/src/modules.rs
+++ b/src/modules.rs
@@ -284,11 +284,10 @@ impl StatusModule for ClockModule {
}
}
-/// A stat module's readout as a cce-icons glyph with its value superimposed
-/// — the glyph IS the unit, so the number is bare: "87" on the battery, not
-/// "Bat 87%". The glyph is tinted the readout's color and ghosted under the
-/// number (`module { icon_alpha }`); see `icons.rs` for why the tint is done
-/// here rather than through `cce_ui::upload_icon`.
+/// A stat module's readout: a cce-icons glyph with its value beside it —
+/// the glyph IS the unit, so the number is bare ("87" next to the battery,
+/// not "Bat 87%"). The glyph is tinted the readout's color; see `icons.rs`
+/// for why the tint is done here rather than through `cce_ui::upload_icon`.
///
/// The pre-glyph text form rides along as the FALLBACK: `tinted_icon` returns
/// `None` when the icon set is missing or unparsable, and a readout that
@@ -296,7 +295,7 @@ impl StatusModule for ClockModule {
/// it degrades to the old "Cpu 45%" instead.
pub(crate) struct IconReadout {
pub icon: &'static str,
- /// The number drawn over the glyph; `None` draws the glyph alone (a
+ /// The number drawn beside the glyph; `None` draws the glyph alone (a
/// value the reader could not produce — cpu with no /proc/stat, a
/// sink without a level).
pub number: Option<String>,
@@ -320,8 +319,8 @@ impl IconReadout {
Some((image, w as f32 / scale, h as f32 / scale))
}
- /// The number's font size — `module { icon_font_size }`, else a
- /// fraction of the module font.
+ /// The number's font size — `module { icon_font_size }`, else the
+ /// module font.
fn number_size(font_size: f32) -> f32 {
crate::read_icon_font_size_from_config(font_size)
}
@@ -332,35 +331,29 @@ impl IconReadout {
buf.layout_runs().next().map(|r| r.line_w).unwrap_or(0.0) / cce_ui::scale::scale_factor()
}
- /// Stable slot width: the glyph, the widest number and the live number
- /// — whichever is widest — plus padding. Normally the glyph wins, which
- /// is what makes the slot stable; a number wider than the glyph would
- /// overhang it symmetrically, and the slot allows for that.
- fn width(&self, font_system: &mut FontSystem, font_family: &str, font_size: f32, padding: f32) -> f32 {
+ /// This item's width — glyph, gap and number — with the number as the
+ /// live value (`template` false) or the widest plausible one (`template`
+ /// true, for the stable slot). In text fallback, the fallback string or
+ /// its template.
+ fn item_width(&self, font_system: &mut FontSystem, font_family: &str, font_size: f32, template: bool) -> f32 {
match self.glyph() {
Some((_, gw, _)) => {
let ns = Self::number_size(font_size);
- let tmpl = Self::number_width(font_system, NUMBER_TEMPLATE, ns, font_family);
- let live = self.number.as_deref().map_or(0.0, |n| Self::number_width(font_system, n, ns, font_family));
- gw.max(tmpl).max(live) + 2.0 * padding
+ let nw = if template {
+ Self::number_width(font_system, NUMBER_TEMPLATE, ns, font_family)
+ } else {
+ self.number.as_deref().map_or(0.0, |n| Self::number_width(font_system, n, ns, font_family))
+ };
+ if nw > 0.0 { gw + crate::read_icon_gap_from_config() + nw } else { gw }
}
- None => stable_text_width(font_system, &self.fallback, self.fallback_template, font_size, font_family, padding),
- }
- }
-
- /// Live content width: the glyph or the live number, whichever is wider,
- /// plus padding.
- fn content_width(&self, font_system: &mut FontSystem, font_family: &str, font_size: f32, padding: f32) -> f32 {
- match self.glyph() {
- Some((_, gw, _)) => {
- let ns = Self::number_size(font_size);
- let live = self.number.as_deref().map_or(0.0, |n| Self::number_width(font_system, n, ns, font_family));
- gw.max(live) + 2.0 * padding
+ None => {
+ let text = if template { self.fallback_template } else { self.fallback.as_str() };
+ Label::new_with_family(font_system, text, font_size, [0.0, 0.0, 0.0, 1.0], font_family).w
}
- None => live_text_width(font_system, &self.fallback, font_size, font_family, padding),
}
}
+ /// Draw the item with its left edge at `x`; returns the width drawn.
fn render(
&self,
x: f32,
@@ -370,78 +363,94 @@ impl IconReadout {
bar_h: f32,
text_prims: &mut Vec<crate::TextPrim>,
icon_prims: &mut Vec<crate::IconPrim>,
- padding: f32,
- ) {
+ ) -> f32 {
match self.glyph() {
Some((image, gw, gh)) => {
- let ns = Self::number_size(font_size);
- let weight = crate::read_icon_weight_from_config();
- let number = self
- .number
- .as_deref()
- .map(|n| (n.to_string(), Self::number_width(font_system, n, ns, font_family)));
- // Glyph and number share one center: the wider of the two
- // spans the content, and both are centered on it. The same
- // `module { text_raise }` lift every text run gets via
- // `centered_text_y` is applied to the glyph too, so the pair
- // stays concentric and level with the neighboring modules.
- let span = gw.max(number.as_ref().map_or(0.0, |(_, w)| *w));
- let cx = x + padding + span / 2.0;
+ // The same `module { text_raise }` lift every text run gets
+ // via `centered_text_y` is applied to the glyph too, so
+ // glyph and number stay level with each other and with the
+ // neighboring modules' text.
let gy = (bar_h - gh) / 2.0 - crate::config::read_text_raise_from_config();
- let rgb = crate::icons::tint_of(self.color);
- let mut pocket = None;
- if let Some((text, nw)) = number {
- let nx = cx - nw / 2.0;
- let ny = centered_text_y(bar_h, ns);
- // The pocket hugs the digits, not the line box: in a
- // line box of exactly `ns`, digits run from about the
- // cap line at 0.1em down to the baseline near 0.85em.
- // Its feather reaches out from that core, so the notch
- // in the glyph is a little larger than the digits.
- let pocket_alpha = crate::read_icon_pocket_from_config();
- if pocket_alpha > 0.0 {
- let c = crate::treatment_rgb(rgb);
- pocket = Some(crate::Pocket {
- x: nx - 1.0,
- y: ny + ns * 0.1,
- w: nw + 2.0,
- h: ns * 0.75,
- feather: ns * 0.3,
- color: [c[0], c[1], c[2], pocket_alpha],
- });
- }
+ icon_prims.push(crate::IconPrim {
+ image,
+ x,
+ y: gy,
+ w: gw,
+ h: gh,
+ alpha: crate::read_icon_alpha_from_config(),
+ });
+ let mut w = gw;
+ if let Some(n) = self.number.as_deref() {
+ let ns = Self::number_size(font_size);
+ let nw = Self::number_width(font_system, n, ns, font_family);
+ let nx = x + gw + crate::read_icon_gap_from_config();
text_prims.push((
- text,
+ n.to_string(),
ns,
nx,
- ny,
- rgb,
+ centered_text_y(bar_h, ns),
+ crate::icons::tint_of(self.color),
Some(font_family.to_string()),
None,
None,
Some(nw),
- weight,
+ crate::read_icon_weight_from_config(),
));
+ w = nx + nw - x;
}
- icon_prims.push(crate::IconPrim {
- image,
- x: cx - gw / 2.0,
- y: gy,
- w: gw,
- h: gh,
- alpha: crate::read_icon_alpha_from_config(),
- pocket,
- });
+ w
}
None => {
let label = Label::new_with_family(font_system, &self.fallback, font_size, self.color, font_family);
- crate::draw_label(text_prims, label, x + padding, centered_text_y(bar_h, font_size));
+ crate::draw_label(text_prims, label, x, centered_text_y(bar_h, font_size))
}
}
}
}
-/// A module that reads out as an [`IconReadout`]: it only has to say which
+/// Width of a row of readouts in one bubble: `padding` inside each end,
+/// `module { icon_spacing }` between items. 0 for an empty row (the module
+/// hides). `template` sizes every number at its widest, for the stable
+/// slot; the live row is what the bubble hugs.
+fn readouts_width(
+ items: &[IconReadout],
+ font_system: &mut FontSystem,
+ font_family: &str,
+ font_size: f32,
+ padding: f32,
+ template: bool,
+) -> f32 {
+ if items.is_empty() {
+ return 0.0;
+ }
+ let spacing = crate::read_icon_spacing_from_config();
+ let sum: f32 = items.iter().map(|r| r.item_width(font_system, font_family, font_size, template)).sum();
+ sum + spacing * (items.len() as f32 - 1.0) + 2.0 * padding
+}
+
+/// Draw a row of readouts starting at the bubble's left edge `x`.
+fn render_readouts(
+ items: &[IconReadout],
+ x: f32,
+ font_system: &mut FontSystem,
+ font_family: &str,
+ font_size: f32,
+ bar_h: f32,
+ text_prims: &mut Vec<crate::TextPrim>,
+ icon_prims: &mut Vec<crate::IconPrim>,
+ padding: f32,
+) {
+ let spacing = crate::read_icon_spacing_from_config();
+ let mut cx = x + padding;
+ for (i, r) in items.iter().enumerate() {
+ if i > 0 {
+ cx += spacing;
+ }
+ cx += r.render(cx, font_system, font_family, font_size, bar_h, text_prims, icon_prims);
+ }
+}
+
+/// A module that reads out as one [`IconReadout`]: it only has to say which
/// glyph, which number and which color, and the blanket `StatusModule` impl
/// below does the shared layout. `None` hides the module (width 0) — a
/// machine with no battery or backlight has nothing to read out.
@@ -465,8 +474,9 @@ impl<T: IconStat> StatusModule for T {
) -> f32 {
// The color only tints the glyph, and the width is the same in any
// tint; the readout's own color is applied at render.
- T::readout(stats, color::TEXT_FG)
- .map_or(0.0, |r| r.width(font_system, font_family, font_size, padding))
+ let items: Vec<_> = T::readout(stats, color::TEXT_FG).into_iter().collect();
+ readouts_width(&items, font_system, font_family, font_size, padding, true)
+ .max(readouts_width(&items, font_system, font_family, font_size, padding, false))
}
fn content_width(
@@ -479,8 +489,8 @@ impl<T: IconStat> StatusModule for T {
_tray_items: &HashMap<String, TrayItem>,
padding: f32,
) -> f32 {
- T::readout(stats, color::TEXT_FG)
- .map_or(0.0, |r| r.content_width(font_system, font_family, font_size, padding))
+ let items: Vec<_> = T::readout(stats, color::TEXT_FG).into_iter().collect();
+ readouts_width(&items, font_system, font_family, font_size, padding, false)
}
fn render(
@@ -506,9 +516,91 @@ impl<T: IconStat> StatusModule for T {
_rounded_boxes: &mut Vec<RoundedBox>,
padding: f32,
) {
- if let Some(r) = T::readout(stats, normal_color) {
- r.render(x, font_system, font_family, font_size, bar_h, text_prims, icon_prims, padding);
- }
+ let items: Vec<_> = T::readout(stats, normal_color).into_iter().collect();
+ render_readouts(&items, x, font_system, font_family, font_size, bar_h, text_prims, icon_prims, padding);
+ }
+}
+
+/// The combined readout segment: every `IconStat` module's readout in one
+/// bubble, in the order the compositor used to lay the five separate
+/// segments out (cpu, memory, brightness, volume, battery). A reader with
+/// nothing (no battery, no backlight) simply drops out of the row. This is
+/// what the launcher daemon runs; the five single names stay valid for a
+/// bar configured to run them separately.
+pub struct StatsModule;
+
+impl StatsModule {
+ fn readouts(stats: &Option<SystemStats>, normal_color: [f32; 4]) -> Vec<IconReadout> {
+ [
+ CpuModule::readout(stats, normal_color),
+ MemoryModule::readout(stats, normal_color),
+ BrightnessModule::readout(stats, normal_color),
+ VolumeModule::readout(stats, normal_color),
+ BatteryModule::readout(stats, normal_color),
+ ]
+ .into_iter()
+ .flatten()
+ .collect()
+ }
+}
+
+impl StatusModule for StatsModule {
+ fn name(&self) -> &'static str { "stats" }
+
+ fn width(
+ &self,
+ stats: &Option<SystemStats>,
+ _title: &str,
+ font_system: &mut FontSystem,
+ font_family: &str,
+ font_size: f32,
+ _tray_items: &HashMap<String, TrayItem>,
+ padding: f32,
+ ) -> f32 {
+ let items = Self::readouts(stats, color::TEXT_FG);
+ readouts_width(&items, font_system, font_family, font_size, padding, true)
+ .max(readouts_width(&items, font_system, font_family, font_size, padding, false))
+ }
+
+ fn content_width(
+ &self,
+ stats: &Option<SystemStats>,
+ _title: &str,
+ font_system: &mut FontSystem,
+ font_family: &str,
+ font_size: f32,
+ _tray_items: &HashMap<String, TrayItem>,
+ padding: f32,
+ ) -> f32 {
+ let items = Self::readouts(stats, color::TEXT_FG);
+ readouts_width(&items, font_system, font_family, font_size, padding, false)
+ }
+
+ fn render(
+ &self,
+ x: f32,
+ _w: f32,
+ stats: &Option<SystemStats>,
+ _title: &str,
+ font_system: &mut FontSystem,
+ font_family: &str,
+ font_size: f32,
+ normal_color: [f32; 4],
+ bar_h: f32,
+ _scale_factor: f64,
+ text_prims: &mut Vec<crate::TextPrim>,
+ icon_prims: &mut Vec<crate::IconPrim>,
+ _rects: &mut Vec<RectWidget>,
+ _overlay_rects: &mut Vec<RectWidget>,
+ _tray_items: &HashMap<String, TrayItem>,
+ _tray_item_bounds: &mut Vec<TrayIconBounds>,
+ _box_bg_color: Option<[f32; 4]>,
+ _status_box_radius: f32,
+ _rounded_boxes: &mut Vec<RoundedBox>,
+ padding: f32,
+ ) {
+ let items = Self::readouts(stats, normal_color);
+ render_readouts(&items, x, font_system, font_family, font_size, bar_h, text_prims, icon_prims, padding);
}
}