use crate::render::RenderContext;
use crate::types::Rect;
use super::style::ScrollbarStyle;
use super::theme::ScrollbarTheme;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScrollbarVisualState {
#[default]
Hidden,
Dormant,
Active,
HandleHovered,
Dragging,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct ScrollbarResult {
pub track_rect: Rect,
pub thumb_rect: Rect,
pub scroll_offset: f64,
pub dragged: bool,
}
pub struct ScrollbarView<'a> {
pub content_height: f64,
pub viewport_height: f64,
pub scroll_offset: f64,
pub state: ScrollbarVisualState,
pub drag_pos_y: Option<f64>,
pub style: &'a dyn ScrollbarStyle,
pub theme: &'a dyn ScrollbarTheme,
}
pub fn draw_scrollbar(
ctx: &mut dyn RenderContext,
rect: Rect,
view: &ScrollbarView<'_>,
) -> ScrollbarResult {
let style = view.style;
let theme = view.theme;
if matches!(view.state, ScrollbarVisualState::Hidden) {
return ScrollbarResult {
scroll_offset: view.scroll_offset,
track_rect: rect,
..Default::default()
};
}
if view.content_height <= view.viewport_height {
return ScrollbarResult {
scroll_offset: view.scroll_offset,
track_rect: rect,
..Default::default()
};
}
let opacity = match view.state {
ScrollbarVisualState::Hidden => 0.0,
ScrollbarVisualState::Dormant => 0.0,
ScrollbarVisualState::Active => 0.5,
ScrollbarVisualState::HandleHovered | ScrollbarVisualState::Dragging => 0.8,
};
let effective_opacity = if style.thumb_min_length() < 30.0 && !style.draw_track_bg() {
1.0
} else {
opacity
};
if effective_opacity <= 0.0 {
return ScrollbarResult {
scroll_offset: view.scroll_offset,
track_rect: rect,
..Default::default()
};
}
let pad = style.track_padding();
let track_rect = Rect::new(
rect.x + pad,
rect.y + pad,
rect.width - pad * 2.0,
rect.height - pad * 2.0,
);
if style.draw_track_bg() {
let track_bg = append_hex_alpha(theme.track_bg(), 0x20);
ctx.set_fill_color(&track_bg);
ctx.fill_rect(track_rect.x, track_rect.y, track_rect.width, track_rect.height);
}
let visible_ratio = (view.viewport_height / view.content_height).clamp(0.0, 1.0);
let max_scroll = (view.content_height - view.viewport_height).max(0.0);
let scroll_ratio = if max_scroll > 0.0 {
(view.scroll_offset / max_scroll).clamp(0.0, 1.0)
} else {
0.0
};
let thumb_len = (track_rect.height * visible_ratio)
.max(style.thumb_min_length())
.min(track_rect.height);
let available = (track_rect.height - thumb_len).max(0.0);
let mut thumb_y = track_rect.y + scroll_ratio * available;
let mut offset = view.scroll_offset;
if let Some(y) = view.drag_pos_y {
let new_ratio = ((y - track_rect.y - thumb_len / 2.0) / available.max(1.0)).clamp(0.0, 1.0);
offset = new_ratio * max_scroll;
thumb_y = track_rect.y + new_ratio * available;
}
let thumb_rect = Rect::new(track_rect.x, thumb_y, track_rect.width, thumb_len);
let thumb_color = if style.draw_track_bg() {
let s = append_hex_alpha(theme.thumb_normal(), 0x80);
ctx.set_fill_color(&s);
ctx.fill_rect(thumb_rect.x, thumb_rect.y, thumb_rect.width, thumb_rect.height);
return ScrollbarResult {
track_rect,
thumb_rect,
scroll_offset: offset,
dragged: view.drag_pos_y.is_some(),
};
} else {
match view.state {
ScrollbarVisualState::HandleHovered | ScrollbarVisualState::Dragging => {
theme.thumb_active()
}
_ => theme.thumb_normal(),
}
};
ctx.set_fill_color_alpha(thumb_color, effective_opacity);
ctx.fill_rounded_rect(
thumb_rect.x,
thumb_rect.y,
thumb_rect.width,
thumb_rect.height,
style.thumb_radius(),
);
ctx.reset_alpha();
ScrollbarResult {
track_rect,
thumb_rect,
scroll_offset: offset,
dragged: view.drag_pos_y.is_some(),
}
}
pub fn draw_scrollbar_standard(
ctx: &mut dyn RenderContext,
rect: Rect,
content_height: f64,
viewport_height: f64,
scroll_offset: f64,
state: ScrollbarVisualState,
drag_pos_y: Option<f64>,
) -> ScrollbarResult {
use super::style::StandardScrollbarStyle;
use super::theme::DefaultScrollbarTheme;
let style = StandardScrollbarStyle;
let theme = DefaultScrollbarTheme;
draw_scrollbar(ctx, rect, &ScrollbarView {
content_height,
viewport_height,
scroll_offset,
state,
drag_pos_y,
style: &style,
theme: &theme,
})
}
pub fn draw_scrollbar_compact(
ctx: &mut dyn RenderContext,
rect: Rect,
content_height: f64,
viewport_height: f64,
scroll_offset: f64,
drag_pos_y: Option<f64>,
) -> ScrollbarResult {
use super::style::CompactScrollbarStyle;
use super::theme::DefaultScrollbarTheme;
let style = CompactScrollbarStyle;
let theme = DefaultScrollbarTheme;
draw_scrollbar(ctx, rect, &ScrollbarView {
content_height,
viewport_height,
scroll_offset,
state: ScrollbarVisualState::Active,
drag_pos_y,
style: &style,
theme: &theme,
})
}
pub fn draw_scrollbar_signal(
ctx: &mut dyn RenderContext,
rect: Rect,
content_height: f64,
viewport_height: f64,
scroll_offset: f64,
drag_pos_y: Option<f64>,
) -> ScrollbarResult {
use super::style::SignalScrollbarStyle;
use super::theme::DefaultScrollbarTheme;
let style = SignalScrollbarStyle;
let theme = DefaultScrollbarTheme;
draw_scrollbar(ctx, rect, &ScrollbarView {
content_height,
viewport_height,
scroll_offset,
state: ScrollbarVisualState::Active,
drag_pos_y,
style: &style,
theme: &theme,
})
}
fn append_hex_alpha(color: &str, alpha_byte: u8) -> String {
if color.starts_with('#') && color.len() == 7 {
format!("{}{:02x}", color, alpha_byte)
} else {
color.to_owned()
}
}