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 }