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

commitdf98b2446e0e94def2050de2c2a92e5bd4396f6e
parent5138bbeb5f
authorLucas Galante <[email protected]>
date2026-07-12 09:14
refactor: ScrollBar onto the narrow traits as Adapted<ScrollBar> (Phase 6az part 2)

The 6av app-side copy converts to Layout/Paint/Input wrapped in Adapted.
ScrollBar is now plain data: the raw-pointer parent/children fields, their
Element plumbing, the hand-rolled Drop, and the unsafe Send/Sync impls all
had zero live consumers and are gone. Constructor returns the wrapper
(6az recipe), so renderer/input_handler call sites are untouched beyond the
field type: set_rect/as_ptr_mut/propagate_event ride Element on the wrapper,
fields and update() ride Deref, collect_window_child takes the wrapper as
&dyn Element. The ±6px horizontal grab margin becomes Input::hit; press =
grab + jump-scroll (hit-gated), release ungated, mid-drag PointerMove tracks
the thumb center in on_event; hover moves to the adapter's Enter/Leave
synthesis feeding a model-local hovered flag.

A/B on the processes page: static scrollbar strip AE=0 (full-window diff =
one row of process churn); after an identical wheel + track-click sequence
on both binaries the strips are byte-identical, which also covers the hover
tint (pointer rests on the bar in both captures).

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

 src/main.rs       |   2 +-
 src/scroll_bar.rs | 239 +++++++++++++++++++++---------------------------------
 2 files changed, 93 insertions(+), 148 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index c242ffc..ff6c17d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -88,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: crate::scroll_bar::ScrollBar,
+    page_scroll_bar: cce_ui::widget::Adapted<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.
diff --git a/src/scroll_bar.rs b/src/scroll_bar.rs
index 30e9a49..bf26503 100644
--- a/src/scroll_bar.rs
+++ b/src/scroll_bar.rs
@@ -1,61 +1,32 @@
 //! 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.
+//! through `propagate_event`). Phase 6az: on the narrow traits, wrapped in
+//! `Adapted<ScrollBar>` — the constructor returns the wrapper so every call site keeps its
+//! shape (Element methods on the wrapper, fields/`update` through Deref).
 
-use cce_ui::widget::*;
-use cce_ui::context::UiContext;
+use cce_ui::scene::layout::Rect;
+use cce_ui::scene::paint::PaintCtx;
+use cce_ui::widget::{Adapted, Event, EventCtx, MouseButton, ElementState};
 
+#[derive(Debug, Clone)]
 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(),
-        }
-    }
+    hovered: bool,
 }
 
 impl ScrollBar {
-    pub fn new() -> Self {
-        let mut base = Widget::new();
-        base.w = 6.0;
-        Self {
-            base,
+    pub fn new() -> Adapted<ScrollBar> {
+        Adapted::new(Self {
             scroll_y: 0.0,
             content_h: 0.0,
             viewport_h: 0.0,
             dragging: false,
-            parent: None,
-            children: Vec::new(),
-        }
+            hovered: false,
+        })
     }
 
     pub fn update(&mut self, scroll_y: f32, content_h: f32, viewport_h: f32) {
@@ -64,138 +35,112 @@ impl ScrollBar {
         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 {
+    fn thumb_rect(&self, rect: Rect) -> Option<(f32, f32, f32, f32)> {
+        if self.content_h <= self.viewport_h || self.viewport_h <= 0.0 || rect.height <= 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
+        let thumb_h = if rect.height <= 20.0 {
+            rect.height
         } else {
-            (sb_track_h * visible_ratio).clamp(20.0, sb_track_h)
+            (rect.height * visible_ratio).clamp(20.0, rect.height)
         };
         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))
+        let thumb_y = rect.y + scroll_ratio * (rect.height - thumb_h);
+
+        Some((rect.x, thumb_y, rect.width, thumb_h))
+    }
+
+    /// Thumb-center-tracking drag scroll (legacy `on_cursor_moved`'s dragging branch).
+    /// Returns whether the scroll position changed.
+    fn drag_track(&mut self, py: f32, rect: Rect) -> bool {
+        if let Some((_, _, _, thumb_h)) = self.thumb_rect(rect) {
+            let track_scroll_range = rect.height - thumb_h;
+            if track_scroll_range > 0.0 {
+                let mouse_y_in_track = (py - rect.y).clamp(0.0, rect.height);
+                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;
+                    return true;
+                }
+            }
+        }
+        false
     }
 }
 
-impl Element for ScrollBar {
-    cce_ui::impl_widget_base!(ScrollBar);
+impl cce_ui::widget::Layout for ScrollBar {}
 
+impl cce_ui::widget::Paint for 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]));
+    fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+        if self.content_h > self.viewport_h && rect.height > 0.0 {
+            ctx.quad(rect, [0.15, 0.15, 0.20, 0.3]);
 
-            if let Some((sb_x, thumb_y, sb_w, thumb_h)) = self.get_thumb_rect() {
+            if let Some((tx, ty, tw, th)) = self.thumb_rect(rect) {
                 let thumb_color = if self.dragging {
                     [0.70, 0.70, 0.75, 0.6]
-                } else if self.base.hovered {
+                } else if self.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));
+                ctx.quad(Rect { x: tx, y: ty, width: tw, height: th }, 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);
+impl cce_ui::widget::Input for ScrollBar {
+    /// The legacy hit shape: ±6px horizontal grab margin, exact vertical span.
+    fn hit(&self, rect: Rect, x: f32, y: f32) -> bool {
+        let hit_margin = 6.0;
+        x >= rect.x - hit_margin
+            && x <= rect.x + rect.width + hit_margin
+            && y >= rect.y
+            && y <= rect.y + rect.height
+    }
+
+    fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
+        match event {
+            // Presses arrive hit-gated (margin hit): grab the thumb and jump-scroll to the
+            // press point, like the legacy `mouse_input` → `on_cursor_moved` pair.
+            Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, y, .. } => {
+                self.dragging = true;
+                self.drag_track(*y, ectx.rect);
+                true
+            }
+            // Releases arrive ungated: end the drag wherever the cursor is.
+            Event::MouseButton { button: MouseButton::Left, state: ElementState::Released, .. } => {
+                if self.dragging {
+                    self.dragging = false;
+                    return true;
+                }
+                false
+            }
+            // Mid-drag moves track the thumb; non-drag moves fall through to the adapter's
+            // hover bookkeeping (which synthesizes the Enter/Leave handled below).
+            Event::PointerMove { y, .. } => {
+                if self.dragging {
+                    return self.drag_track(*y, ectx.rect);
+                }
+                false
+            }
+            Event::MouseEnter => {
+                self.hovered = true;
+                true
+            }
+            Event::MouseLeave => {
+                self.hovered = false;
+                true
+            }
+            _ => false,
+        }
     }
 }
-
-unsafe impl Send for ScrollBar {}
-unsafe impl Sync for ScrollBar {}