git.lucas.co / cce-status-interface
status bar
git clone https://git.lucas.co/cce-status-interface.git

commit418f1fbf7981d5bd953eb433fc76dfe158666630
parent9465cdf2b1
authorLucas Galante <[email protected]>
date2026-08-06 10:01
feat: light module is an empty circle; radians value moves to its menu

The strip presence is now just an unfilled ring, bar-thickness
diameter, no module box and no text — the radians value shows as an
inert first row of the module's context menu instead. RoundedBox
grows an optional border (color, thickness): display_list draws a
marked full circle through the Arc prim (a true ring — the
rounded-rect corner family follows corner_shape, which reads as a
rounded square at half-extent radius) and any other outlined box
through the Border prim, both skipping the bevel treatment.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/main.rs    | 32 ++++++++++++++++++++++++++++++-
 src/modules.rs | 59 ++++++++++++++++++++++++++++++++++++++--------------------
 2 files changed, 70 insertions(+), 21 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 7b92402..5061102 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -275,6 +275,10 @@ pub struct RoundedBox {
     pub radius: f32,
     pub color: [f32; 4],
     pub corners: (bool, bool, bool, bool),
+    /// Some((color, thickness)): draw as an outlined shape — `color` fills
+    /// (pass transparent for an empty ring) and the stroke uses this color
+    /// and thickness. Skips the box bevel treatment.
+    pub border: Option<([f32; 4], f32)>,
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -469,6 +473,7 @@ impl StatusApp {
                             radius: status_box_radius,
                             color,
                             corners: if self.selected_module_name.is_some() { (true, true, true, true) } else { (false, false, true, true) },
+                            border: None,
                         });
                     }
                 }
@@ -552,6 +557,7 @@ impl StatusApp {
                             radius: status_box_radius,
                             color,
                             corners: if self.selected_module_name.is_some() { (true, true, true, true) } else { (false, false, true, true) },
+                            border: None,
                         });
                     }
                 }
