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 }