227 lines
6.7 KiB
Rust
227 lines
6.7 KiB
Rust
// Copyright (c) 2022 Marceline Cramer
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
use super::prelude::*;
|
|
use shell::Offset;
|
|
|
|
pub struct ScrollBarStyle {
|
|
pub margin: Vec2,
|
|
pub body_radius: f32,
|
|
pub body_width: f32,
|
|
pub body_idle_color: Color,
|
|
pub body_hover_color: Color,
|
|
pub body_selected_color: Color,
|
|
pub rail_width: f32,
|
|
pub rail_color: Color,
|
|
}
|
|
|
|
impl Default for ScrollBarStyle {
|
|
fn default() -> Self {
|
|
Self {
|
|
margin: Vec2::splat(2.0),
|
|
body_radius: 1.0,
|
|
body_width: 3.0,
|
|
body_idle_color: THEME.palette.base,
|
|
body_hover_color: THEME.palette.base_hover,
|
|
body_selected_color: THEME.palette.base_active,
|
|
rail_width: 1.0,
|
|
rail_color: THEME.palette.base,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ScrollBar {
|
|
// TODO: accessor method/trait for styled widgets?
|
|
pub style: ScrollBarStyle,
|
|
|
|
height: f32,
|
|
is_dirty: bool,
|
|
scroll: f32,
|
|
content_height: f32,
|
|
rail_rect: Rect,
|
|
body_color_anim: Animation<EaseInQuad, Color>,
|
|
is_hovering: bool,
|
|
is_selected: bool,
|
|
grab_coord: f32,
|
|
grab_scroll: f32,
|
|
}
|
|
|
|
impl ScrollBar {
|
|
pub fn new(height: f32, content_height: f32, style: ScrollBarStyle) -> Self {
|
|
let center_x = style.body_width / 2.0 + style.margin.x;
|
|
let rail_rect = Rect {
|
|
tl: Vec2::new(center_x - style.rail_width / 2.0, style.margin.y),
|
|
br: Vec2::new(center_x + style.rail_width / 2.0, height - style.margin.y),
|
|
};
|
|
|
|
let body_color_anim = Animation::new(
|
|
EaseInQuad,
|
|
0.05,
|
|
style.body_idle_color,
|
|
style.body_idle_color,
|
|
);
|
|
|
|
Self {
|
|
height,
|
|
style,
|
|
is_dirty: true,
|
|
scroll: 0.0,
|
|
content_height,
|
|
rail_rect,
|
|
body_color_anim,
|
|
is_hovering: false,
|
|
is_selected: false,
|
|
grab_coord: 0.0,
|
|
grab_scroll: 0.0,
|
|
}
|
|
}
|
|
|
|
pub fn get_body_rect(&self) -> Rect {
|
|
let style = &self.style;
|
|
let rail_height = self.rail_rect.height();
|
|
let body_height = (self.height / self.content_height) * rail_height;
|
|
let body_y = (self.scroll / self.content_height) * rail_height;
|
|
let body_xy = Vec2::new(style.margin.x, body_y + style.margin.y);
|
|
let body_size = Vec2::new(style.body_width, body_height);
|
|
Rect::from_xy_size(body_xy, body_size)
|
|
}
|
|
|
|
pub fn is_dirty(&self) -> bool {
|
|
self.is_dirty
|
|
}
|
|
|
|
pub fn get_scroll(&self) -> f32 {
|
|
self.scroll
|
|
}
|
|
}
|
|
|
|
impl Widget for ScrollBar {
|
|
fn update(&mut self, dt: f32) {
|
|
self.is_dirty = false;
|
|
self.body_color_anim.update(dt);
|
|
}
|
|
|
|
fn draw(&mut self, ctx: &DrawContext) {
|
|
let style = &self.style;
|
|
let body_rect = self.get_body_rect();
|
|
ctx.draw_rect(self.rail_rect, style.rail_color);
|
|
ctx.draw_rounded_rect(body_rect, style.body_radius, self.body_color_anim.get());
|
|
}
|
|
|
|
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
|
|
let is_on = self.get_body_rect().contains_point(at);
|
|
|
|
match kind {
|
|
CursorEventKind::Hover | CursorEventKind::Drag => {
|
|
if is_on {
|
|
if !self.is_hovering && !self.is_selected {
|
|
self.body_color_anim.ease_to(self.style.body_hover_color);
|
|
}
|
|
|
|
self.is_hovering = true;
|
|
} else {
|
|
if self.is_hovering && !self.is_selected {
|
|
self.body_color_anim.ease_to(self.style.body_idle_color);
|
|
}
|
|
|
|
self.is_hovering = false;
|
|
}
|
|
|
|
if kind == CursorEventKind::Drag && self.is_selected {
|
|
self.scroll = ((at.y - self.grab_coord) / self.rail_rect.height())
|
|
* self.content_height
|
|
+ self.grab_scroll;
|
|
|
|
let scroll_cap = self.content_height - self.height;
|
|
if self.scroll > scroll_cap {
|
|
self.scroll = scroll_cap;
|
|
} else if self.scroll < 0.0 {
|
|
self.scroll = 0.0;
|
|
}
|
|
|
|
self.is_dirty = true;
|
|
}
|
|
}
|
|
CursorEventKind::Select => {
|
|
if is_on {
|
|
self.is_selected = true;
|
|
self.body_color_anim.ease_to(self.style.body_selected_color);
|
|
self.grab_coord = at.y;
|
|
self.grab_scroll = self.scroll;
|
|
}
|
|
}
|
|
CursorEventKind::Deselect => {
|
|
if self.is_selected {
|
|
self.is_selected = false;
|
|
|
|
if self.is_hovering {
|
|
self.body_color_anim.ease_to(self.style.body_hover_color);
|
|
} else {
|
|
self.body_color_anim.ease_to(self.style.body_idle_color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ScrollView<T> {
|
|
inner: Offset<T>,
|
|
scroll_bar: Offset<ScrollBar>,
|
|
clip_rect: Rect,
|
|
height: f32,
|
|
content_height: f32,
|
|
}
|
|
|
|
impl<T: Widget> ScrollView<T> {
|
|
pub fn new(
|
|
bar_style: ScrollBarStyle,
|
|
width: f32,
|
|
height: f32,
|
|
inner_cb: impl FnOnce(f32) -> (T, f32),
|
|
) -> Self {
|
|
let content_width = width - bar_style.body_width - bar_style.margin.x * 2.0;
|
|
let (inner, content_height) = inner_cb(content_width);
|
|
let inner = Offset::new(inner, Vec2::ZERO);
|
|
|
|
let scroll_bar_offset = Vec2::new(content_width, 0.0);
|
|
let scroll_bar = ScrollBar::new(height, content_height, bar_style);
|
|
let scroll_bar = Offset::new(scroll_bar, scroll_bar_offset);
|
|
|
|
let clip_rect = Rect::from_xy_size(Vec2::ZERO, Vec2::new(content_width, height));
|
|
|
|
Self {
|
|
inner,
|
|
scroll_bar,
|
|
clip_rect,
|
|
content_height,
|
|
height,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: Widget> Widget for ScrollView<T> {
|
|
fn update(&mut self, dt: f32) {
|
|
if self.scroll_bar.is_dirty() {
|
|
let yoff = -self.scroll_bar.get_scroll();
|
|
self.inner.set_offset(Vec2::new(0.0, yoff));
|
|
}
|
|
|
|
self.inner.update(dt);
|
|
self.scroll_bar.update(dt);
|
|
}
|
|
|
|
fn draw(&mut self, ctx: &DrawContext) {
|
|
self.scroll_bar.draw(ctx);
|
|
|
|
if let Some(ctx) = ctx.with_clip_rect(self.clip_rect) {
|
|
self.inner.draw(&ctx);
|
|
}
|
|
}
|
|
|
|
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
|
|
self.inner.on_cursor_event(kind, at);
|
|
self.scroll_bar.on_cursor_event(kind, at);
|
|
}
|
|
}
|