@@ -752,6 +758,7 @@ impl StatusApp {
                         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,
                     });
 
                     let text_u8 = [
@@ -1446,6 +1453,19 @@ impl cce_ui::engine::Application for StatusApp {
                 if rb.corners.2 { rb.radius } else { 0.0 },
                 if rb.corners.3 { rb.radius } else { 0.0 },
             );
+            // Outlined shapes: no bevel treatment. A full circle draws as an
+            // Arc ring — the rounded-rect corner family is the squircle
+            // (corner_shape), which reads as a rounded SQUARE at half-extent
+            // radius — anything else as a Border prim (fill + stroke).
+            if let Some((border_color, thickness)) = rb.border {
+                if (rb.w - rb.h).abs() < 0.5 && (rb.radius - rb.w / 2.0).abs() < 0.5 {
+                    let r = rb.w / 2.0;
+                    pc.arc(rb.x + r, rb.y + r, r, thickness, 0.0, std::f32::consts::TAU, border_color);
+                } else {
+                    pc.border(rect, radii, rb.color, border_color, thickness);
+                }
+                continue;
+            }
             match self.box_bevel {
                 Some(StatusBoxBevel::Raised) => {
                     // A lit plate: fill + rolled lip in one prim.
@@ -1745,7 +1765,7 @@ impl cce_ui::engine::Application for StatusApp {
                         separator: false,
                         action: MenuRowAction::Dispatch(ev),
                     };
-                    let rows = if self.adjust_position_mode {
+                    let mut rows = if self.adjust_position_mode {
                         vec![dispatch_row("Done", CustomEvent::ToggleAdjustPositionMode)]
                     } else {
                         vec![
@@ -1756,6 +1776,16 @@ impl cce_ui::engine::Application for StatusApp {
                             dispatch_row("Adjust Positions", CustomEvent::ToggleAdjustPositionMode),
                         ]
                     };
+                    // The light module's strip presence is just the empty
+                    // circle; its value lives here in the menu.
+                    if mb.name == "light_source" {
+                        rows.insert(0, MenuRow {
+                            label: format!("{:.2} rad", modules::get_light_source_pos_from_config()),
+                            enabled: true,
+                            separator: false,
+                            action: MenuRowAction::Inert,
+                        });
+                    }
                     self.context_menu = Some(ModuleContextMenu {
                         pages: vec![MenuPage { title: mb.name.clone(), rows }],
                         page: 0,
diff --git a/src/modules.rs b/src/modules.rs
index e36d10a..4f548d2 100644
--- a/src/modules.rs
+++ b/src/modules.rs
@@ -225,6 +225,7 @@ impl StatusModule for WindowModule {
                         radius: status_box_radius,
                         color,
                         corners: (false, false, true, true),
+                        border: None,
                     });
                 }
                 crate::draw_label(text_prims, label, cur_x + padding, centered_text_y(bar_h, font_size));
@@ -828,7 +829,7 @@ impl StatusModule for TrayModule {
 
 pub struct LightSourceModule;
 
-fn get_light_source_pos_from_config() -> f32 {
+pub(crate) fn get_light_source_pos_from_config() -> f32 {
     let val = crate::get_cached_config();
     if let Some(pos_val) = crate::json_find_key(&val, "light_source_position") {
         if let Some(f) = pos_val.as_f64() {
@@ -846,39 +847,41 @@ fn get_light_source_pos_from_config() -> f32 {
 impl StatusModule for LightSourceModule {
     fn name(&self) -> &'static str { "light_source" }
 
+    // The module is just the empty circle — no module box behind it.
+    fn has_custom_background(&self, _title: &str) -> bool { true }
+
     fn width(
         &self,
         _stats: &Option<SystemStats>,
         _viewport: &str,
         _layout: &str,
         _title: &str,
-        font_system: &mut FontSystem,
-        font_family: &str,
-        font_size: f32,
+        _font_system: &mut FontSystem,
+        _font_family: &str,
+        _font_size: f32,
         _tray_items: &HashMap<String, TrayItem>,
-        padding: f32,
+        _padding: f32,
     ) -> f32 {
-        let pos = get_light_source_pos_from_config();
-        let text = format!("Light: {:.2} rad", pos);
-        let label = Label::new_with_family(font_system, &text, font_size, [0.0, 0.0, 0.0, 1.0], font_family);
-        label.w + 2.0 * padding
+        // An empty circle with the bar's own thickness as its diameter; the
+        // radians value lives in the module's menu, not the strip.
+        crate::read_status_height_from_config()
     }
 
     fn render(
         &self,
         x: f32,
-        _w: f32,
+        w: f32,
         _stats: &Option<SystemStats>,
         _viewport: &str,
         _layout: &str,
         _title: &str,
-        font_system: &mut FontSystem,
-        font_family: &str,
-        font_size: f32,
+        _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>,
+        _text_prims: &mut Vec<crate::TextPrim>,
         _rects: &mut Vec<RectWidget>,
         _overlay_rects: &mut Vec<RectWidget>,
         _viewport_bounds: &mut Vec<ViewportBounds>,
@@ -887,12 +890,28 @@ impl StatusModule for LightSourceModule {
         _tray_item_bounds: &mut Vec<TrayIconBounds>,
         _box_bg_color: Option<[f32; 4]>,
         _status_box_radius: f32,
-        _rounded_boxes: &mut Vec<RoundedBox>,
-        padding: f32,
+        rounded_boxes: &mut Vec<RoundedBox>,
+        _padding: f32,
     ) {
-        let pos = get_light_source_pos_from_config();
-        let text = format!("Light: {:.2} rad", pos);
-        let label = Label::new_with_family(font_system, &text, font_size, normal_color, font_family);
-        crate::draw_label(text_prims, label, x + padding, centered_text_y(bar_h, font_size));
+        let d = w.min(bar_h);
+        // normal_color is raw sRGB (a text color); the ring draws through the
+        // quad pipeline, which expects linear — convert so the stroke reads
+        // as the same shade as module text.
+        let ring = [
+            normal_color[0].powf(2.2),
+            normal_color[1].powf(2.2),
+            normal_color[2].powf(2.2),
+            1.0,
+        ];
+        rounded_boxes.push(RoundedBox {
+            x: x + (w - d) / 2.0,
+            y: (bar_h - d) / 2.0,
+            w: d,
+            h: d,
+            radius: d / 2.0,
+            color: [0.0, 0.0, 0.0, 0.0],
+            corners: (true, true, true, true),
+            border: Some((ring, 1.5)),
+        });
     }
 }