1use std::collections::HashMap;
7
8use crate::focus::WidgetId;
9use crate::geometry::Rect;
10use crate::tcss::cascade::ComputedStyle;
11use crate::tcss::property::PropertyName;
12use crate::tcss::value::CssValue;
13
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub enum OverflowBehavior {
17 #[default]
19 Visible,
20 Hidden,
22 Scroll,
24 Auto,
26}
27
28#[derive(Clone, Debug, Default, PartialEq)]
30pub struct ScrollState {
31 pub offset_x: u16,
33 pub offset_y: u16,
35 pub content_width: u16,
37 pub content_height: u16,
39 pub viewport_width: u16,
41 pub viewport_height: u16,
43}
44
45impl ScrollState {
46 pub const fn new(
48 content_width: u16,
49 content_height: u16,
50 viewport_width: u16,
51 viewport_height: u16,
52 ) -> Self {
53 Self {
54 offset_x: 0,
55 offset_y: 0,
56 content_width,
57 content_height,
58 viewport_width,
59 viewport_height,
60 }
61 }
62
63 pub const fn can_scroll_x(&self) -> bool {
65 self.content_width > self.viewport_width
66 }
67
68 pub const fn can_scroll_y(&self) -> bool {
70 self.content_height > self.viewport_height
71 }
72
73 pub fn max_offset_x(&self) -> u16 {
75 self.content_width.saturating_sub(self.viewport_width)
76 }
77
78 pub fn max_offset_y(&self) -> u16 {
80 self.content_height.saturating_sub(self.viewport_height)
81 }
82
83 pub fn visible_rect(&self) -> Rect {
85 Rect::new(
86 self.offset_x,
87 self.offset_y,
88 self.viewport_width,
89 self.viewport_height,
90 )
91 }
92}
93
94pub struct ScrollManager {
96 regions: HashMap<WidgetId, ScrollState>,
97}
98
99impl ScrollManager {
100 pub fn new() -> Self {
102 Self {
103 regions: HashMap::new(),
104 }
105 }
106
107 pub fn register(
109 &mut self,
110 widget_id: WidgetId,
111 content_width: u16,
112 content_height: u16,
113 viewport_width: u16,
114 viewport_height: u16,
115 ) {
116 self.regions.insert(
117 widget_id,
118 ScrollState::new(
119 content_width,
120 content_height,
121 viewport_width,
122 viewport_height,
123 ),
124 );
125 }
126
127 pub fn scroll_by(&mut self, widget_id: WidgetId, dx: i16, dy: i16) {
129 if let Some(state) = self.regions.get_mut(&widget_id) {
130 let new_x = i32::from(state.offset_x) + i32::from(dx);
131 let new_y = i32::from(state.offset_y) + i32::from(dy);
132 state.offset_x = clamp_offset(new_x, state.max_offset_x());
133 state.offset_y = clamp_offset(new_y, state.max_offset_y());
134 }
135 }
136
137 pub fn scroll_to(&mut self, widget_id: WidgetId, x: u16, y: u16) {
139 if let Some(state) = self.regions.get_mut(&widget_id) {
140 state.offset_x = x.min(state.max_offset_x());
141 state.offset_y = y.min(state.max_offset_y());
142 }
143 }
144
145 pub fn get(&self, widget_id: WidgetId) -> Option<&ScrollState> {
147 self.regions.get(&widget_id)
148 }
149
150 pub fn can_scroll_x(&self, widget_id: WidgetId) -> bool {
152 self.regions
153 .get(&widget_id)
154 .is_some_and(|s| s.can_scroll_x())
155 }
156
157 pub fn can_scroll_y(&self, widget_id: WidgetId) -> bool {
159 self.regions
160 .get(&widget_id)
161 .is_some_and(|s| s.can_scroll_y())
162 }
163
164 pub fn visible_rect(&self, widget_id: WidgetId) -> Option<Rect> {
166 self.regions.get(&widget_id).map(|s| s.visible_rect())
167 }
168
169 pub fn remove(&mut self, widget_id: WidgetId) {
171 self.regions.remove(&widget_id);
172 }
173}
174
175impl Default for ScrollManager {
176 fn default() -> Self {
177 Self::new()
178 }
179}
180
181pub fn extract_overflow(style: &ComputedStyle) -> (OverflowBehavior, OverflowBehavior) {
185 let base = style
186 .get(&PropertyName::Overflow)
187 .map(keyword_to_overflow)
188 .unwrap_or_default();
189 let ox = style
190 .get(&PropertyName::OverflowX)
191 .map(keyword_to_overflow)
192 .unwrap_or(base);
193 let oy = style
194 .get(&PropertyName::OverflowY)
195 .map(keyword_to_overflow)
196 .unwrap_or(base);
197 (ox, oy)
198}
199
200fn keyword_to_overflow(value: &CssValue) -> OverflowBehavior {
202 match value {
203 CssValue::Keyword(k) => match k.to_ascii_lowercase().as_str() {
204 "visible" => OverflowBehavior::Visible,
205 "hidden" => OverflowBehavior::Hidden,
206 "scroll" => OverflowBehavior::Scroll,
207 "auto" => OverflowBehavior::Auto,
208 _ => OverflowBehavior::Visible,
209 },
210 _ => OverflowBehavior::Visible,
211 }
212}
213
214fn clamp_offset(value: i32, max: u16) -> u16 {
216 if value < 0 {
217 0
218 } else if value > i32::from(max) {
219 max
220 } else {
221 value as u16
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 fn wid(n: u64) -> WidgetId {
230 n
231 }
232
233 #[test]
234 fn scroll_state_creation() {
235 let state = ScrollState::new(100, 200, 80, 24);
236 assert_eq!(state.content_width, 100);
237 assert_eq!(state.content_height, 200);
238 assert_eq!(state.viewport_width, 80);
239 assert_eq!(state.viewport_height, 24);
240 assert_eq!(state.offset_x, 0);
241 assert_eq!(state.offset_y, 0);
242 }
243
244 #[test]
245 fn scroll_state_can_scroll() {
246 let state = ScrollState::new(100, 200, 80, 24);
247 assert!(state.can_scroll_x());
248 assert!(state.can_scroll_y());
249
250 let no_scroll = ScrollState::new(40, 10, 80, 24);
251 assert!(!no_scroll.can_scroll_x());
252 assert!(!no_scroll.can_scroll_y());
253 }
254
255 #[test]
256 fn scroll_state_max_offsets() {
257 let state = ScrollState::new(100, 200, 80, 24);
258 assert_eq!(state.max_offset_x(), 20);
259 assert_eq!(state.max_offset_y(), 176);
260 }
261
262 #[test]
263 fn scroll_state_visible_rect() {
264 let mut state = ScrollState::new(100, 200, 80, 24);
265 assert_eq!(state.visible_rect(), Rect::new(0, 0, 80, 24));
266 state.offset_x = 5;
267 state.offset_y = 10;
268 assert_eq!(state.visible_rect(), Rect::new(5, 10, 80, 24));
269 }
270
271 #[test]
272 fn manager_register_and_get() {
273 let mut mgr = ScrollManager::new();
274 mgr.register(wid(1), 100, 200, 80, 24);
275 let state = mgr.get(wid(1));
276 assert!(state.is_some());
277 let state = match state {
278 Some(s) => s,
279 None => unreachable!(),
280 };
281 assert_eq!(state.content_width, 100);
282 }
283
284 #[test]
285 fn manager_scroll_by_clamps() {
286 let mut mgr = ScrollManager::new();
287 mgr.register(wid(1), 100, 200, 80, 24);
288
289 mgr.scroll_by(wid(1), 10, 20);
290 let state = mgr.get(wid(1));
291 assert!(state.is_some());
292 let state = match state {
293 Some(s) => s,
294 None => unreachable!(),
295 };
296 assert_eq!(state.offset_x, 10);
297 assert_eq!(state.offset_y, 20);
298
299 mgr.scroll_by(wid(1), 100, 1000);
301 let state = match mgr.get(wid(1)) {
302 Some(s) => s,
303 None => unreachable!(),
304 };
305 assert_eq!(state.offset_x, 20); assert_eq!(state.offset_y, 176); mgr.scroll_by(wid(1), -100, -1000);
310 let state = match mgr.get(wid(1)) {
311 Some(s) => s,
312 None => unreachable!(),
313 };
314 assert_eq!(state.offset_x, 0);
315 assert_eq!(state.offset_y, 0);
316 }
317
318 #[test]
319 fn manager_scroll_to() {
320 let mut mgr = ScrollManager::new();
321 mgr.register(wid(1), 100, 200, 80, 24);
322
323 mgr.scroll_to(wid(1), 15, 100);
324 let state = match mgr.get(wid(1)) {
325 Some(s) => s,
326 None => unreachable!(),
327 };
328 assert_eq!(state.offset_x, 15);
329 assert_eq!(state.offset_y, 100);
330
331 mgr.scroll_to(wid(1), 500, 500);
333 let state = match mgr.get(wid(1)) {
334 Some(s) => s,
335 None => unreachable!(),
336 };
337 assert_eq!(state.offset_x, 20);
338 assert_eq!(state.offset_y, 176);
339 }
340
341 #[test]
342 fn manager_can_scroll() {
343 let mut mgr = ScrollManager::new();
344 mgr.register(wid(1), 100, 200, 80, 24);
345 assert!(mgr.can_scroll_x(wid(1)));
346 assert!(mgr.can_scroll_y(wid(1)));
347 assert!(!mgr.can_scroll_x(wid(999)));
348 assert!(!mgr.can_scroll_y(wid(999)));
349 }
350
351 #[test]
352 fn manager_visible_rect() {
353 let mut mgr = ScrollManager::new();
354 mgr.register(wid(1), 100, 200, 80, 24);
355 let rect = mgr.visible_rect(wid(1));
356 assert_eq!(rect, Some(Rect::new(0, 0, 80, 24)));
357 assert_eq!(mgr.visible_rect(wid(999)), None);
358 }
359
360 #[test]
361 fn manager_remove() {
362 let mut mgr = ScrollManager::new();
363 mgr.register(wid(1), 100, 200, 80, 24);
364 mgr.remove(wid(1));
365 assert!(mgr.get(wid(1)).is_none());
366 }
367
368 #[test]
369 fn extract_overflow_default() {
370 let style = ComputedStyle::new();
371 let (ox, oy) = extract_overflow(&style);
372 assert_eq!(ox, OverflowBehavior::Visible);
373 assert_eq!(oy, OverflowBehavior::Visible);
374 }
375
376 #[test]
377 fn extract_overflow_shorthand() {
378 let mut style = ComputedStyle::new();
379 style.set(PropertyName::Overflow, CssValue::Keyword("hidden".into()));
380 let (ox, oy) = extract_overflow(&style);
381 assert_eq!(ox, OverflowBehavior::Hidden);
382 assert_eq!(oy, OverflowBehavior::Hidden);
383 }
384
385 #[test]
386 fn extract_overflow_xy_separate() {
387 let mut style = ComputedStyle::new();
388 style.set(PropertyName::OverflowX, CssValue::Keyword("scroll".into()));
389 style.set(PropertyName::OverflowY, CssValue::Keyword("hidden".into()));
390 let (ox, oy) = extract_overflow(&style);
391 assert_eq!(ox, OverflowBehavior::Scroll);
392 assert_eq!(oy, OverflowBehavior::Hidden);
393 }
394
395 #[test]
396 fn extract_overflow_auto() {
397 let mut style = ComputedStyle::new();
398 style.set(PropertyName::Overflow, CssValue::Keyword("auto".into()));
399 let (ox, oy) = extract_overflow(&style);
400 assert_eq!(ox, OverflowBehavior::Auto);
401 assert_eq!(oy, OverflowBehavior::Auto);
402 }
403
404 #[test]
405 fn overflow_behavior_default() {
406 assert_eq!(OverflowBehavior::default(), OverflowBehavior::Visible);
407 }
408
409 #[test]
410 fn scroll_state_no_scroll_max_offset_zero() {
411 let state = ScrollState::new(40, 10, 80, 24);
412 assert_eq!(state.max_offset_x(), 0);
413 assert_eq!(state.max_offset_y(), 0);
414 }
415}