canary-rs/scripts/sao-ui/src/widgets/scroll.rs

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);
}
}