// 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, 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 { inner: Offset, scroll_bar: Offset, clip_rect: Rect, height: f32, content_height: f32, } impl ScrollView { 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 Widget for ScrollView { 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); } }