1use crate::components::ComponentDimensions;
2use crate::draw_utils::print_with_color_and_background_at;
3use crate::{Bounds, ScreenBuffer};
4
5#[derive(Debug, Clone)]
15pub struct VerticalScrollbar {
16 pub parent_id: String,
18 track_char: &'static str,
20 knob_char: &'static str,
22}
23
24impl VerticalScrollbar {
25 pub fn new(parent_id: String) -> Self {
27 Self {
28 parent_id,
29 track_char: "│", knob_char: "█", }
32 }
33
34 pub fn should_draw(&self, content_height: usize, viewable_height: usize) -> bool {
36 content_height > viewable_height
37 }
38
39 pub fn get_bounds(&self, parent_bounds: &Bounds) -> (usize, usize, usize) {
41 let component_dims = ComponentDimensions::new(*parent_bounds);
42 let track_bounds = component_dims.vertical_scrollbar_track_bounds();
43 (track_bounds.x1, track_bounds.y1, track_bounds.y2)
44 }
45
46 pub fn calculate_knob_metrics(
48 &self,
49 content_height: usize,
50 viewable_height: usize,
51 vertical_scroll: f64,
52 track_height: usize,
53 ) -> (usize, usize) {
54 if track_height == 0 {
55 return (0, 0);
56 }
57
58 use crate::components::dimensions::{Orientation, ScrollDimensions};
60 use crate::Bounds;
61
62 let scroll_dims = ScrollDimensions::new(
63 (1, content_height), (1, viewable_height),
65 (0.0, vertical_scroll), Bounds::new(0, 0, 2, track_height + 1), );
68
69 let knob_size = scroll_dims.calculate_knob_size(Orientation::Vertical);
70 let knob_position = scroll_dims.calculate_knob_position(Orientation::Vertical);
71
72 (knob_position, knob_size)
73 }
74
75 pub fn draw(
78 &self,
79 parent_bounds: &Bounds,
80 content_height: usize,
81 viewable_height: usize,
82 vertical_scroll: f64,
83 border_color: &Option<String>,
84 bg_color: &Option<String>,
85 buffer: &mut ScreenBuffer,
86 ) {
87 if !self.should_draw(content_height, viewable_height) {
88 return;
89 }
90
91 let (x, start_y, end_y) = self.get_bounds(parent_bounds);
92 let track_height = end_y.saturating_sub(start_y);
93
94 for y in start_y..end_y {
96 print_with_color_and_background_at(
97 y,
98 x,
99 &Some("bright_black".to_string()),
100 bg_color,
101 self.track_char,
102 buffer,
103 );
104 }
105
106 if track_height > 0 {
107 let (knob_position, knob_size) = self.calculate_knob_metrics(
108 content_height,
109 viewable_height,
110 vertical_scroll,
111 track_height,
112 );
113
114 for i in 0..knob_size {
116 let knob_y = start_y + knob_position + i;
117 if knob_y < end_y {
118 print_with_color_and_background_at(
119 knob_y,
120 x,
121 border_color,
122 bg_color,
123 self.knob_char,
124 buffer,
125 );
126 }
127 }
128 }
129 }
130
131 pub fn is_click_on_scrollbar(
133 &self,
134 click_x: usize,
135 click_y: usize,
136 parent_bounds: &Bounds,
137 ) -> bool {
138 let (x, _, _) = self.get_bounds(parent_bounds);
139 click_x == x && click_y > parent_bounds.top() && click_y < parent_bounds.bottom()
140 }
141
142 pub fn is_click_on_knob(
144 &self,
145 click_x: usize,
146 click_y: usize,
147 parent_bounds: &Bounds,
148 content_height: usize,
149 viewable_height: usize,
150 vertical_scroll: f64,
151 ) -> bool {
152 if !self.is_click_on_scrollbar(click_x, click_y, parent_bounds) {
153 return false;
154 }
155
156 let (_, start_y, end_y) = self.get_bounds(parent_bounds);
157 let track_height = end_y.saturating_sub(start_y);
158
159 if track_height == 0 {
160 return false;
161 }
162
163 let (knob_position, knob_size) = self.calculate_knob_metrics(
164 content_height,
165 viewable_height,
166 vertical_scroll,
167 track_height,
168 );
169
170 let knob_start_y = start_y + knob_position;
171 let knob_end_y = knob_start_y + knob_size;
172
173 click_y >= knob_start_y && click_y < knob_end_y
174 }
175
176 pub fn click_position_to_scroll_percentage(
178 &self,
179 click_y: usize,
180 parent_bounds: &Bounds,
181 ) -> f64 {
182 let (_, start_y, end_y) = self.get_bounds(parent_bounds);
183 let track_height = end_y.saturating_sub(start_y);
184
185 if track_height == 0 {
186 return 0.0;
187 }
188
189 let click_position = (click_y.saturating_sub(start_y)) as f64 / track_height as f64;
190 (click_position * 100.0).clamp(0.0, 100.0)
191 }
192
193 pub fn drag_to_scroll_percentage(
195 &self,
196 start_y: u16,
197 current_y: u16,
198 start_scroll_percentage: f64,
199 parent_bounds: &Bounds,
200 ) -> f64 {
201 let (_, track_start_y, track_end_y) = self.get_bounds(parent_bounds);
202 let track_height = track_end_y.saturating_sub(track_start_y);
203
204 if track_height == 0 {
205 return start_scroll_percentage;
206 }
207
208 let drag_delta = (current_y as isize) - (start_y as isize);
209 let percentage_delta = (drag_delta as f64 / track_height as f64) * 100.0;
210
211 (start_scroll_percentage + percentage_delta).clamp(0.0, 100.0)
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn test_vertical_scrollbar_creation() {
221 let scrollbar = VerticalScrollbar::new("test_muxbox".to_string());
222 assert_eq!(scrollbar.parent_id, "test_muxbox");
223 assert_eq!(scrollbar.track_char, "│");
224 assert_eq!(scrollbar.knob_char, "█");
225 }
226
227 #[test]
228 fn test_should_draw_logic() {
229 let scrollbar = VerticalScrollbar::new("test".to_string());
230
231 assert!(!scrollbar.should_draw(10, 20));
233 assert!(!scrollbar.should_draw(10, 10));
234
235 assert!(scrollbar.should_draw(20, 10));
237 }
238
239 #[test]
240 fn test_knob_metrics_calculation() {
241 let scrollbar = VerticalScrollbar::new("test".to_string());
242
243 let (position, size) = scrollbar.calculate_knob_metrics(100, 50, 0.0, 20);
245 assert_eq!(size, 10); assert_eq!(position, 0); let (position, _) = scrollbar.calculate_knob_metrics(100, 50, 50.0, 20);
250 assert_eq!(position, 5); }
252
253 #[test]
254 fn test_click_position_conversion() {
255 let scrollbar = VerticalScrollbar::new("test".to_string());
256 let bounds = Bounds::new(10, 10, 50, 30);
257
258 let scroll_pct = scrollbar.click_position_to_scroll_percentage(11, &bounds);
260 assert!((scroll_pct - 0.0).abs() < 0.01);
261
262 let scroll_pct = scrollbar.click_position_to_scroll_percentage(28, &bounds);
264 assert!((scroll_pct - 94.44).abs() < 1.0); let scroll_pct = scrollbar.click_position_to_scroll_percentage(20, &bounds);
268 assert!((scroll_pct - 50.0).abs() < 1.0); }
270}