git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commitc9f7c199ed27977d97578419e3a5ec52f4d771fd
parenta7fb9c4fc1
authorLucas Galante <[email protected]>
date2026-07-12 00:46
refactor: own the page ScrollBar (verbatim from deleted cce-ui type, Phase 6av)

Settings was the last ScrollBar consumer — the dissolved Page subtree's
page scrollbar, rendered via collect_window_child and evented through
dispatch_page_event. Verbatim copy (minus the parent-Page scroll
write-back that died with Page); cce-ui deletes the type. A/B on the
processes page: diff = live process rows only; the bar's thumb still
tracks the pointer on click.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018u7qTwzX95dd5ysAkaSCLk

 src/main.rs       |   5 +-
 src/scroll_bar.rs | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 204 insertions(+), 2 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 4b00615..c242ffc 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,6 +5,7 @@ use cce_settings::app::{AppAction, AppState};
 use cce_settings::pages::{self, Page};
 mod input_handler;
 mod renderer;
+mod scroll_bar;
 
 fn make_text_buffer_with_font(
     fs: &mut FontSystem,
@@ -87,7 +88,7 @@ struct SystemInterface {
     // scroll is scroll_y/max_scroll_y, and the page scrollbar is this app-owned widget
     // (rendered into the window assembly, evented directly). content_h feeds it — the
     // window pass reads last frame's value, exactly as the legacy Page did.
-    page_scroll_bar: cce_ui::widget::ScrollBar,
+    page_scroll_bar: crate::scroll_bar::ScrollBar,
     content_h: f32,
     // Root Backplate + StatusBar DISSOLVED (Phase 6s): the window plate and the status
     // bar are emitted as tuples in rebuild_layout; this is the bar's text.
@@ -176,7 +177,7 @@ impl cce_ui::engine::Application for SystemInterface {
             last_scroll_y: 0.0,
             focused_section: None,
             page_dropdown,
-            page_scroll_bar: cce_ui::widget::ScrollBar::new(),
+            page_scroll_bar: crate::scroll_bar::ScrollBar::new(),
             content_h: 0.0,
             status_text: String::new(),
             sans_serif_family: sans_family,
diff --git a/src/scroll_bar.rs b/src/scroll_bar.rs
new file mode 100644
index 0000000..30e9a49
--- /dev/null
+++ b/src/scroll_bar.rs
@@ -0,0 +1,201 @@
+//! App-owned copy of the dissolved cce-ui `ScrollBar` (Phase 6av): settings is the last
+//! consumer — the page scrollbar of the dissolved Page subtree, rendered into the window
+//! assembly (`collect_window_child`) and evented directly (`dispatch_page_event` feeds it
+//! through `propagate_event`). Verbatim from cce-ui, minus the parent-Page scroll
+//! write-back that died with Page.
+
+use cce_ui::widget::*;
+use cce_ui::context::UiContext;
+
+pub struct ScrollBar {
+    pub base: Widget,
+    pub scroll_y: f32,
+    pub content_h: f32,
+    pub viewport_h: f32,
+    pub dragging: bool,
+    pub parent: Option<*mut (dyn Element + 'static)>,
+    pub children: Vec<*mut (dyn Element + 'static)>,
+}
+
+impl std::fmt::Debug for ScrollBar {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("ScrollBar")
+            .field("base", &self.base)
+            .field("scroll_y", &self.scroll_y)
+            .field("content_h", &self.content_h)
+            .field("viewport_h", &self.viewport_h)
+            .field("dragging", &self.dragging)
+            .finish()
+    }
+}
+
+impl Clone for ScrollBar {
+    fn clone(&self) -> Self {
+        Self {
+            base: self.base.clone(),
+            scroll_y: self.scroll_y,
+            content_h: self.content_h,
+            viewport_h: self.viewport_h,
+            dragging: self.dragging,
+            parent: self.parent,
+            children: self.children.clone(),
+        }
+    }
+}
+
+impl ScrollBar {
+    pub fn new() -> Self {
+        let mut base = Widget::new();
+        base.w = 6.0;
+        Self {
+            base,
+            scroll_y: 0.0,
+            content_h: 0.0,
+            viewport_h: 0.0,
+            dragging: false,
+            parent: None,
+            children: Vec::new(),
+        }
+    }
+
+    pub fn update(&mut self, scroll_y: f32, content_h: f32, viewport_h: f32) {
+        self.scroll_y = scroll_y;
+        self.content_h = content_h;
+        self.viewport_h = viewport_h;
+    }
+
+    pub fn get_thumb_rect(&self) -> Option<(f32, f32, f32, f32)> {
+        if self.content_h <= self.viewport_h || self.viewport_h <= 0.0 || self.base.h <= 0.0 {
+            return None;
+        }
+        let sb_x = self.base.x;
+        let sb_w = self.base.w;
+        let sb_track_h = self.base.h;
+        let sb_track_y = self.base.y;
+
+        let visible_ratio = self.viewport_h / self.content_h;
+        let thumb_h = if sb_track_h <= 20.0 {
+            sb_track_h
+        } else {
+            (sb_track_h * visible_ratio).clamp(20.0, sb_track_h)
+        };
+        let max_scroll = (self.content_h - self.viewport_h).max(0.0);
+        let scroll_ratio = if max_scroll > 0.0 { self.scroll_y / max_scroll } else { 0.0 };
+        let thumb_y = sb_track_y + scroll_ratio * (sb_track_h - thumb_h);
+
+        Some((sb_x, thumb_y, sb_w, thumb_h))
+    }
+}
+
+impl Element for ScrollBar {
+    cce_ui::impl_widget_base!(ScrollBar);
+
+    fn color(&self) -> [f32; 4] {
+        [0.0, 0.0, 0.0, 0.0]
+    }
+
+    fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
+        if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
+            return false;
+        }
+        let hit_margin = 6.0;
+        px >= self.base.x - hit_margin && px <= self.base.x + self.base.w + hit_margin && py >= self.base.y && py <= self.base.y + self.base.h
+    }
+
+    fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        let mut changed = false;
+        let is_hit = self.hit_test(px, py, ctx);
+        if self.base.hovered != is_hit {
+            self.base.hovered = is_hit;
+            changed = true;
+        }
+
+        if self.dragging {
+            if let Some((_, _, _, thumb_h)) = self.get_thumb_rect() {
+                let sb_track_y = self.base.y;
+                let sb_track_h = self.base.h;
+                let track_scroll_range = sb_track_h - thumb_h;
+                if track_scroll_range > 0.0 {
+                    let mouse_y_in_track = (py - sb_track_y).clamp(0.0, sb_track_h);
+                    let scroll_ratio = (mouse_y_in_track - thumb_h / 2.0) / track_scroll_range;
+                    let max_scroll = (self.content_h - self.viewport_h).max(0.0);
+                    let new_scroll_y = (scroll_ratio.clamp(0.0, 1.0) * max_scroll).clamp(0.0, max_scroll);
+                    if (self.scroll_y - new_scroll_y).abs() > 0.01 {
+                        self.scroll_y = new_scroll_y;
+                        changed = true;
+                    }
+                }
+            }
+        }
+        changed
+    }
+
+    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        if button == MouseButton::Left {
+            match state {
+                ElementState::Pressed => {
+                    if self.hit_test(px, py, ctx) {
+                        self.dragging = true;
+                        self.on_cursor_moved(px, py, ctx);
+                        return true;
+                    }
+                }
+                ElementState::Released => {
+                    if self.dragging {
+                        self.dragging = false;
+                        return true;
+                    }
+                }
+            }
+        }
+        false
+    }
+
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let mut quads = Vec::new();
+        if self.content_h > self.viewport_h && self.base.h > 0.0 {
+            quads.push((self.base.x, self.base.y, self.base.w, self.base.h, [0.15, 0.15, 0.20, 0.3]));
+
+            if let Some((sb_x, thumb_y, sb_w, thumb_h)) = self.get_thumb_rect() {
+                let thumb_color = if self.dragging {
+                    [0.70, 0.70, 0.75, 0.6]
+                } else if self.base.hovered {
+                    [0.65, 0.65, 0.70, 0.5]
+                } else {
+                    [0.60, 0.60, 0.65, 0.4]
+                };
+                quads.push((sb_x, thumb_y, sb_w, thumb_h, thumb_color));
+            }
+        }
+        quads
+    }
+
+    fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
+        self.parent
+    }
+
+    fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
+        self.parent = parent;
+    }
+
+    fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
+        self.children.clone()
+    }
+
+    fn add_child(&mut self, child: *mut (dyn Element + 'static), _ctx: &mut UiContext) {
+        self.children.push(child);
+    }
+
+    fn clear_children(&mut self, _ctx: &mut UiContext) {
+        self.children.clear();
+    }
+}
+
+impl Drop for ScrollBar {
+    fn drop(&mut self) {
+        clear_widget_references(self);
+    }
+}
+
+unsafe impl Send for ScrollBar {}
+unsafe impl Sync for ScrollBar {}