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

src/scroll_bar.rs (8.1K)

  1 //! App-owned copy of the dissolved cce-ui `ScrollBar` (Phase 6av): settings is the last
  2 //! consumer — the page scrollbar of the dissolved Page subtree, rendered into the window
  3 //! assembly (`collect_window_child`) and evented directly (`dispatch_page_event` feeds it
  4 //! through `propagate_event`). Phase 6az: on the narrow traits, wrapped in
  5 //! `Adapted<ScrollBar>` — the constructor returns the wrapper so every call site keeps its
  6 //! shape (WidgetHost methods on the wrapper, fields/`update` through Deref).
  7 
  8 use cce_ui::scene::layout::Rect;
  9 use cce_ui::scene::paint::PaintCtx;
 10 use cce_ui::widget::{Adapted, Event, EventCtx, MouseButton, ElementState, ScrollbarActivity};
 11 
 12 #[derive(Debug, Clone)]
 13 pub struct ScrollBar {
 14     pub scroll_y: f32,
 15     pub content_h: f32,
 16     pub viewport_h: f32,
 17     pub dragging: bool,
 18     hovered: bool,
 19     /// The shared raise/sink hysteresis (the designer parameter-pane treatment):
 20     /// idle the bar sinks behind the translucent window plate and takes no
 21     /// input; a scroll raises it, hover sustains it, the hold decays in `tick`.
 22     activity: ScrollbarActivity,
 23 }
 24 
 25 impl ScrollBar {
 26     pub fn new() -> Adapted<ScrollBar> {
 27         Adapted::new(Self {
 28             scroll_y: 0.0,
 29             content_h: 0.0,
 30             viewport_h: 0.0,
 31             dragging: false,
 32             hovered: false,
 33             activity: ScrollbarActivity::new(),
 34         })
 35     }
 36 
 37     pub fn update(&mut self, scroll_y: f32, content_h: f32, viewport_h: f32) {
 38         self.scroll_y = scroll_y;
 39         self.content_h = content_h;
 40         self.viewport_h = viewport_h;
 41     }
 42 
 43     fn overflowing(&self) -> bool {
 44         self.content_h > self.viewport_h
 45     }
 46 
 47     /// Whether the bar currently rides in front of the content (and takes
 48     /// input) rather than idling behind the window plate.
 49     pub fn raised(&self) -> bool {
 50         self.activity.raised()
 51     }
 52 
 53     /// A scroll landed (wheel fast path, keyboard): refresh the hold and raise
 54     /// the bar in the same frame.
 55     pub fn on_scroll(&mut self) {
 56         self.activity.bump();
 57         let visible = self.overflowing();
 58         self.activity.recompute(visible, self.dragging);
 59     }
 60 
 61     /// Per-frame raise/sink upkeep; true = keep redrawing (hold running or the
 62     /// bar just flipped depth). Named apart from the `Input`/`WidgetHost` tick
 63     /// so the call through `Adapted`'s Deref can't collide.
 64     pub fn tick_activity(&mut self, dt: f32) -> bool {
 65         let holding = self.activity.holding();
 66         let visible = self.overflowing();
 67         self.activity.tick(dt, visible, self.dragging) || holding
 68     }
 69 
 70     /// The track + thumb quads for the host's two-layer emission: drawn under
 71     /// the window plate while sunk, over the page content while raised. Colors
 72     /// keep the widget's hover/drag tint.
 73     pub fn layer_quads(&self, rect: Rect) -> Vec<(Rect, [f32; 4])> {
 74         let mut out = Vec::new();
 75         if self.content_h > self.viewport_h && rect.height > 0.0 {
 76             out.push((rect, [0.15, 0.15, 0.20, 0.3]));
 77             if let Some((tx, ty, tw, th)) = self.thumb_rect(rect) {
 78                 let thumb_color = if self.dragging {
 79                     [0.70, 0.70, 0.75, 0.6]
 80                 } else if self.hovered && self.activity.raised() {
 81                     [0.65, 0.65, 0.70, 0.5]
 82                 } else {
 83                     [0.60, 0.60, 0.65, 0.4]
 84                 };
 85                 out.push((Rect { x: tx, y: ty, width: tw, height: th }, thumb_color));
 86             }
 87         }
 88         out
 89     }
 90 
 91     fn thumb_rect(&self, rect: Rect) -> Option<(f32, f32, f32, f32)> {
 92         if self.content_h <= self.viewport_h || self.viewport_h <= 0.0 || rect.height <= 0.0 {
 93             return None;
 94         }
 95         let visible_ratio = self.viewport_h / self.content_h;
 96         let thumb_h = if rect.height <= 20.0 {
 97             rect.height
 98         } else {
 99             (rect.height * visible_ratio).clamp(20.0, rect.height)
100         };
101         let max_scroll = (self.content_h - self.viewport_h).max(0.0);
102         let scroll_ratio = if max_scroll > 0.0 { self.scroll_y / max_scroll } else { 0.0 };
103         let thumb_y = rect.y + scroll_ratio * (rect.height - thumb_h);
104 
105         Some((rect.x, thumb_y, rect.width, thumb_h))
106     }
107 
108     /// Thumb-center-tracking drag scroll (legacy `on_cursor_moved`'s dragging branch).
109     /// Returns whether the scroll position changed.
110     fn drag_track(&mut self, py: f32, rect: Rect) -> bool {
111         if let Some((_, _, _, thumb_h)) = self.thumb_rect(rect) {
112             let track_scroll_range = rect.height - thumb_h;
113             if track_scroll_range > 0.0 {
114                 let mouse_y_in_track = (py - rect.y).clamp(0.0, rect.height);
115                 let scroll_ratio = (mouse_y_in_track - thumb_h / 2.0) / track_scroll_range;
116                 let max_scroll = (self.content_h - self.viewport_h).max(0.0);
117                 let new_scroll_y = (scroll_ratio.clamp(0.0, 1.0) * max_scroll).clamp(0.0, max_scroll);
118                 if (self.scroll_y - new_scroll_y).abs() > 0.01 {
119                     self.scroll_y = new_scroll_y;
120                     return true;
121                 }
122             }
123         }
124         false
125     }
126 }
127 
128 impl cce_ui::widget::Layout for ScrollBar {}
129 
130 impl cce_ui::widget::Paint for ScrollBar {
131     fn color(&self) -> [f32; 4] {
132         [0.0, 0.0, 0.0, 0.0]
133     }
134 
135     fn paint(&self, _rect: Rect, _ctx: &mut PaintCtx) {
136         // Deliberately empty: the bar straddles the window plate (sunk under it
137         // idle, over the page content while raised), so the host emits it as two
138         // possible layers in `display_list` via [`ScrollBar::layer_quads`] — a
139         // single in-tree paint could only ever sit at one depth.
140     }
141 }
142 
143 impl cce_ui::widget::Input for ScrollBar {
144     /// The legacy hit shape: ±6px horizontal grab margin, exact vertical span.
145     fn hit(&self, rect: Rect, x: f32, y: f32) -> bool {
146         let hit_margin = 6.0;
147         x >= rect.x - hit_margin
148             && x <= rect.x + rect.width + hit_margin
149             && y >= rect.y
150             && y <= rect.y + rect.height
151     }
152 
153     fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
154         match event {
155             // Presses arrive hit-gated (margin hit): grab the thumb and jump-scroll to the
156             // press point, like the legacy `mouse_input` → `on_cursor_moved` pair.
157             // Only a raised bar can be grabbed — sunk it sits behind the window
158             // plate, so the press falls through to whatever the plate carries.
159             Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, y, .. } => {
160                 if !self.activity.raised() {
161                     return false;
162                 }
163                 self.dragging = true;
164                 self.drag_track(*y, ectx.rect);
165                 true
166             }
167             // Releases arrive ungated: end the drag wherever the cursor is.
168             Event::MouseButton { button: MouseButton::Left, state: ElementState::Released, .. } => {
169                 if self.dragging {
170                     self.dragging = false;
171                     // The release starts the hold window: the bar lingers
172                     // briefly, then sinks back behind the plate.
173                     self.activity.bump();
174                     return true;
175                 }
176                 false
177             }
178             // Mid-drag moves track the thumb; non-drag moves fall through to the adapter's
179             // hover bookkeeping (which synthesizes the Enter/Leave handled below).
180             Event::PointerMove { y, .. } => {
181                 if self.dragging {
182                     return self.drag_track(*y, ectx.rect);
183                 }
184                 false
185             }
186             Event::MouseEnter => {
187                 self.hovered = true;
188                 // Hover only SUSTAINS a raised bar (recomputed in tick); it can
189                 // never raise a sunk one — the plate is what the pointer is on.
190                 self.activity.set_hover(true);
191                 true
192             }
193             Event::MouseLeave => {
194                 self.hovered = false;
195                 self.activity.set_hover(false);
196                 true
197             }
198             _ => false,
199         }
200     }
201 }