use eframe::egui::{self, Context, Response, Sense, Ui, Widget};
pub struct FixedScrollbar<'a> {
value: &'a mut f32,
bar_width: f32,
handle_height: f32,
scroll_sensitivity: f32,
scroll_smoothing: bool,
}
impl<'a> FixedScrollbar<'a> {
pub fn new(value: &'a mut f32) -> Self {
*value = value.clamp(0.0, 1.0);
Self {
value,
bar_width: 20.0,
handle_height: 50.0,
scroll_sensitivity: 0.1,
scroll_smoothing: true,
}
}
pub fn scroll_sensitivity(mut self, sensitivity: f32) -> Self {
self.scroll_sensitivity = sensitivity;
self
}
pub fn scroll_smoothing(mut self, scroll_smoothing: bool) -> Self {
self.scroll_smoothing = scroll_smoothing;
self
}
pub fn handle_height(mut self, handle_height: f32) -> Self {
self.handle_height = handle_height;
self
}
pub fn bar_width(mut self, bar_width: f32) -> Self {
self.bar_width = bar_width;
self
}
pub fn show_in_side_panel(self, ctx: &Context, title: impl Into<String>) {
egui::SidePanel::right(title.into())
.resizable(false)
.max_width(self.bar_width)
.frame(egui::Frame::none())
.show(ctx, |ui| {
ui.add(self);
});
}
pub fn show_floating(self, ui: &mut Ui, pos: egui::Pos2) {
let area = egui::Area::new("floating_scrollbar".into())
.movable(false)
.fixed_pos(pos);
area.show(ui.ctx(), |ui| {
ui.add(self);
});
}
pub fn show_docked(self, ui: &mut Ui, area: egui::Rect) {
let scroll_rect = egui::Rect::from_min_size(
egui::pos2(area.max.x - self.bar_width, area.min.y),
egui::vec2(self.bar_width, area.height()),
);
let area = egui::Area::new("docked_scrollbar".into())
.movable(false)
.fixed_pos(scroll_rect.min);
area.show(ui.ctx(), |ui| {
ui.add(self);
});
}
}
impl Widget for FixedScrollbar<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let available_height = ui.available_height();
let (rect, response) = ui.allocate_exact_size(
egui::vec2(self.bar_width, available_height),
Sense::click_and_drag(),
);
if response.dragged() {
if let Some(pointer) = response.interact_pointer_pos() {
let normalized = (pointer.y - rect.min.y) / rect.height();
*self.value = normalized.clamp(0.0, 1.0);
}
}
let scroll_delta = if self.scroll_smoothing {
ui.input(|i| i.smooth_scroll_delta.y)
} else {
ui.input(|i| i.raw_scroll_delta.y)
};
if scroll_delta != 0.0 {
*self.value = (*self.value
- (scroll_delta * self.scroll_sensitivity / available_height))
.clamp(0.0, 1.0);
}
ui.painter().rect_filled(
egui::Rect::from_min_size(rect.min, egui::vec2(self.bar_width, rect.height())),
0.0,
ui.style().visuals.extreme_bg_color,
);
let handle_height = (available_height * 0.2).min(self.handle_height);
let handle_y = rect.min.y + (rect.height() - handle_height) * *self.value;
ui.painter().rect_filled(
egui::Rect::from_min_size(
egui::pos2(rect.min.x, handle_y),
egui::vec2(self.bar_width, handle_height),
),
0.0,
ui.style().visuals.widgets.active.bg_fill,
);
response
}
}
#[cfg(test)]
mod tests {
use super::*;
use eframe::egui::Context;
use egui::{LayerId, UiBuilder};
fn test_context() -> Context {
let ctx = Context::default();
ctx.begin_pass(egui::RawInput::default());
ctx
}
fn test_ui(ctx: &Context) -> Ui {
let mut ui = Ui::new(
ctx.clone(),
LayerId::new(egui::Order::Foreground, "test".into()),
egui::Id::new("test"),
UiBuilder::default(),
);
ui.set_style(egui::Style::default());
ui
}
#[test]
fn test_scrollbar_creation() {
let mut value = 0.5;
let scrollbar = FixedScrollbar::new(&mut value);
assert_eq!(scrollbar.bar_width, 20.0);
assert_eq!(*scrollbar.value, 0.5);
}
#[test]
fn test_scrollbar_builder() {
let mut value = 0.5;
let scrollbar = FixedScrollbar::new(&mut value)
.scroll_sensitivity(0.2)
.scroll_smoothing(false)
.handle_height(30.0)
.bar_width(30.0);
assert_eq!(scrollbar.scroll_sensitivity, 0.2);
assert!(!scrollbar.scroll_smoothing);
assert_eq!(scrollbar.handle_height, 30.0);
assert_eq!(scrollbar.bar_width, 30.0)
}
#[test]
fn test_scrollbar_clamping() {
let ctx = test_context();
let mut ui = test_ui(&ctx);
let mut value = 2.0; let scrollbar = FixedScrollbar::new(&mut value);
ui.add(scrollbar);
assert!(value <= 1.0);
let mut value = -1.0;
let scrollbar = FixedScrollbar::new(&mut value);
ui.add(scrollbar);
assert!(value >= 0.0);
}
#[test]
fn test_side_panel_creation() {
let ctx = test_context();
let mut value = 0.5;
let scrollbar = FixedScrollbar::new(&mut value);
scrollbar.show_in_side_panel(&ctx, "test_panel");
}
#[test]
fn test_floating_creation() {
let ctx = test_context();
let mut ui = test_ui(&ctx);
let mut value = 0.5;
let scrollbar = FixedScrollbar::new(&mut value);
scrollbar.show_floating(&mut ui, egui::pos2(100.0, 100.0));
}
#[test]
fn test_docked_creation() {
let ctx = test_context();
let mut ui = test_ui(&ctx);
let mut value = 0.5;
let scrollbar = FixedScrollbar::new(&mut value);
let area = ui.available_rect_before_wrap();
scrollbar.show_docked(&mut ui, area);
}
}