status bar
git clone https://git.lucas.co/cce-status-interface.git
refactor: delete text_halo and text_relief
Both decorated the letterforms — a four-copy outline and a letterpress
underlay — and both were superseded by the scrim, which darkens the ground
instead. They survived one more commit as "the manual override for a
session whose compositor predates the backdrop topic", which was true and
is now not worth the branch: that compositor is this workspace's, the
topic shipped, and two treatments answering one question is how the halo
came to be drawn on top of a darkened ground in the first place.
The adaptive machinery stays, renamed off the halo it used to drive:
halo_demand -> contrast_demand, halo_now -> contrast_now,
backdrop_halo_target -> backdrop_contrast_demand. The paint path for a
non-boxed run is now one call.
text_contrast alone gains a meaning it did not have: with no text_scrim to
rest on, the pool appears ONLY when the backdrop earns it, since the
resting opacity is zero and the demand supplies the rest. Verified in a
shadow session — over a near-white desktop the pool appears under white
text, and over the black one (backdrop `4 8`) the bubble is bare.
Co-Authored-By: Claude Opus 5 <[email protected]>
CLAUDE.md | 69 +++++++++++++++++-----------------
src/config.rs | 30 +++------------
src/main.rs | 118 +++++++++++++++++++++-------------------------------------
3 files changed, 83 insertions(+), 134 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index db18191..aa054b3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -153,19 +153,18 @@ warn-and-skip on unknown keys; the expanded menu box becomes the drop growing,
and drops inset 1px from the surface bottom for the silhouette's AA feather)
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_relief }` (letterpress underlay strength 0-1: a translucent
-white copy 0.75px down-right beneath each non-boxed text run — engraved text,
-guaranteed contrast on dark backdrops) and `module { text_halo }` (full white
-outline 0-1: FOUR diagonal white copies around each run; beats text_relief
-when set) and `module { text_scrim }` (0-1 opacity of a
-feathered pool filling each module box; beats the halo when set) and
-`module { text_scrim_feather }` (that pool's falloff in logical px, default a
-quarter of the box height) and `module { text_contrast }` (adaptive contrast 0-1, default 0 =
-off — when set it SUPERSEDES both fixed knobs above and drives the halo from
-the compositor's `backdrop` measurement instead, so the outline appears only
-over a backdrop the configured text color cannot carry; `text_relief`/
-`text_halo` remain as the manual override for a session whose compositor
-predates the topic). Everything is read through
+`module { text_scrim }` (0-1 resting opacity of a
+feathered pool filling each module box, the DE's one text-contrast treatment)
+and `module { text_scrim_feather }` (that pool's falloff in logical px,
+default a quarter of the box height) and `module { text_contrast }` (adaptive
+contrast 0-1, default 0 = off — the compositor's `backdrop` measurement
+deepens the pool through it, so the ground darkens only as far as a backdrop
+the configured text color cannot carry demands; on its own, with no
+`text_scrim`, it makes the pool appear ONLY when the backdrop earns it).
+(The glyph-decorating treatments this replaced — `text_relief`'s letterpress
+underlay and `text_halo`'s four-copy outline — were deleted 2026-08-28 once
+the scrim superseded both; don't reintroduce a per-letterform treatment
+without a reason the ground cannot serve.) Everything is read through
`cce_ui::config::cached_config()`; KDL is converted to JSON
(`cce_ui::config::parse_kdl_to_json`) and looked up by **explicit JSON
pointer only**: every key names its canonical nesting
@@ -199,21 +198,21 @@ closes that loop with the compositor, which CAN see:
1. `cce-fx` measures each segment's backdrop per frame (`backdrop.rs`) and
pushes `<luma> <spread>` on the status socket's `backdrop` topic.
-2. `halo_demand()` checks the configured text color's WCAG contrast against
- that backdrop at three points — the mean AND both ends of the spread — and
- takes the worst. Checking only the mean is the trap: a segment half on a
- black grid cell and half on a light gap averages to a comfortable mid-gray
- while the text is invisible over one half.
-3. `tick` eases `halo_now` toward that demand over ~120ms. Stepping straight
- to it makes the outline strobe as the desktop pans under the segment.
-
-`module { text_scrim }` is the alternative treatment, and supersedes the halo
-when set: a feathered pool (`cce_ui`'s `Prim::Glow` — solid through a core
-rect, falling off to nothing across `text_scrim_feather` px, tessellated as
-per-vertex-alpha rings so there is no banding) filling each module box. Where
-the halo rims every letterform, this darkens the ground they sit on. It rests
-at the configured opacity and `text_contrast` deepens it from there, so it is
-a constant when that knob is off.
+2. `contrast_demand()` checks the configured text color's WCAG contrast
+ against that backdrop at three points — the mean AND both ends of the
+ spread — and takes the worst. Checking only the mean is the trap: a segment
+ half on a black grid cell and half on a light gap averages to a comfortable
+ mid-gray while the text is invisible over one half.
+3. `tick` eases `contrast_now` toward that demand over ~120ms. Stepping
+ straight to it makes the scrim pulse as the desktop pans under the segment.
+
+`module { text_scrim }` is the treatment itself: a feathered pool (`cce_ui`'s
+`Prim::Glow` — solid through a core rect, falling off to nothing across
+`text_scrim_feather` px, tessellated as per-vertex-alpha rings so there is no
+banding) filling each module box. It darkens the ground the glyphs sit on
+rather than decorating the letterforms. It rests at the configured opacity and
+`text_contrast` deepens it from there, so it is a constant when that knob is
+off — and either knob alone is meaningful.
One pool per bubble, not per text run, so a segment reads as one darkened
lozenge rather than a pill inside a pill. Its shape is the bubble's ACTUAL
@@ -231,14 +230,14 @@ nothing to ground). That, and the pool's color, come from
rides `TextPrim`'s last field. Width is the tiebreak because a module mixing
colors is led by its longest label.
-Both treatments take their color from `treatment_rgb` — whichever of
-black/white the run reads against, by contrast ratio (the WCAG crossover is
-near 0.18, not 0.5). Applied PER RUN, not from the configured module color: a
+The pool takes its color from `treatment_rgb` — whichever of black/white the
+run reads against, by contrast ratio (the WCAG crossover is near 0.18, not
+0.5). Chosen from the run's OWN color, not the configured module color: a
module may paint a run in something else entirely, and the volume module's
-muted state uses the shared `disabled_color`, which on this DE is black. A
-white halo around white text, or a black pool behind black text, is not a
-weaker treatment — it is an eraser. (`text_relief` is still white-only; it is
-the manual knob, and it was written for dark text.)
+muted state uses the shared `disabled_color`. A black pool behind black text
+is not a weaker treatment — it is an eraser. (That `disabled_color` is a light
+red as of 2026-08-28, chosen so the muted run keeps the same dark pool as
+every other bubble instead of inverting to a light one.)
A window covering part of a segment is measured too — the compositor reads
that window's own content over the overlapping strip and blends it with the
diff --git a/src/config.rs b/src/config.rs
index 5daa196..8b16381 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -191,32 +191,14 @@ pub(crate) fn read_text_raise_from_config() -> f32 {
cfg_f32("/module/text_raise").unwrap_or(0.0)
}
-/// `module { text_relief }` — letterpress underlay strength (0 = off, 1 =
-/// opaque): each module text run gets a white copy offset ~0.75px down-right
-/// BENEATH the glyphs, so dark text keeps a lit edge on dark backdrops (the
-/// engraved-text treatment, matching the DE's relief language). Bar-side only.
-pub(crate) fn read_text_relief_from_config() -> f32 {
- cfg_f32("/module/text_relief").unwrap_or(0.0).clamp(0.0, 1.0)
-}
-
-/// `module { text_halo }` — full white outline strength (0 = off, 1 = opaque):
-/// four translucent white copies at the diagonal offsets ±0.75px around each
-/// module text run, a true halo readable over any backdrop. When set it
-/// replaces the single-offset `text_relief` underlay.
-pub(crate) fn read_text_halo_from_config() -> f32 {
- cfg_f32("/module/text_halo").unwrap_or(0.0).clamp(0.0, 1.0)
-}
-
/// `module { text_contrast }` — adaptive contrast strength (0 = off, the
-/// default; 1 = full). When set, the compositor's per-segment `backdrop`
-/// measurement drives the halo instead of `text_halo`'s fixed strength: the
-/// outline appears only over a backdrop the configured text color cannot
-/// carry, and its strength tracks how badly it is losing. Bar-side only.
+/// default; 1 = full). The compositor's per-segment `backdrop` measurement
+/// drives the scrim through it: the ground darkens only as far as a backdrop
+/// the configured text color cannot carry demands, and its strength tracks
+/// how badly the text is losing. Bar-side only.
///
-/// This is the knob that makes `text_relief`/`text_halo` unnecessary rather
-/// than a constant tax — those stay as the manual override for a session
-/// with no backdrop feed (an older compositor), which is why they are not
-/// deprecated by it.
+/// On its own it makes the scrim appear only when the backdrop earns it;
+/// alongside `module { text_scrim }` it deepens that resting ground.
pub(crate) fn read_text_contrast_from_config() -> f32 {
cfg_f32("/module/text_contrast").unwrap_or(0.0).clamp(0.0, 1.0)
}
diff --git a/src/main.rs b/src/main.rs
index f758cb7..7a1de19 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -145,7 +145,7 @@ fn contrast_deficit(text: f32, bg: f32) -> f32 {
((AA - contrast_ratio(text, bg)) / (AA - 1.0)).clamp(0.0, 1.0)
}
-/// How much outline text of luminance `text_luma` needs over a backdrop
+/// How much help text of luminance `text_luma` needs over a backdrop
/// measured as `(luma, spread)`, 0 (none) to 1 (as much as the knob allows).
///
/// Contrast is checked at BOTH ends of the spread as well as at the mean, and
@@ -154,7 +154,7 @@ fn contrast_deficit(text: f32, bg: f32) -> f32 {
/// unreadable over one of the two halves. Checking only the mean is the
/// mistake that would make an adaptive scheme look broken exactly where the
/// fixed one already worked.
-fn halo_demand(text_luma: f32, (luma, spread): (u8, u8)) -> f32 {
+fn contrast_demand(text_luma: f32, (luma, spread): (u8, u8)) -> f32 {
let mid = luma as f32 / 100.0;
let half = (spread as f32 / 100.0) / 2.0;
let lo = (mid - half).clamp(0.0, 1.0);
@@ -372,15 +372,10 @@ struct StatusApp {
/// first so module content sits on the drop.
droplet_boxes: Vec<(f32, f32, f32, f32, [f32; 4])>,
droplet: Option<cce_ui::scene::paint::DropletSpec>,
- /// Letterpress underlay strength for module text (0 = off) — see
- /// `read_text_relief_from_config`.
- text_relief: f32,
- /// Full white outline strength (0 = off); beats `text_relief` when set —
- /// see `read_text_halo_from_config`.
- text_halo: f32,
/// Adaptive-contrast strength (0 = off) — see
- /// `read_text_contrast_from_config`. When on it supersedes the two fixed
- /// knobs above, driving the halo from the measured backdrop instead.
+ /// `read_text_contrast_from_config`. Deepens the scrim as the measured
+ /// backdrop demands more; on its own (no `text_scrim`) it makes the scrim
+ /// appear only when it is needed.
text_contrast: f32,
/// The compositor's last `backdrop` push for this segment: (luma,
/// spread), both 0-100. Starts at the worst case, so a segment that
@@ -397,11 +392,11 @@ struct StatusApp {
/// Feather distance for that pool, logical px; None derives it from the
/// box height.
text_scrim_feather: Option<f32>,
- /// The halo strength actually being painted, eased toward the backdrop's
- /// demand in `tick`. Stepping straight to the target makes the outline
- /// snap on and off as the desktop pans under a segment, which reads as a
- /// flicker rather than as an adaptation.
- halo_now: f32,
+ /// The contrast demand actually in effect, eased toward the backdrop's
+ /// in `tick`. Stepping straight to the target makes the scrim pulse as
+ /// the desktop pans under a segment, which reads as a flicker rather than
+ /// as an adaptation.
+ contrast_now: f32,
text_prims: Vec<TextPrim>,
scale_factor: f64,
@@ -454,7 +449,7 @@ impl StatusApp {
status_app_id(selected)
}
- /// The halo strength this segment's measured backdrop calls for, 0-1.
+ /// The contrast help this segment's measured backdrop calls for, 0-1.
///
/// The compositor reports a mean luminance and a spread. Contrast is
/// checked at BOTH ends of that spread as well as at the mean, and the
@@ -463,11 +458,11 @@ impl StatusApp {
/// is still unreadable over one of the two halves. Checking only the mean
/// is the mistake that makes an adaptive scheme look broken exactly where
/// a fixed one already worked.
- fn backdrop_halo_target(&self) -> f32 {
+ fn backdrop_contrast_demand(&self) -> f32 {
if self.text_contrast <= 0.0 {
return 0.0;
}
- halo_demand(self.text_luma, self.backdrop) * self.text_contrast
+ contrast_demand(self.text_luma, self.backdrop) * self.text_contrast
}
fn is_vertical(&self) -> bool {
@@ -530,8 +525,6 @@ impl StatusApp {
self.box_bevel_depth = read_status_box_bevel_depth_from_config();
self.droplet = read_droplet_from_config();
self.droplet_boxes.clear();
- self.text_relief = read_text_relief_from_config();
- self.text_halo = read_text_halo_from_config();
self.text_contrast = read_text_contrast_from_config();
self.text_scrim = read_text_scrim_from_config();
self.text_scrim_feather = read_text_scrim_feather_from_config();
@@ -1308,14 +1301,12 @@ impl cce_ui::engine::Application for StatusApp {
rounded_boxes: Vec::new(),
droplet_boxes: Vec::new(),
droplet: None,
- text_relief: 0.0,
- text_halo: 0.0,
text_contrast: 0.0,
backdrop: (50, 100),
text_luma: 0.0,
text_scrim: 0.0,
text_scrim_feather: None,
- halo_now: 0.0,
+ contrast_now: 0.0,
text_prims: Vec::new(),
scale_factor: 1.0,
width: if selected_module.is_some() { 120 } else { 1920 },
@@ -1497,20 +1488,20 @@ impl cce_ui::engine::Application for StatusApp {
}
fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
- // Ease the halo toward what the backdrop currently demands. The
- // measurement itself is quantized and only pushed on change, so this
- // is the only thing standing between a camera pan and the outline
- // strobing on the cell edges it crosses.
+ // Ease toward what the backdrop currently demands. The measurement
+ // itself is quantized and only pushed on change, so this is the only
+ // thing standing between a camera pan and the scrim pulsing on the
+ // cell edges it crosses.
if self.text_contrast > 0.0 {
- const HALO_EASE_S: f32 = 0.12;
- let target = self.backdrop_halo_target();
- if (target - self.halo_now).abs() > 0.002 {
- let step = (dt / HALO_EASE_S).clamp(0.0, 1.0);
- self.halo_now += (target - self.halo_now) * step;
+ const CONTRAST_EASE_S: f32 = 0.12;
+ let target = self.backdrop_contrast_demand();
+ if (target - self.contrast_now).abs() > 0.002 {
+ let step = (dt / CONTRAST_EASE_S).clamp(0.0, 1.0);
+ self.contrast_now += (target - self.contrast_now) * step;
*needs_rebuild = true;
self.needs_rebuild = true;
- } else if self.halo_now != target {
- self.halo_now = target;
+ } else if self.contrast_now != target {
+ self.contrast_now = target;
*needs_rebuild = true;
self.needs_rebuild = true;
}
@@ -1641,11 +1632,13 @@ impl cce_ui::engine::Application for StatusApp {
// through its core rect and falling off across `reach`, so insetting
// the core by exactly the feather lands the gradient's outer edge on
// the box's own edge.
- if self.text_scrim > 0.0 {
+ if self.text_scrim > 0.0 || self.text_contrast > 0.0 {
// Rests at the configured opacity and deepens with the measured
- // demand; `halo_now` is already zero when text_contrast is off,
- // so without that knob this is a constant.
- let alpha = scrim_alpha(self.text_scrim, self.halo_now);
+ // demand. Either knob alone is meaningful: `text_scrim` with no
+ // `text_contrast` is a constant ground (the demand stays zero),
+ // and `text_contrast` with no `text_scrim` is a ground that
+ // appears only when the backdrop earns it.
+ let alpha = scrim_alpha(self.text_scrim, self.contrast_now);
let feather_cfg = self.text_scrim_feather;
let runs = &self.text_prims;
let pool_in = |pc: &mut cce_ui::scene::paint::PaintCtx, bx: f32, by: f32, bw: f32, bh: f32, radius: f32| {
@@ -1700,36 +1693,11 @@ impl cce_ui::engine::Application for StatusApp {
for (text, tsize, x, y, color, font, bounds, layout, _run_w) in &self.text_prims {
match layout {
Some(l) => pc.text_boxed(text.clone(), *x, *y, *tsize, *color, font.clone(), *bounds, cce_ui::scene::paint::TextAttrs::default(), *l),
- None => {
- // With `module { text_contrast }` on, the measured
- // backdrop drives the outline and the fixed knobs step
- // aside; with it off, they rule exactly as before.
- // The scrim, when configured, IS the treatment — an
- // outline on top of a darkened ground is two answers to
- // one question.
- let halo = if self.text_scrim > 0.0 {
- 0.0
- } else if self.text_contrast > 0.0 {
- self.halo_now
- } else {
- self.text_halo
- };
- if halo > 0.0 {
- // Full halo: four diagonal white copies — a true
- // outline, readable over any backdrop.
- for (dx, dy) in [(-0.75, -0.75), (0.75, -0.75), (-0.75, 0.75), (0.75, 0.75)] {
- let hc = treatment_rgb(*color);
- let hrgb = [(hc[0] * 255.0) as u8, (hc[1] * 255.0) as u8, (hc[2] * 255.0) as u8];
- pc.text_faded(text.clone(), *x + dx, *y + dy, *tsize, hrgb, halo, font.clone(), *bounds);
- }
- } else if self.text_scrim <= 0.0 && self.text_contrast <= 0.0 && self.text_relief > 0.0 {
- // Letterpress underlay: a translucent white copy offset
- // down-right BENEATH the glyphs — dark text keeps a lit
- // edge on dark backdrops (the engraved-text treatment).
- pc.text_faded(text.clone(), *x + 0.75, *y + 0.75, *tsize, [255, 255, 255], self.text_relief, font.clone(), *bounds);
- }
- pc.text_with(text.clone(), *x, *y, *tsize, *color, font.clone(), *bounds)
- }
+ // Glyphs are drawn plain. Contrast is the scrim's job now —
+ // it darkens the ground rather than decorating the
+ // letterforms, and the two together were always one treatment
+ // too many.
+ None => pc.text_with(text.clone(), *x, *y, *tsize, *color, font.clone(), *bounds),
}
}
@@ -2223,22 +2191,22 @@ mod tests {
fn dark_text_on_a_light_uniform_backdrop_wants_no_halo() {
// The case that must stay untouched: the bar already reads fine, so
// an adaptive scheme that decorates it anyway is worse than nothing.
- assert_eq!(halo_demand(BLACK_TEXT, (100, 0)), 0.0);
+ assert_eq!(contrast_demand(BLACK_TEXT, (100, 0)), 0.0);
}
#[test]
fn dark_text_on_a_dark_uniform_backdrop_wants_a_full_halo() {
// Black text over a black grid cell — invisible, and the whole
// reason for the feature.
- assert_eq!(halo_demand(BLACK_TEXT, (0, 0)), 1.0);
+ assert_eq!(contrast_demand(BLACK_TEXT, (0, 0)), 1.0);
}
#[test]
fn light_text_reverses_the_verdict() {
// The decision is about the CONFIGURED color, not a hardcoded
// assumption that module text is dark.
- assert_eq!(halo_demand(WHITE_TEXT, (0, 0)), 0.0);
- assert_eq!(halo_demand(WHITE_TEXT, (100, 0)), 1.0);
+ assert_eq!(contrast_demand(WHITE_TEXT, (0, 0)), 0.0);
+ assert_eq!(contrast_demand(WHITE_TEXT, (100, 0)), 1.0);
}
#[test]
@@ -2247,7 +2215,7 @@ mod tests {
// fine" while the text is invisible over one half. Checking the
// spread's ends is what catches it.
let mean_only = contrast_deficit(BLACK_TEXT, 0.5);
- let with_spread = halo_demand(BLACK_TEXT, (50, 100));
+ let with_spread = contrast_demand(BLACK_TEXT, (50, 100));
assert!(with_spread > mean_only, "{} !> {}", with_spread, mean_only);
assert_eq!(with_spread, 1.0);
}
@@ -2256,7 +2224,7 @@ mod tests {
fn an_unknown_backdrop_is_treated_as_the_worst_case() {
// What a segment reports before it has heard from the compositor,
// and what an occluding window resolves to: assume unreadable.
- assert_eq!(halo_demand(BLACK_TEXT, (50, 100)), 1.0);
+ assert_eq!(contrast_demand(BLACK_TEXT, (50, 100)), 1.0);
}
const BLACK: [f32; 3] = [0.0, 0.0, 0.0];
@@ -2375,7 +2343,7 @@ mod tests {
for line in ["unknown", "", "42", "nonsense here", "42 spread"] {
assert_eq!(crate::listeners::parse_backdrop(line), (50, 100), "line {:?}", line);
}
- assert_eq!(halo_demand(BLACK_TEXT, crate::listeners::parse_backdrop("unknown")), 1.0);
+ assert_eq!(contrast_demand(BLACK_TEXT, crate::listeners::parse_backdrop("unknown")), 1.0);
}
#[test]