system settings
git clone https://git.lucas.co/cce-system-interface.git
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 {}