Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-widgets/src/scroll_view.rs
StratoSDK / crates / strato-widgets / src / scroll_view.rs
1use crate::prelude::*;
2use strato_core::event::{Event, EventResult, MouseEvent};
3use strato_core::layout::{Constraints, Layout, Size};
4use strato_core::types::{Color, Point, Rect, Transform};
5use strato_renderer::batch::RenderBatch;
6 
7use crate::widget::BaseWidget;
8 
9#[derive(Debug)]
10pub struct ScrollView {
11 base: BaseWidget,
12 child: Box<dyn Widget>,
13 offset: Point,
14 content_size: Size,
15 viewport_size: Size,
16 
17 // Interaction state
18 bounds: strato_core::state::Signal<Rect>,
19 scrollbar_rect: strato_core::state::Signal<Rect>,
20 is_dragging: bool,
21 drag_start_y: f32,
22 offset_start_y: f32,
23}
24 
25impl ScrollView {
26 pub fn new(child: impl Widget + 'static) -> Self {
27 Self {
28 base: BaseWidget::new(),
29 child: Box::new(child),
30 offset: Point::new(0.0, 0.0),
31 content_size: Size::zero(),
32 viewport_size: Size::zero(),
33 bounds: strato_core::state::Signal::new(Rect::new(0.0, 0.0, 0.0, 0.0)),
34 scrollbar_rect: strato_core::state::Signal::new(Rect::new(0.0, 0.0, 0.0, 0.0)),
35 is_dragging: false,
36 drag_start_y: 0.0,
37 offset_start_y: 0.0,
38 }
39 }
40 
41 fn update_scrollbar_rect(
42 &self,
43 content_height: f32,
44 viewport_height: f32,
45 offset_y: f32,
46 bounds: Rect,
47 ) {
48 if content_height > viewport_height {
49 let ratio = viewport_height / content_height;
50 let thumb_height = (viewport_height * ratio).max(20.0);
51 let track_height = viewport_height;
52 let max_offset = content_height - viewport_height;
53 let thumb_y = if max_offset > 0.0 {
54 (offset_y / max_offset) * (track_height - thumb_height)
55 } else {
56 0.0
57 };
58 
59 let scrollbar_width = 10.0;
60 let scrollbar_x = bounds.x + bounds.width - scrollbar_width;
61 let scrollbar_y = bounds.y + thumb_y;
62 
63 self.scrollbar_rect.set(Rect::new(
64 scrollbar_x,
65 scrollbar_y,
66 scrollbar_width,
67 thumb_height,
68 ));
69 } else {
70 self.scrollbar_rect.set(Rect::new(0.0, 0.0, 0.0, 0.0));
71 }
72 }
73}
74 
75impl Widget for ScrollView {
76 fn id(&self) -> WidgetId {
77 self.base.id()
78 }
79 
80 fn layout(&mut self, constraints: Constraints) -> Size {
81 // ScrollView takes all available space or respects max size
82 let self_size = Size::new(constraints.max_width, constraints.max_height);
83 self.viewport_size = self_size;
84 
85 // Layout child with infinite constraints
86 let child_constraints = Constraints {
87 min_width: 0.0,
88 max_width: f32::INFINITY,
89 min_height: 0.0,
90 max_height: f32::INFINITY,
91 };
92 
93 self.content_size = self.child.layout(child_constraints);
94 
95 self_size
96 }
97 
98 fn render(&self, batch: &mut RenderBatch, layout: Layout) {
99 let bounds = Rect::new(
100 layout.position.x,
101 layout.position.y,
102 layout.size.width,
103 layout.size.height,
104 );
105 self.bounds.set(bounds);
106 
107 // Update scrollbar rect
108 self.update_scrollbar_rect(
109 self.content_size.height,
110 layout.size.height,
111 self.offset.y,
112 bounds,
113 );
114 
115 // 1. Push Clip
116 batch.push_clip(bounds);
117 
118 // 2. Render child offset
119 let draw_pos = layout.position - self.offset.to_vec2();
120 
121 // We use the computed content size for the child layout
122 let child_layout = Layout::new(draw_pos, self.content_size);
123 self.child.render(batch, child_layout);
124 
125 // 3. Pop Clip
126 batch.pop_clip();
127 
128 // 4. Draw Scrollbar
129 let scrollbar = self.scrollbar_rect.get();
130 if scrollbar.width > 0.0 {
131 // Draw thumb
132 batch.add_rect(
133 scrollbar,
134 if self.is_dragging {
135 Color::rgba(0.4, 0.4, 0.4, 0.8)
136 } else {
137 Color::rgba(0.5, 0.5, 0.5, 0.5)
138 },
139 Transform::identity(),
140 );
141 }
142 }
143 
144 fn handle_event(&mut self, event: &Event) -> EventResult {
145 match event {
146 Event::MouseWheel { delta, .. } => {
147 let delta_x = delta.x;
148 let delta_y = delta.y;
149 
150 let viewport_w = self.viewport_size.width;
151 let viewport_h = self.viewport_size.height;
152 
153 let max_x = (self.content_size.width - viewport_w).max(0.0);
154 let max_y = (self.content_size.height - viewport_h).max(0.0);
155 
156 self.offset.x = (self.offset.x - delta_x).clamp(0.0, max_x);
157 self.offset.y = (self.offset.y - delta_y).clamp(0.0, max_y);
158 
159 // Update scrollbar rect immediately for responsiveness if we were running a single loop
160 // but render will handle it.
161 
162 EventResult::Handled
163 }
164 Event::MouseDown(mouse) => {
165 let point = Point::new(mouse.position.x, mouse.position.y);
166 let scrollbar = self.scrollbar_rect.get();
167 
168 if scrollbar.contains(point) {
169 self.is_dragging = true;
170 self.drag_start_y = point.y;
171 self.offset_start_y = self.offset.y;
172 return EventResult::Handled;
173 }
174 
175 self.child.handle_event(event)
176 }
177 Event::MouseMove(mouse) => {
178 if self.is_dragging {
179 let point = Point::new(mouse.position.x, mouse.position.y);
180 let delta_y = point.y - self.drag_start_y;
181 
182 let viewport_h = self.viewport_size.height;
183 let content_h = self.content_size.height;
184 
185 if content_h > viewport_h {
186 // Calculate how much offset changes per pixel of scrollbar movement
187 let track_height = viewport_h;
188 // We need the thumb height to know track range
189 let ratio = viewport_h / content_h;
190 let thumb_height = (viewport_h * ratio).max(20.0);
191 let track_range = track_height - thumb_height;
192 
193 if track_range > 0.0 {
194 let max_offset = content_h - viewport_h;
195 let offset_delta = (delta_y / track_range) * max_offset;
196 
197 self.offset.y =
198 (self.offset_start_y + offset_delta).clamp(0.0, max_offset);
199 }
200 }
201 
202 return EventResult::Handled;
203 }
204 self.child.handle_event(event)
205 }
206 Event::MouseUp(_) => {
207 if self.is_dragging {
208 self.is_dragging = false;
209 return EventResult::Handled;
210 }
211 self.child.handle_event(event)
212 }
213 _ => self.child.handle_event(event),
214 }
215 }
216 
217 fn as_any(&self) -> &dyn std::any::Any {
218 self
219 }
220 
221 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
222 self
223 }
224 
225 fn clone_widget(&self) -> Box<dyn Widget> {
226 Box::new(Self {
227 base: self.base.clone(),
228 child: self.child.clone_widget(),
229 offset: self.offset,
230 content_size: self.content_size,
231 viewport_size: self.viewport_size,
232 bounds: strato_core::state::Signal::new(self.bounds.get()),
233 scrollbar_rect: strato_core::state::Signal::new(self.scrollbar_rect.get()),
234 is_dragging: false,
235 drag_start_y: 0.0,
236 offset_start_y: 0.0,
237 })
238 }
239 
240 fn children(&self) -> Vec<&(dyn Widget + '_)> {
241 vec![self.child.as_ref()]
242 }
243 
244 fn children_mut(&mut self) -> Vec<&mut (dyn Widget + '_)> {
245 vec![self.child.as_mut()]
246 }
247}
248