use crate::components::ComponentDimensions;
use crate::draw_utils::print_with_color_and_background_at;
use crate::{Bounds, ScreenBuffer};
#[derive(Debug, Clone)]
pub struct VerticalScrollbar {
pub parent_id: String,
track_char: &'static str,
knob_char: &'static str,
}
impl VerticalScrollbar {
pub fn new(parent_id: String) -> Self {
Self {
parent_id,
track_char: "│", knob_char: "█", }
}
pub fn should_draw(&self, content_height: usize, viewable_height: usize) -> bool {
content_height > viewable_height
}
pub fn get_bounds(&self, parent_bounds: &Bounds) -> (usize, usize, usize) {
let component_dims = ComponentDimensions::new(*parent_bounds);
let track_bounds = component_dims.vertical_scrollbar_track_bounds();
(track_bounds.x1, track_bounds.y1, track_bounds.y2)
}
pub fn calculate_knob_metrics(
&self,
content_height: usize,
viewable_height: usize,
vertical_scroll: f64,
track_height: usize,
) -> (usize, usize) {
if track_height == 0 {
return (0, 0);
}
use crate::components::dimensions::{Orientation, ScrollDimensions};
use crate::Bounds;
let scroll_dims = ScrollDimensions::new(
(1, content_height), (1, viewable_height),
(0.0, vertical_scroll), Bounds::new(0, 0, 2, track_height + 1), );
let knob_size = scroll_dims.calculate_knob_size(Orientation::Vertical);
let knob_position = scroll_dims.calculate_knob_position(Orientation::Vertical);
(knob_position, knob_size)
}
pub fn draw(
&self,
parent_bounds: &Bounds,
content_height: usize,
viewable_height: usize,
vertical_scroll: f64,
border_color: &Option<String>,
bg_color: &Option<String>,
buffer: &mut ScreenBuffer,
) {
if !self.should_draw(content_height, viewable_height) {
return;
}
let (x, start_y, end_y) = self.get_bounds(parent_bounds);
let track_height = end_y.saturating_sub(start_y);
for y in start_y..end_y {
print_with_color_and_background_at(
y,
x,
&Some("bright_black".to_string()),
bg_color,
self.track_char,
buffer,
);
}
if track_height > 0 {
let (knob_position, knob_size) = self.calculate_knob_metrics(
content_height,
viewable_height,
vertical_scroll,
track_height,
);
for i in 0..knob_size {
let knob_y = start_y + knob_position + i;
if knob_y < end_y {
print_with_color_and_background_at(
knob_y,
x,
border_color,
bg_color,
self.knob_char,
buffer,
);
}
}
}
}
pub fn is_click_on_scrollbar(
&self,
click_x: usize,
click_y: usize,
parent_bounds: &Bounds,
) -> bool {
let (x, _, _) = self.get_bounds(parent_bounds);
click_x == x && click_y > parent_bounds.top() && click_y < parent_bounds.bottom()
}
pub fn is_click_on_knob(
&self,
click_x: usize,
click_y: usize,
parent_bounds: &Bounds,
content_height: usize,
viewable_height: usize,
vertical_scroll: f64,
) -> bool {
if !self.is_click_on_scrollbar(click_x, click_y, parent_bounds) {
return false;
}
let (_, start_y, end_y) = self.get_bounds(parent_bounds);
let track_height = end_y.saturating_sub(start_y);
if track_height == 0 {
return false;
}
let (knob_position, knob_size) = self.calculate_knob_metrics(
content_height,
viewable_height,
vertical_scroll,
track_height,
);
let knob_start_y = start_y + knob_position;
let knob_end_y = knob_start_y + knob_size;
click_y >= knob_start_y && click_y < knob_end_y
}
pub fn click_position_to_scroll_percentage(
&self,
click_y: usize,
parent_bounds: &Bounds,
) -> f64 {
let (_, start_y, end_y) = self.get_bounds(parent_bounds);
let track_height = end_y.saturating_sub(start_y);
if track_height == 0 {
return 0.0;
}
let click_position = (click_y.saturating_sub(start_y)) as f64 / track_height as f64;
(click_position * 100.0).clamp(0.0, 100.0)
}
pub fn drag_to_scroll_percentage(
&self,
start_y: u16,
current_y: u16,
start_scroll_percentage: f64,
parent_bounds: &Bounds,
) -> f64 {
let (_, track_start_y, track_end_y) = self.get_bounds(parent_bounds);
let track_height = track_end_y.saturating_sub(track_start_y);
if track_height == 0 {
return start_scroll_percentage;
}
let drag_delta = (current_y as isize) - (start_y as isize);
let percentage_delta = (drag_delta as f64 / track_height as f64) * 100.0;
(start_scroll_percentage + percentage_delta).clamp(0.0, 100.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vertical_scrollbar_creation() {
let scrollbar = VerticalScrollbar::new("test_muxbox".to_string());
assert_eq!(scrollbar.parent_id, "test_muxbox");
assert_eq!(scrollbar.track_char, "│");
assert_eq!(scrollbar.knob_char, "█");
}
#[test]
fn test_should_draw_logic() {
let scrollbar = VerticalScrollbar::new("test".to_string());
assert!(!scrollbar.should_draw(10, 20));
assert!(!scrollbar.should_draw(10, 10));
assert!(scrollbar.should_draw(20, 10));
}
#[test]
fn test_knob_metrics_calculation() {
let scrollbar = VerticalScrollbar::new("test".to_string());
let (position, size) = scrollbar.calculate_knob_metrics(100, 50, 0.0, 20);
assert_eq!(size, 10); assert_eq!(position, 0);
let (position, _) = scrollbar.calculate_knob_metrics(100, 50, 50.0, 20);
assert_eq!(position, 5); }
#[test]
fn test_click_position_conversion() {
let scrollbar = VerticalScrollbar::new("test".to_string());
let bounds = Bounds::new(10, 10, 50, 30);
let scroll_pct = scrollbar.click_position_to_scroll_percentage(11, &bounds);
assert!((scroll_pct - 0.0).abs() < 0.01);
let scroll_pct = scrollbar.click_position_to_scroll_percentage(28, &bounds);
assert!((scroll_pct - 94.44).abs() < 1.0);
let scroll_pct = scrollbar.click_position_to_scroll_percentage(20, &bounds);
assert!((scroll_pct - 50.0).abs() < 1.0); }
}