use std::f64::INFINITY;
use std::time::Duration;
use crate::kurbo::{Affine, Point, Rect, RoundedRect, Size, Vec2};
use crate::theme;
use crate::{
BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
RenderContext, TimerToken, UpdateCtx, Widget, WidgetPod,
};
const SCROLLBAR_MIN_SIZE: f64 = 45.0;
#[derive(Debug, Clone)]
enum ScrollDirection {
Horizontal,
Vertical,
All,
}
impl ScrollDirection {
pub fn max_size(&self, bc: &BoxConstraints) -> Size {
match self {
ScrollDirection::Horizontal => Size::new(INFINITY, bc.max().height),
ScrollDirection::Vertical => Size::new(bc.max().width, INFINITY),
ScrollDirection::All => Size::new(INFINITY, INFINITY),
}
}
}
enum BarHoveredState {
None,
Vertical,
Horizontal,
}
impl BarHoveredState {
fn is_hovered(&self) -> bool {
match self {
BarHoveredState::Vertical | BarHoveredState::Horizontal => true,
_ => false,
}
}
}
enum BarHeldState {
None,
Vertical(f64),
Horizontal(f64),
}
struct ScrollbarsState {
opacity: f64,
timer_id: TimerToken,
hovered: BarHoveredState,
held: BarHeldState,
}
impl Default for ScrollbarsState {
fn default() -> Self {
Self {
opacity: 0.0,
timer_id: TimerToken::INVALID,
hovered: BarHoveredState::None,
held: BarHeldState::None,
}
}
}
impl ScrollbarsState {
fn are_held(&self) -> bool {
match self.held {
BarHeldState::None => false,
_ => true,
}
}
}
pub struct Scroll<T, W> {
child: WidgetPod<T, W>,
child_size: Size,
scroll_offset: Vec2,
direction: ScrollDirection,
scrollbars: ScrollbarsState,
}
impl<T, W: Widget<T>> Scroll<T, W> {
pub fn new(child: W) -> Scroll<T, W> {
Scroll {
child: WidgetPod::new(child),
child_size: Default::default(),
scroll_offset: Vec2::new(0.0, 0.0),
direction: ScrollDirection::All,
scrollbars: ScrollbarsState::default(),
}
}
pub fn vertical(mut self) -> Self {
self.direction = ScrollDirection::Vertical;
self
}
pub fn horizontal(mut self) -> Self {
self.direction = ScrollDirection::Horizontal;
self
}
pub fn child(&self) -> &W {
self.child.widget()
}
pub fn child_mut(&mut self) -> &mut W {
self.child.widget_mut()
}
pub fn child_size(&self) -> Size {
self.child_size
}
pub fn scroll(&mut self, delta: Vec2, size: Size) -> bool {
let mut offset = self.scroll_offset + delta;
offset.x = offset.x.min(self.child_size.width - size.width).max(0.0);
offset.y = offset.y.min(self.child_size.height - size.height).max(0.0);
if (offset - self.scroll_offset).hypot2() > 1e-12 {
self.scroll_offset = offset;
self.child.set_viewport_offset(offset);
true
} else {
false
}
}
pub fn reset_scrollbar_fade<F>(&mut self, request_timer: F, env: &Env)
where
F: FnOnce(Duration) -> TimerToken,
{
self.scrollbars.opacity = env.get(theme::SCROLLBAR_MAX_OPACITY);
let fade_delay = env.get(theme::SCROLLBAR_FADE_DELAY);
let deadline = Duration::from_millis(fade_delay);
self.scrollbars.timer_id = request_timer(deadline);
}
pub fn offset(&self) -> Vec2 {
self.scroll_offset
}
fn calc_vertical_bar_bounds(&self, viewport: Rect, env: &Env) -> Rect {
let bar_width = env.get(theme::SCROLLBAR_WIDTH);
let bar_pad = env.get(theme::SCROLLBAR_PAD);
let percent_visible = viewport.height() / self.child_size.height;
let percent_scrolled = self.scroll_offset.y / (self.child_size.height - viewport.height());
let length = (percent_visible * viewport.height()).ceil();
let length = length.max(SCROLLBAR_MIN_SIZE);
let vertical_padding = bar_pad + bar_pad + bar_width;
let top_y_offset =
((viewport.height() - length - vertical_padding) * percent_scrolled).ceil();
let bottom_y_offset = top_y_offset + length;
let x0 = self.scroll_offset.x + viewport.width() - bar_width - bar_pad;
let y0 = self.scroll_offset.y + top_y_offset + bar_pad;
let x1 = self.scroll_offset.x + viewport.width() - bar_pad;
let y1 = self.scroll_offset.y + bottom_y_offset;
Rect::new(x0, y0, x1, y1)
}
fn calc_horizontal_bar_bounds(&self, viewport: Rect, env: &Env) -> Rect {
let bar_width = env.get(theme::SCROLLBAR_WIDTH);
let bar_pad = env.get(theme::SCROLLBAR_PAD);
let percent_visible = viewport.width() / self.child_size.width;
let percent_scrolled = self.scroll_offset.x / (self.child_size.width - viewport.width());
let length = (percent_visible * viewport.width()).ceil();
let length = length.max(SCROLLBAR_MIN_SIZE);
let horizontal_padding = bar_pad + bar_pad + bar_width;
let left_x_offset =
((viewport.width() - length - horizontal_padding) * percent_scrolled).ceil();
let right_x_offset = left_x_offset + length;
let x0 = self.scroll_offset.x + left_x_offset + bar_pad;
let y0 = self.scroll_offset.y + viewport.height() - bar_width - bar_pad;
let x1 = self.scroll_offset.x + right_x_offset;
let y1 = self.scroll_offset.y + viewport.height() - bar_pad;
Rect::new(x0, y0, x1, y1)
}
fn draw_bars(&self, ctx: &mut PaintCtx, viewport: Rect, env: &Env) {
if self.scrollbars.opacity <= 0.0 {
return;
}
let brush = ctx.render_ctx.solid_brush(
env.get(theme::SCROLLBAR_COLOR)
.with_alpha(self.scrollbars.opacity),
);
let border_brush = ctx.render_ctx.solid_brush(
env.get(theme::SCROLLBAR_BORDER_COLOR)
.with_alpha(self.scrollbars.opacity),
);
let radius = env.get(theme::SCROLLBAR_RADIUS);
let edge_width = env.get(theme::SCROLLBAR_EDGE_WIDTH);
if viewport.height() < self.child_size.height {
let bounds = self
.calc_vertical_bar_bounds(viewport, env)
.inset(-edge_width / 2.0);
let rect = RoundedRect::from_rect(bounds, radius);
ctx.render_ctx.fill(rect, &brush);
ctx.render_ctx.stroke(rect, &border_brush, edge_width);
}
if viewport.width() < self.child_size.width {
let bounds = self
.calc_horizontal_bar_bounds(viewport, env)
.inset(-edge_width / 2.0);
let rect = RoundedRect::from_rect(bounds, radius);
ctx.render_ctx.fill(rect, &brush);
ctx.render_ctx.stroke(rect, &border_brush, edge_width);
}
}
fn point_hits_vertical_bar(&self, viewport: Rect, pos: Point, env: &Env) -> bool {
if viewport.height() < self.child_size.height {
let mut bounds = self.calc_vertical_bar_bounds(viewport, env);
bounds.x1 = self.scroll_offset.x + viewport.width();
bounds.contains(pos)
} else {
false
}
}
fn point_hits_horizontal_bar(&self, viewport: Rect, pos: Point, env: &Env) -> bool {
if viewport.width() < self.child_size.width {
let mut bounds = self.calc_horizontal_bar_bounds(viewport, env);
bounds.y1 = self.scroll_offset.y + viewport.height();
bounds.contains(pos)
} else {
false
}
}
}
impl<T: Data, W: Widget<T>> Widget<T> for Scroll<T, W> {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
let size = ctx.size();
let viewport = Rect::from_origin_size(Point::ORIGIN, size);
let scrollbar_is_hovered = match event {
Event::MouseMove(e) | Event::MouseUp(e) | Event::MouseDown(e) => {
let offset_pos = e.pos + self.scroll_offset;
self.point_hits_vertical_bar(viewport, offset_pos, env)
|| self.point_hits_horizontal_bar(viewport, offset_pos, env)
}
_ => false,
};
if self.scrollbars.are_held() {
match event {
Event::MouseMove(event) => {
match self.scrollbars.held {
BarHeldState::Vertical(offset) => {
let scale_y = viewport.height() / self.child_size.height;
let bounds = self.calc_vertical_bar_bounds(viewport, env);
let mouse_y = event.pos.y + self.scroll_offset.y;
let delta = mouse_y - bounds.y0 - offset;
self.scroll(Vec2::new(0f64, (delta / scale_y).ceil()), size);
}
BarHeldState::Horizontal(offset) => {
let scale_x = viewport.width() / self.child_size.width;
let bounds = self.calc_horizontal_bar_bounds(viewport, env);
let mouse_x = event.pos.x + self.scroll_offset.x;
let delta = mouse_x - bounds.x0 - offset;
self.scroll(Vec2::new((delta / scale_x).ceil(), 0f64), size);
}
_ => (),
}
ctx.request_paint();
}
Event::MouseUp(_) => {
self.scrollbars.held = BarHeldState::None;
ctx.set_active(false);
if !scrollbar_is_hovered {
self.scrollbars.hovered = BarHoveredState::None;
self.reset_scrollbar_fade(|d| ctx.request_timer(d), env);
}
}
_ => (),
}
} else if scrollbar_is_hovered {
match event {
Event::MouseMove(event) => {
let offset_pos = event.pos + self.scroll_offset;
if self.point_hits_vertical_bar(viewport, offset_pos, env) {
self.scrollbars.hovered = BarHoveredState::Vertical;
} else {
self.scrollbars.hovered = BarHoveredState::Horizontal;
}
self.scrollbars.opacity = env.get(theme::SCROLLBAR_MAX_OPACITY);
self.scrollbars.timer_id = TimerToken::INVALID;
ctx.request_paint();
}
Event::MouseDown(event) => {
let pos = event.pos + self.scroll_offset;
if self.point_hits_vertical_bar(viewport, pos, env) {
ctx.set_active(true);
self.scrollbars.held = BarHeldState::Vertical(
pos.y - self.calc_vertical_bar_bounds(viewport, env).y0,
);
} else if self.point_hits_horizontal_bar(viewport, pos, env) {
ctx.set_active(true);
self.scrollbars.held = BarHeldState::Horizontal(
pos.x - self.calc_horizontal_bar_bounds(viewport, env).x0,
);
}
}
Event::MouseUp(_) => (),
_ => unreachable!(),
}
} else {
let force_event = self.child.is_hot() || self.child.is_active();
let child_event = event.transform_scroll(self.scroll_offset, viewport, force_event);
if let Some(child_event) = child_event {
self.child.event(ctx, &child_event, data, env);
};
match event {
Event::MouseMove(_) => {
if self.scrollbars.hovered.is_hovered() && !scrollbar_is_hovered {
self.scrollbars.hovered = BarHoveredState::None;
self.reset_scrollbar_fade(|d| ctx.request_timer(d), env);
}
}
Event::Timer(id) if *id == self.scrollbars.timer_id => {
ctx.request_anim_frame();
self.scrollbars.timer_id = TimerToken::INVALID;
}
_ => (),
}
}
if !ctx.is_handled() {
if let Event::Wheel(mouse) = event {
if self.scroll(mouse.wheel_delta, size) {
ctx.request_paint();
ctx.set_handled();
self.reset_scrollbar_fade(|d| ctx.request_timer(d), env);
}
}
}
}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
match event {
LifeCycle::AnimFrame(interval) => {
if self.scrollbars.timer_id == TimerToken::INVALID {
let diff = 2.0 * (*interval as f64) * 1e-9;
self.scrollbars.opacity -= diff;
if self.scrollbars.opacity > 0.0 {
ctx.request_anim_frame();
}
}
}
LifeCycle::Size(_) => self.reset_scrollbar_fade(|d| ctx.request_timer(d), &env),
_ => (),
}
self.child.lifecycle(ctx, event, data, env)
}
fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) {
self.child.update(ctx, data, env);
}
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
bc.debug_check("Scroll");
let child_bc = BoxConstraints::new(Size::ZERO, self.direction.max_size(bc));
let size = self.child.layout(ctx, &child_bc, data, env);
log_size_warnings(size);
self.child_size = size;
self.child.set_layout_rect(ctx, data, env, size.to_rect());
let self_size = bc.constrain(self.child_size);
let _ = self.scroll(Vec2::new(0.0, 0.0), self_size);
self_size
}
fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
let viewport = ctx.size().to_rect();
ctx.with_save(|ctx| {
ctx.clip(viewport);
ctx.transform(Affine::translate(-self.scroll_offset));
let visible = ctx.region().to_rect() + self.scroll_offset;
ctx.with_child_ctx(visible, |ctx| self.child.paint_raw(ctx, data, env));
self.draw_bars(ctx, viewport, env);
});
}
}
fn log_size_warnings(size: Size) {
if size.width.is_infinite() {
log::warn!("Scroll widget's child has an infinite width.");
}
if size.height.is_infinite() {
log::warn!("Scroll widget's child has an infinite height.");
}
}