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     /// The bar's width: the DE's centred-bar width (it rides the page's
 38     /// centre line, over the sections, with no lane of its own).
 39     pub fn width() -> f32 {
 40         cce_ui::layout::centred_scrollbar_width()
 41     }
 42 
 43     pub fn update(&mut self, scroll_y: f32, content_h: f32, viewport_h: f32) {
 44         self.scroll_y = scroll_y;
 45         self.content_h = content_h;
 46         self.viewport_h = viewport_h;
 47     }
 48 
 49     fn overflowing(&self) -> bool {
 50         self.content_h > self.viewport_h
 51     }
 52 
 53     /// How far the fore copy has faded in, 0..=1: the raise and the sink are
 54     /// a fade over the idle copy, not a flip.
 55     pub fn fade(&self) -> f32 {
 56         self.activity.fade()
 57     }
 58 
 59     /// A scroll landed (wheel fast path, keyboard): refresh the hold and raise
 60     /// the bar in the same frame.
 61     pub fn on_scroll(&mut self) {
 62         self.activity.bump();
 63         let visible = self.overflowing();
 64         self.activity.recompute(visible, self.dragging);
 65     }
 66 
 67     /// Per-frame raise/sink upkeep; true = keep redrawing (hold running or the
 68     /// bar just flipped depth). Named apart from the `Input`/`WidgetHost` tick
 69     /// so the call through `Adapted`'s Deref can't collide.
 70     pub fn tick_activity(&mut self, dt: f32) -> bool {
 71         let holding = self.activity.holding();
 72         let visible = self.overflowing();
 73         self.activity.tick(dt, visible, self.dragging) || holding
 74     }
 75 
 76     /// The track + thumb quads for the host's two copies: the idle one under
 77     /// the window plate every frame, the fore one over the page content at
 78     /// [`Self::fade`]. The DE's shared track and thumb colours.
 79     pub fn layer_quads(&self, rect: Rect) -> Vec<(Rect, [f32; 4])> {
 80         let mut out = Vec::new();
 81         if self.content_h > self.viewport_h && rect.height > 0.0 {
 82             out.push((rect, cce_ui::color::scrollbar_track_color()));
 83             if let Some((tx, ty, tw, th)) = self.thumb_rect(rect) {
 84                 out.push((Rect { x: tx, y: ty, width: tw, height: th }, cce_ui::color::scrollbar_thumb_color()));
 85             }
 86         }
 87         out
 88     }
 89 
 90     fn thumb_rect(&self, rect: Rect) -> Option<(f32, f32, f32, f32)> {
 91         if self.content_h <= self.viewport_h || self.viewport_h <= 0.0 || rect.height <= 0.0 {
 92             return None;
 93         }
 94         let visible_ratio = self.viewport_h / self.content_h;
 95         let thumb_h = if rect.height <= 20.0 {
 96             rect.height
 97         } else {
 98             (rect.height * visible_ratio).clamp(20.0, rect.height)
 99         };
100         let max_scroll = (self.content_h - self.viewport_h).max(0.0);
101         let scroll_ratio = if max_scroll > 0.0 { self.scroll_y / max_scroll } else { 0.0 };
102         let thumb_y = rect.y + scroll_ratio * (rect.height - thumb_h);
103 
104         Some((rect.x, thumb_y, rect.width, thumb_h))
105     }
106 
107     /// Thumb-center-tracking drag scroll (legacy `on_cursor_moved`'s dragging branch).
108     /// Returns whether the scroll position changed.
109     fn drag_track(&mut self, py: f32, rect: Rect) -> bool {
110         if let Some((_, _, _, thumb_h)) = self.thumb_rect(rect) {
111             let track_scroll_range = rect.height - thumb_h;
112             if track_scroll_range > 0.0 {
113                 let mouse_y_in_track = (py - rect.y).clamp(0.0, rect.height);
114                 let scroll_ratio = (mouse_y_in_track - thumb_h / 2.0) / track_scroll_range;
115                 let max_scroll = (self.content_h - self.viewport_h).max(0.0);
116                 let new_scroll_y = (scroll_ratio.clamp(0.0, 1.0) * max_scroll).clamp(0.0, max_scroll);
117                 if (self.scroll_y - new_scroll_y).abs() > 0.01 {
118                     self.scroll_y = new_scroll_y;
119                     return true;
120                 }
121             }
122         }
123         false
124     }
125 }
126 
127 impl cce_ui::widget::Layout for ScrollBar {}
128 
129 impl cce_ui::widget::Paint for ScrollBar {
130     fn color(&self) -> [f32; 4] {
131         [0.0, 0.0, 0.0, 0.0]
132     }
133 
134     fn paint(&self, _rect: Rect, _ctx: &mut PaintCtx) {
135         // Deliberately empty: the bar straddles the window plate (sunk under it
136         // idle, over the page content while raised), so the host emits it as two
137         // possible layers in `display_list` via [`ScrollBar::layer_quads`] — a
138         // single in-tree paint could only ever sit at one depth.
139     }
140 }
141 
142 impl cce_ui::widget::Input for ScrollBar {
143     /// The legacy hit shape: ±6px horizontal grab margin, exact vertical span.
144     fn hit(&self, rect: Rect, x: f32, y: f32) -> bool {
145         let hit_margin = 6.0;
146         x >= rect.x - hit_margin
147             && x <= rect.x + rect.width + hit_margin
148             && y >= rect.y
149             && y <= rect.y + rect.height
150     }
151 
152     fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
153         match event {
154             // Presses arrive hit-gated (margin hit): grab the thumb and jump-scroll to the
155             // press point, like the legacy `mouse_input` → `on_cursor_moved` pair.
156             // Only a raised bar can be grabbed — sunk it sits behind the window
157             // plate, so the press falls through to whatever the plate carries.
158             Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, y, .. } => {
159                 if !self.activity.raised() {
160                     return false;
161                 }
162                 self.dragging = true;
163                 self.drag_track(*y, ectx.rect);
164                 true
165             }
166             // Releases arrive ungated: end the drag wherever the cursor is.
167             Event::MouseButton { button: MouseButton::Left, state: ElementState::Released, .. } => {
168                 if self.dragging {
169                     self.dragging = false;
170                     // The release starts the hold window: the bar lingers
171                     // briefly, then sinks back behind the plate.
172                     self.activity.bump();
173                     return true;
174                 }
175                 false
176             }
177             // Mid-drag moves track the thumb; non-drag moves fall through to the adapter's
178             // hover bookkeeping (which synthesizes the Enter/Leave handled below).
179             Event::PointerMove { y, .. } => {
180                 if self.dragging {
181                     return self.drag_track(*y, ectx.rect);
182                 }
183                 false
184             }
185             Event::MouseEnter => {
186                 self.hovered = true;
187                 // Hover only SUSTAINS a raised bar (recomputed in tick); it can
188                 // never raise a sunk one — the plate is what the pointer is on.
189                 self.activity.set_hover(true);
190                 true
191             }
192             Event::MouseLeave => {
193                 self.hovered = false;
194                 self.activity.set_hover(false);
195                 true
196             }
197             _ => false,
198         }
199     }
200 }