use crate::event::{Event, EventCtx, MouseButton};
use crate::geometry::{Color, Point, Rect};
use crate::include_svg;
use crate::painter::Painter;
use crate::svg::SvgImage;
use crate::theme::Theme;
use crate::widget::Widget;
pub const SCROLLBAR_THICKNESS: i32 = 16;
const ARROW_BTN: i32 = SCROLLBAR_THICKNESS;
const MIN_THUMB: i32 = 16;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Orientation {
Vertical,
Horizontal,
}
pub struct ScrollBar {
rect: Rect,
orientation: Orientation,
value: i32,
max: i32,
viewport: i32,
line_step: i32,
drag_offset: Option<i32>,
wheel_accum: f32,
}
impl ScrollBar {
pub fn new(rect: Rect, orientation: Orientation) -> Self {
Self {
rect,
orientation,
value: 0,
max: 0,
viewport: 0,
line_step: 1,
drag_offset: None,
wheel_accum: 0.0,
}
}
pub fn vertical(rect: Rect) -> Self {
Self::new(rect, Orientation::Vertical)
}
pub fn horizontal(rect: Rect) -> Self {
Self::new(rect, Orientation::Horizontal)
}
pub fn rect(&self) -> Rect {
self.rect
}
pub fn set_rect(&mut self, rect: Rect) {
self.rect = rect;
}
pub fn value(&self) -> i32 {
self.value
}
pub fn set_value(&mut self, value: i32) {
self.value = value.clamp(0, self.max);
}
pub fn max(&self) -> i32 {
self.max
}
pub fn viewport(&self) -> i32 {
self.viewport
}
pub fn set_range(&mut self, viewport: i32, max: i32) {
self.viewport = viewport.max(0);
self.max = max.max(0);
if self.value > self.max {
self.value = self.max;
}
}
pub fn set_line_step(&mut self, step: i32) {
self.line_step = step.max(1);
}
pub fn end_drag(&mut self) {
self.drag_offset = None;
}
fn track_rect(&self) -> Rect {
match self.orientation {
Orientation::Vertical => Rect::new(
self.rect.x,
self.rect.y + ARROW_BTN,
self.rect.w,
(self.rect.h - 2 * ARROW_BTN).max(0),
),
Orientation::Horizontal => Rect::new(
self.rect.x + ARROW_BTN,
self.rect.y,
(self.rect.w - 2 * ARROW_BTN).max(0),
self.rect.h,
),
}
}
fn track_extent(&self) -> i32 {
let t = self.track_rect();
match self.orientation {
Orientation::Vertical => t.h,
Orientation::Horizontal => t.w,
}
}
fn thumb_size(&self) -> i32 {
let track = self.track_extent();
if self.max <= 0 || self.viewport <= 0 {
return track;
}
let total = self.viewport + self.max;
((track * self.viewport) / total.max(1))
.max(MIN_THUMB)
.min(track)
}
fn thumb_offset(&self) -> i32 {
if self.max <= 0 {
return 0;
}
let movable = (self.track_extent() - self.thumb_size()).max(0);
(movable as i64 * self.value as i64 / self.max.max(1) as i64) as i32
}
fn thumb_rect(&self) -> Rect {
let track = self.track_rect();
let off = self.thumb_offset();
let size = self.thumb_size();
match self.orientation {
Orientation::Vertical => Rect::new(track.x, track.y + off, track.w, size),
Orientation::Horizontal => Rect::new(track.x + off, track.y, size, track.h),
}
}
fn neg_arrow_rect(&self) -> Rect {
match self.orientation {
Orientation::Vertical => Rect::new(self.rect.x, self.rect.y, self.rect.w, ARROW_BTN),
Orientation::Horizontal => Rect::new(self.rect.x, self.rect.y, ARROW_BTN, self.rect.h),
}
}
fn pos_arrow_rect(&self) -> Rect {
match self.orientation {
Orientation::Vertical => Rect::new(
self.rect.x,
self.rect.bottom() - ARROW_BTN,
self.rect.w,
ARROW_BTN,
),
Orientation::Horizontal => Rect::new(
self.rect.right() - ARROW_BTN,
self.rect.y,
ARROW_BTN,
self.rect.h,
),
}
}
fn scroll_by(&mut self, delta: i32) {
self.set_value(self.value.saturating_add(delta));
}
fn scroll_lines(&mut self, lines: f32) -> bool {
self.wheel_accum += lines;
let whole = self.wheel_accum.trunc();
self.wheel_accum -= whole;
let step = whole as i32;
if step == 0 {
return false;
}
let before = self.value;
self.scroll_by(step);
if self.value == before {
self.wheel_accum = 0.0;
false
} else {
true
}
}
fn page_step(&self) -> i32 {
self.viewport.max(1)
}
fn handle_press(&mut self, pos: Point) {
if self.neg_arrow_rect().contains(pos) {
self.scroll_by(-self.line_step);
} else if self.pos_arrow_rect().contains(pos) {
self.scroll_by(self.line_step);
} else if self.thumb_rect().contains(pos) {
let thumb = self.thumb_rect();
let offset = match self.orientation {
Orientation::Vertical => pos.y - thumb.y,
Orientation::Horizontal => pos.x - thumb.x,
};
self.drag_offset = Some(offset);
} else if self.track_rect().contains(pos) {
let thumb = self.thumb_rect();
let page = self.page_step();
match self.orientation {
Orientation::Vertical => {
if pos.y < thumb.y {
self.scroll_by(-page);
} else if pos.y >= thumb.bottom() {
self.scroll_by(page);
}
}
Orientation::Horizontal => {
if pos.x < thumb.x {
self.scroll_by(-page);
} else if pos.x >= thumb.right() {
self.scroll_by(page);
}
}
}
}
}
fn handle_drag(&mut self, pos: Point) {
let Some(offset) = self.drag_offset else {
return;
};
let track = self.track_rect();
let thumb_size = self.thumb_size();
let movable = (self.track_extent() - thumb_size).max(1);
let pos_in_track = match self.orientation {
Orientation::Vertical => pos.y - offset - track.y,
Orientation::Horizontal => pos.x - offset - track.x,
};
let clamped = pos_in_track.clamp(0, movable);
self.value = ((self.max as i64 * clamped as i64) / movable as i64) as i32;
}
}
impl Widget for ScrollBar {
fn bounds(&self) -> Rect {
self.rect
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
let up = self.neg_arrow_rect();
let down = self.pos_arrow_rect();
let thumb_opt = (self.max > 0).then(|| self.thumb_rect());
painter.fill_checker(self.track_rect(), theme.face, theme.border);
painter.light_button(up, theme);
painter.light_button(down, theme);
if let Some(thumb) = thumb_opt {
let track = self.track_rect();
let mut t = thumb;
match self.orientation {
Orientation::Vertical => {
if thumb.y <= track.y {
t.y -= 1;
t.h += 1;
}
if thumb.bottom() >= track.bottom() {
t.h += 1;
}
}
Orientation::Horizontal => {
if thumb.x <= track.x {
t.x -= 1;
t.w += 1;
}
if thumb.right() >= track.right() {
t.w += 1;
}
}
}
painter.light_button(t, theme);
}
painter.stroke_rect(self.rect, theme.border);
draw_arrow(
painter,
up,
self.orientation,
ArrowDir::Negative,
theme.text,
);
draw_arrow(
painter,
down,
self.orientation,
ArrowDir::Positive,
theme.text,
);
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
match event {
Event::PointerDown {
pos,
button: MouseButton::Left,
} => {
self.handle_press(*pos);
ctx.request_paint();
}
Event::PointerMove { pos } if self.drag_offset.is_some() => {
self.handle_drag(*pos);
ctx.request_paint();
}
Event::PointerUp {
button: MouseButton::Left,
..
} if self.drag_offset.is_some() => {
self.drag_offset = None;
ctx.request_paint();
}
Event::PointerLeave if self.drag_offset.is_some() => {
self.drag_offset = None;
ctx.request_paint();
}
Event::Scroll {
delta_x, delta_y, ..
} => {
let lines = match self.orientation {
Orientation::Vertical => *delta_y,
Orientation::Horizontal => *delta_x,
};
if self.scroll_lines(lines) {
ctx.request_paint();
}
}
_ => {}
}
}
fn captures_pointer(&self) -> bool {
self.drag_offset.is_some()
}
fn layout(&mut self, bounds: Rect) {
self.rect = bounds;
}
}
#[derive(Clone, Copy)]
enum ArrowDir {
Negative,
Positive,
}
const ARROW_UP: SvgImage = include_svg!("assets/scrollbar/up.svg");
const ARROW_DOWN: SvgImage = include_svg!("assets/scrollbar/down.svg");
const ARROW_LEFT: SvgImage = include_svg!("assets/scrollbar/left.svg");
const ARROW_RIGHT: SvgImage = include_svg!("assets/scrollbar/right.svg");
fn draw_arrow(painter: &mut Painter, btn: Rect, orient: Orientation, dir: ArrowDir, color: Color) {
let arrow = match (orient, dir) {
(Orientation::Vertical, ArrowDir::Negative) => &ARROW_UP,
(Orientation::Vertical, ArrowDir::Positive) => &ARROW_DOWN,
(Orientation::Horizontal, ArrowDir::Negative) => &ARROW_LEFT,
(Orientation::Horizontal, ArrowDir::Positive) => &ARROW_RIGHT,
};
arrow.draw_tinted(painter, btn, color);
}