status bar
git clone https://git.lucas.co/cce-status-interface.git
feat: droplet module style — module { droplet } spec string
Module boxes (and the expanded in-surface menu box, which the drop
grows into) render as cce-ui's new water-droplet material
(Prim::Droplet, shader mode 10) when the app config carries
module { droplet "k=v ..." } — presence enables it, an empty string
takes every DropletSpec default, and the k=v pairs map onto the spec's
fields with warn-and-skip on unknown keys. Drops inset 1px from the
surface bottom so the belly silhouette and its AA feather stay inside
the buffer. The window module's no-focus chip keeps the rounded-box
look for now.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/config.rs | 36 ++++++++++++++++++++++
src/main.rs | 99 ++++++++++++++++++++++++++++++++++++++++-------------------
2 files changed, 104 insertions(+), 31 deletions(-)
diff --git a/src/config.rs b/src/config.rs
index da487b3..9a9ce1e 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -172,6 +172,42 @@ pub(crate) fn read_status_box_background_color_from_config() -> Option<[f32; 4]>
Some(color)
}
+/// `module { droplet "sag=0.45 belly=0.75 gleam=1.2 ..." }` — the water-drop
+/// module style ([`cce_ui::scene::paint::Prim::Droplet`]). The key's PRESENCE
+/// turns the style on (an empty string takes every default); its value is
+/// whitespace-separated `k=v` pairs onto [`DropletSpec`]'s fields, in the DE's
+/// spec-string idiom. Unknown keys warn and are skipped, so a typo shows up in
+/// the log instead of silently reverting one knob.
+pub(crate) fn read_droplet_from_config() -> Option<cce_ui::scene::paint::DropletSpec> {
+ let raw = cfg_string("/module/droplet")?;
+ let mut spec = cce_ui::scene::paint::DropletSpec::default();
+ for tok in raw.split_whitespace() {
+ let Some((key, val)) = tok.split_once('=') else {
+ log::warn!("module.droplet: token '{}' is not k=v — skipped", tok);
+ continue;
+ };
+ let Ok(v) = val.parse::<f32>() else {
+ log::warn!("module.droplet: '{}' has a non-numeric value — skipped", tok);
+ continue;
+ };
+ match key {
+ "sag" => spec.sag = v,
+ "belly" => spec.belly = v,
+ "belly_w" => spec.belly_w = v,
+ "blend" => spec.blend = v,
+ "sheet_r" => spec.sheet_r = v,
+ "clarity" => spec.clarity = v,
+ "dome" => spec.dome = v,
+ "band" => spec.band = v,
+ "gleam" => spec.gleam = v,
+ "shine" => spec.shine = v,
+ "rim" => spec.rim = v,
+ _ => log::warn!("module.droplet: unknown key '{}' — skipped", key),
+ }
+ }
+ Some(spec)
+}
+
pub(crate) fn read_status_box_corner_radius_from_config() -> f32 {
// App-native ONLY (~/.config/cce/cce-status-interface/config.kdl,
// merged over the shared config by cce-ui): `module { corner_radius }`.
diff --git a/src/main.rs b/src/main.rs
index 91bd209..8f2bae3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -255,6 +255,11 @@ struct StatusApp {
rects: Vec<RectWidget>,
overlay_rects: Vec<RectWidget>,
rounded_boxes: Vec<RoundedBox>,
+ /// Module boxes drawn as water droplets instead of `rounded_boxes` entries
+ /// when `module { droplet }` is configured — (x, y, w, h, color), painted
+ /// first so module content sits on the drop.
+ droplet_boxes: Vec<(f32, f32, f32, f32, [f32; 4])>,
+ droplet: Option<cce_ui::scene::paint::DropletSpec>,
text_prims: Vec<TextPrim>,
scale_factor: f64,
@@ -360,6 +365,8 @@ impl StatusApp {
let status_box_radius = read_status_box_corner_radius_from_config();
self.box_bevel = read_status_box_bevel_from_config();
self.box_bevel_depth = read_status_box_bevel_depth_from_config();
+ self.droplet = read_droplet_from_config();
+ self.droplet_boxes.clear();
self.status_bar.set_rect(0.0, 0.0, self.width as f32, self.height as f32);
// The surface itself is transparent: every StatusApp is a single
@@ -395,16 +402,23 @@ impl StatusApp {
// the module box GROWS into the menu, it doesn't sit atop it.
if !module.has_custom_background(&self.title) && self.context_menu.is_none() {
if let Some(color) = box_bg_color {
- self.rounded_boxes.push(RoundedBox {
- x: left_x,
- y: 0.0,
- w,
- h: bar_h,
- radius: status_box_radius,
- color,
- corners: if self.selected_module_name.is_some() { (true, true, true, true) } else { (false, false, true, true) },
- border: None,
- });
+ if self.droplet.is_some() {
+ // 1px inset from the surface bottom: the belly's
+ // silhouette (and its AA feather) must live inside
+ // the buffer, or the drop bottom cuts off flat.
+ self.droplet_boxes.push((left_x, 0.0, w, bar_h - 1.0, color));
+ } else {
+ self.rounded_boxes.push(RoundedBox {
+ x: left_x,
+ y: 0.0,
+ w,
+ h: bar_h,
+ radius: status_box_radius,
+ color,
+ corners: if self.selected_module_name.is_some() { (true, true, true, true) } else { (false, false, true, true) },
+ border: None,
+ });
+ }
}
}
@@ -473,16 +487,21 @@ impl StatusApp {
// box for the unified expanded box.
if !module.has_custom_background(&self.title) && self.context_menu.is_none() {
if let Some(color) = box_bg_color {
- self.rounded_boxes.push(RoundedBox {
- x: right_x,
- y: 0.0,
- w,
- h: bar_h,
- radius: status_box_radius,
- color,
- corners: if self.selected_module_name.is_some() { (true, true, true, true) } else { (false, false, true, true) },
- border: None,
- });
+ if self.droplet.is_some() {
+ // Same 1px bottom inset as the left loop.
+ self.droplet_boxes.push((right_x, 0.0, w, bar_h - 1.0, color));
+ } else {
+ self.rounded_boxes.push(RoundedBox {
+ x: right_x,
+ y: 0.0,
+ w,
+ h: bar_h,
+ radius: status_box_radius,
+ color,
+ corners: if self.selected_module_name.is_some() { (true, true, true, true) } else { (false, false, true, true) },
+ border: None,
+ });
+ }
}
}
@@ -669,17 +688,25 @@ impl StatusApp {
// the strip band and the menu — the module box literally
// grows into the menu. Inserted at the front so the
// module's strip content (tray icons, labels)
- // renders on top of its band.
- self.rounded_boxes.insert(0, RoundedBox {
- x: plate_x,
- y: 0.0,
- w: anim_w,
- h: bar_h + reveal_h,
- radius: status_box_radius.max(4.0),
- color: box_bg_color.unwrap_or([0.055, 0.055, 0.075, 0.97]),
- corners: (true, true, true, true),
- border: None,
- });
+ // renders on top of its band. In droplet style the drop
+ // itself grows: the belly follows the expanding bottom
+ // edge, which is the metaball merge the smin silhouette
+ // gives for free.
+ let menu_color = box_bg_color.unwrap_or([0.055, 0.055, 0.075, 0.97]);
+ if self.droplet.is_some() {
+ self.droplet_boxes.push((plate_x, 0.0, anim_w, bar_h + reveal_h - 1.0, menu_color));
+ } else {
+ self.rounded_boxes.insert(0, RoundedBox {
+ x: plate_x,
+ y: 0.0,
+ w: anim_w,
+ h: bar_h + reveal_h,
+ radius: status_box_radius.max(4.0),
+ color: menu_color,
+ corners: (true, true, true, true),
+ border: None,
+ });
+ }
let text_u8 = [
(normal_color[0] * 255.0) as u8,
@@ -1087,6 +1114,8 @@ impl cce_ui::engine::Application for StatusApp {
rects: Vec::new(),
overlay_rects: Vec::new(),
rounded_boxes: Vec::new(),
+ droplet_boxes: Vec::new(),
+ droplet: None,
text_prims: Vec::new(),
scale_factor: 1.0,
width: if selected_module.is_some() { 120 } else { 1920 },
@@ -1306,6 +1335,14 @@ impl cce_ui::engine::Application for StatusApp {
}
let mut pc = cce_ui::scene::paint::PaintCtx::new();
+ // Droplet-style module boxes paint first, so any remaining rounded
+ // boxes (module-internal chips) and all content sit on the drops.
+ if let Some(spec) = self.droplet {
+ for &(x, y, w, h, color) in &self.droplet_boxes {
+ pc.droplet(Rect { x, y, width: w, height: h }, color, spec);
+ }
+ }
+
for rb in &self.rounded_boxes {
let rect = Rect { x: rb.x, y: rb.y, width: rb.w, height: rb.h };
// Same positional corner→radius mapping the RoundedRect prim uses.