use fltk::{draw as fltk_draw, enums::*, prelude::*, valuator::Scrollbar};
use std::cell::RefCell;
use std::rc::Rc;
use std::time::{Duration, Instant};
pub const MIN_THUMB_HEIGHT: i32 = 20;
fn thumb_geometry(
y: i32,
h: i32,
min: f64,
max: f64,
val: f64,
slider_size: f32,
) -> Option<(i32, i32)> {
if max <= min || slider_size <= 0.0 || h <= 0 {
return None;
}
let s = ((slider_size as f64 * h as f64).round() as i32).clamp(MIN_THUMB_HEIGHT, h);
if s >= h {
return None;
}
let val_frac = ((val - min) / (max - min)).clamp(0.0, 1.0);
let track = h - s;
Some((y + (val_frac * track as f64).round() as i32, s))
}
fn value_for_thumb_top(
thumb_top: i32,
y: i32,
h: i32,
thumb_height: i32,
min: f64,
max: f64,
) -> f64 {
let track = h - thumb_height;
if track <= 0 {
return min;
}
let frac = ((thumb_top - y) as f64 / track as f64).clamp(0.0, 1.0);
min + frac * (max - min)
}
fn page_step(min: f64, max: f64, slider_size: f32) -> f64 {
let ss = slider_size as f64;
if ss <= 0.0 || ss >= 1.0 {
return 0.0;
}
(max - min) * ss / (1.0 - ss)
}
#[derive(Debug, Clone, Copy)]
struct Paging {
down: bool,
target_y: i32,
}
fn apply_page_step(state: &Rc<RefCell<ResponsiveScrollbarState>>, sb: &mut Scrollbar) -> bool {
let paging = match state.borrow().paging {
Some(p) => p,
None => return false,
};
let (y, h) = (sb.y(), sb.h());
let (min, max) = (sb.minimum(), sb.maximum());
let slider_size = sb.slider_size();
let val = sb.value();
let (thumb_top, thumb_h) = match thumb_geometry(y, h, min, max, val, slider_size) {
Some(g) => g,
None => return false,
};
let reached = if paging.down {
paging.target_y < thumb_top + thumb_h
} else {
paging.target_y >= thumb_top
};
if reached {
return false;
}
let page = page_step(min, max, slider_size);
if page <= 0.0 {
return false;
}
let new_val = if paging.down {
(val + page).min(max)
} else {
(val - page).max(min)
};
if new_val == val {
return false; }
sb.set_value(new_val);
sb.do_callback();
sb.redraw();
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollbarState {
Asleep,
Awake,
Hovered,
}
#[derive(Debug, Clone)]
struct ResponsiveScrollbarState {
state: ScrollbarState,
last_wake_time: Instant,
background_color: Color,
drag_offset: Option<i32>,
paging: Option<Paging>,
paging_timer_active: bool,
}
#[derive(Clone)]
pub struct ResponsiveScrollbar {
scrollbar: Scrollbar,
state: Rc<RefCell<ResponsiveScrollbarState>>,
}
impl ResponsiveScrollbar {
pub fn new(x: i32, y: i32, w: i32, h: i32, background_color: Color) -> Self {
let mut scrollbar = Scrollbar::default().with_pos(x, y).with_size(w, h);
let state = Rc::new(RefCell::new(ResponsiveScrollbarState {
state: ScrollbarState::Asleep,
last_wake_time: Instant::now() - Duration::from_secs(10),
background_color,
drag_offset: None,
paging: None,
paging_timer_active: false,
}));
scrollbar.draw({
let state = state.clone();
let sb = scrollbar.clone();
move |_| {
let st = state.borrow();
let x = sb.x();
let y = sb.y();
let w = sb.w();
let h = sb.h();
let rect_col = Color::from_rgb(204, 204, 204);
match st.state {
ScrollbarState::Asleep | ScrollbarState::Awake => {
fltk_draw::set_draw_color(st.background_color);
fltk_draw::draw_rectf(x, y, w, h);
let min = sb.minimum();
let max = sb.maximum();
let val = sb.value();
let slider_size = sb.slider_size();
let sbw = if st.state == ScrollbarState::Awake {
w - 4
} else {
3
};
let offset = if st.state == ScrollbarState::Awake {
3
} else {
w - sbw - 2
};
if let Some((slider_y, slider_height)) =
thumb_geometry(y, h, min, max, val, slider_size)
{
fltk_draw::set_draw_color(rect_col);
fltk_draw::draw_rounded_rectf(
x + offset,
slider_y + 1,
sbw,
slider_height - 2,
1,
);
}
}
ScrollbarState::Hovered => {
fltk_draw::set_draw_color(st.background_color);
fltk_draw::draw_rectf(x, y, w, h);
let min = sb.minimum();
let max = sb.maximum();
let val = sb.value();
let slider_size = sb.slider_size();
if let Some((slider_y, slider_height)) =
thumb_geometry(y, h, min, max, val, slider_size)
{
fltk_draw::draw_box(
FrameType::ThinUpBox,
x + 1,
slider_y + 1,
w - 2,
slider_height - 2,
rect_col,
);
}
}
}
}
});
scrollbar.handle({
let state = state.clone();
let mut sb = scrollbar.clone();
move |_, event| {
match event {
Event::Enter => {
state.borrow_mut().state = ScrollbarState::Hovered;
sb.redraw();
true }
Event::Leave => {
let mut st = state.borrow_mut();
st.state = ScrollbarState::Awake;
st.last_wake_time = Instant::now();
sb.redraw();
true
}
Event::Move => {
let mut st = state.borrow_mut();
if st.state != ScrollbarState::Hovered {
st.state = ScrollbarState::Hovered;
sb.redraw();
}
true
}
Event::Push => {
let ey = fltk::app::event_y();
let (y, h) = (sb.y(), sb.h());
let geom = thumb_geometry(
y,
h,
sb.minimum(),
sb.maximum(),
sb.value(),
sb.slider_size(),
);
let mut start_paging = false;
{
let mut st = state.borrow_mut();
st.state = ScrollbarState::Hovered;
st.drag_offset = None;
st.paging = None;
if let Some((thumb_top, thumb_h)) = geom {
if ey >= thumb_top && ey < thumb_top + thumb_h {
st.drag_offset = Some(ey - thumb_top);
} else {
st.paging = Some(Paging {
down: ey >= thumb_top + thumb_h,
target_y: ey,
});
start_paging = true;
}
}
}
if start_paging {
let more = apply_page_step(&state, &mut sb);
let mut st = state.borrow_mut();
if more && !st.paging_timer_active {
st.paging_timer_active = true;
drop(st);
let state_pg = state.clone();
let mut sb_pg = sb.clone();
fltk::app::add_timeout3(0.3, move |handle| {
let cont = apply_page_step(&state_pg, &mut sb_pg);
if cont && state_pg.borrow().paging.is_some() {
fltk::app::repeat_timeout3(0.05, handle);
} else {
state_pg.borrow_mut().paging_timer_active = false;
}
});
} else if !more {
st.paging = None;
}
}
sb.redraw();
true
}
Event::Drag => {
let offset = state.borrow().drag_offset;
if let Some(offset) = offset {
let ey = fltk::app::event_y();
let y = sb.y();
let h = sb.h();
let (min, max) = (sb.minimum(), sb.maximum());
if let Some((_, thumb_h)) =
thumb_geometry(y, h, min, max, sb.value(), sb.slider_size())
{
let v = value_for_thumb_top(ey - offset, y, h, thumb_h, min, max);
sb.set_value(v);
sb.do_callback();
}
sb.redraw();
}
state.borrow_mut().state = ScrollbarState::Hovered;
true
}
Event::Released => {
let mut st = state.borrow_mut();
st.drag_offset = None;
st.paging = None;
st.state = ScrollbarState::Hovered;
st.last_wake_time = Instant::now();
drop(st);
sb.redraw();
true
}
_ => false,
}
}
});
scrollbar.super_handle_first(false);
{
let state_timer = state.clone();
let mut sb_timer = scrollbar.clone();
fltk::app::add_timeout3(0.1, move |handle| {
let needs_redraw = {
let mut st = state_timer.borrow_mut();
if st.state == ScrollbarState::Awake {
let elapsed = Instant::now().duration_since(st.last_wake_time);
if elapsed > Duration::from_secs(1) {
st.state = ScrollbarState::Asleep;
true
} else {
false
}
} else {
false
}
};
if needs_redraw {
sb_timer.redraw();
}
fltk::app::repeat_timeout3(0.1, handle);
});
}
Self { scrollbar, state }
}
pub fn wake(&mut self) {
let mut st = self.state.borrow_mut();
let old_state = st.state;
if st.state != ScrollbarState::Hovered {
st.state = ScrollbarState::Awake;
}
st.last_wake_time = Instant::now();
if old_state != st.state {
self.scrollbar.redraw();
}
}
pub fn set_type(&mut self, typ: fltk::valuator::ScrollbarType) {
self.scrollbar.set_type(typ);
}
pub fn set_bounds(&mut self, min: f64, max: f64) {
self.scrollbar.set_bounds(min, max);
}
pub fn set_slider_size(&mut self, size: f32) {
self.scrollbar.set_slider_size(size);
}
pub fn min_slider_size(&self) -> f32 {
let h = self.scrollbar.h();
if h > 0 {
(MIN_THUMB_HEIGHT as f32 / h as f32).min(1.0)
} else {
1.0
}
}
pub fn set_step(&mut self, a: f64, b: i32) {
self.scrollbar.set_step(a, b);
}
pub fn set_value(&mut self, val: f64) {
self.scrollbar.set_value(val);
}
pub fn value(&self) -> f64 {
self.scrollbar.value()
}
pub fn minimum(&self) -> f64 {
self.scrollbar.minimum()
}
pub fn maximum(&self) -> f64 {
self.scrollbar.maximum()
}
pub fn slider_size(&self) -> f32 {
self.scrollbar.slider_size()
}
pub fn set_callback<F: FnMut(&mut Scrollbar) + 'static>(&mut self, cb: F) {
self.scrollbar.set_callback(cb);
}
pub fn resize(&mut self, x: i32, y: i32, w: i32, h: i32) {
self.scrollbar.resize(x, y, w, h);
}
pub fn show(&mut self) {
self.scrollbar.show();
}
pub fn redraw(&mut self) {
self.scrollbar.redraw();
}
pub fn as_base_widget(&self) -> fltk::widget::Widget {
self.scrollbar.as_base_widget()
}
}