Skip to main content

uzor_core/widgets/
scrollbar.rs

1//! Scrollbar widget geometry and configuration
2//!
3//! Handles the math of mapping scroll offsets to handle positions for headless architecture.
4
5use crate::types::Rect;
6use serde::{Deserialize, Serialize};
7
8/// Scrollbar configuration
9#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct ScrollbarConfig {
11    /// Total content size (height or width)
12    pub content_size: f64,
13    /// Visible viewport size
14    pub viewport_size: f64,
15    /// Current scroll offset
16    pub scroll_offset: f64,
17    /// Minimum handle size
18    pub min_handle_size: f64,
19    /// Whether this is a horizontal scrollbar
20    pub horizontal: bool,
21}
22
23impl Default for ScrollbarConfig {
24    fn default() -> Self {
25        Self {
26            content_size: 0.0,
27            viewport_size: 0.0,
28            scroll_offset: 0.0,
29            min_handle_size: 30.0,
30            horizontal: false,
31        }
32    }
33}
34
35impl ScrollbarConfig {
36    pub fn new(content_size: f64, viewport_size: f64, scroll_offset: f64) -> Self {
37        Self {
38            content_size,
39            viewport_size,
40            scroll_offset,
41            ..Default::default()
42        }
43    }
44
45    pub fn needs_scrollbar(&self) -> bool {
46        self.content_size > self.viewport_size
47    }
48
49    fn visible_ratio(&self) -> f64 {
50        if self.content_size <= 0.0 { 1.0 } else { (self.viewport_size / self.content_size).clamp(0.0, 1.0) }
51    }
52
53    fn scroll_ratio(&self) -> f64 {
54        let max_scroll = (self.content_size - self.viewport_size).max(0.0);
55        if max_scroll <= 0.0 { 0.0 } else { (self.scroll_offset / max_scroll).clamp(0.0, 1.0) }
56    }
57
58    pub fn max_scroll(&self) -> f64 {
59        (self.content_size - self.viewport_size).max(0.0)
60    }
61}
62
63/// Scrollbar geometry response
64#[derive(Clone, Debug, Default, Serialize, Deserialize)]
65pub struct ScrollbarResponse {
66    /// Track rectangle
67    pub track_rect: Rect,
68    /// Handle rectangle
69    pub handle_rect: Rect,
70    /// New scroll offset (if dragging)
71    pub scroll_offset: f64,
72    /// Whether scrollbar was dragged
73    pub dragged: bool,
74}
75
76impl ScrollbarConfig {
77    /// Calculate scrollbar handle geometry
78    pub fn calculate_geometry(&self, track_rect: Rect, drag_pos: Option<f64>) -> ScrollbarResponse {
79        if !self.needs_scrollbar() {
80            return ScrollbarResponse {
81                track_rect,
82                handle_rect: Rect::default(),
83                scroll_offset: self.scroll_offset,
84                dragged: false,
85            };
86        }
87
88        let visible_ratio = self.visible_ratio();
89        let scroll_ratio = self.scroll_ratio();
90
91        let (handle_rect, new_scroll_offset) = if self.horizontal {
92            let handle_width = (visible_ratio * track_rect.width).max(self.min_handle_size);
93            let available_width = track_rect.width - handle_width;
94
95            let mut offset = self.scroll_offset;
96            let mut handle_x = track_rect.x + scroll_ratio * available_width;
97
98            if let Some(x) = drag_pos {
99                let new_ratio = ((x - track_rect.x - handle_width / 2.0) / available_width).clamp(0.0, 1.0);
100                offset = new_ratio * self.max_scroll();
101                handle_x = track_rect.x + new_ratio * available_width;
102            }
103
104            (Rect::new(handle_x, track_rect.y, handle_width, track_rect.height), offset)
105        } else {
106            let handle_height = (visible_ratio * track_rect.height).max(self.min_handle_size);
107            let available_height = track_rect.height - handle_height;
108
109            let mut offset = self.scroll_offset;
110            let mut handle_y = track_rect.y + scroll_ratio * available_height;
111
112            if let Some(y) = drag_pos {
113                let new_ratio = ((y - track_rect.y - handle_height / 2.0) / available_height).clamp(0.0, 1.0);
114                offset = new_ratio * self.max_scroll();
115                handle_y = track_rect.y + new_ratio * available_height;
116            }
117
118            (Rect::new(track_rect.x, handle_y, track_rect.width, handle_height), offset)
119        };
120
121        ScrollbarResponse {
122            track_rect,
123            handle_rect,
124            scroll_offset: new_scroll_offset,
125            dragged: drag_pos.is_some(),
126        }
127    }
128}