use super::{BorderChars, Container, Widget};
use crate::widgets::common::WindowView;
use crate::widgets::scroll::{ScrollOrientation, ScrollSize, ScrollState};
use crate::{ColorPair, Result, Window};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StickyEdge {
Top,
Bottom,
Left,
Right,
}
pub trait ScrollAcceleration: Send + Sync {
fn tick(&mut self, now_ms: u128) -> f32;
fn reset(&mut self);
}
pub struct LinearScrollAccel {
base_speed: f32,
}
impl LinearScrollAccel {
pub fn new() -> Self {
Self { base_speed: 1.0 }
}
pub fn with_speed(mut self, speed: f32) -> Self {
self.base_speed = speed;
self
}
}
impl Default for LinearScrollAccel {
fn default() -> Self {
Self::new()
}
}
impl ScrollAcceleration for LinearScrollAccel {
fn tick(&mut self, _now_ms: u128) -> f32 {
self.base_speed
}
fn reset(&mut self) {}
}
pub struct MacOSScrollAccel {
velocity: f32,
friction: f32,
last_tick_ms: u128,
}
impl MacOSScrollAccel {
pub fn new() -> Self {
Self {
velocity: 1.0,
friction: 0.1, last_tick_ms: 0,
}
}
pub fn with_friction(mut self, friction: f32) -> Self {
self.friction = friction.max(0.0).min(1.0);
self
}
}
impl Default for MacOSScrollAccel {
fn default() -> Self {
Self::new()
}
}
impl ScrollAcceleration for MacOSScrollAccel {
fn tick(&mut self, now_ms: u128) -> f32 {
if self.last_tick_ms == 0 {
self.last_tick_ms = now_ms;
return self.velocity;
}
let elapsed_ms = now_ms.saturating_sub(self.last_tick_ms);
let elapsed_s = (elapsed_ms as f32) / 1000.0;
self.velocity *= (1.0 - self.friction).powf(elapsed_s);
self.last_tick_ms = now_ms;
self.velocity.max(0.1)
}
fn reset(&mut self) {
self.velocity = 1.0;
self.last_tick_ms = 0;
}
}
pub struct ScrollBox {
root: Container,
state: Rc<RefCell<ScrollState>>,
viewport_x: u16,
viewport_y: u16,
viewport_width: u16,
viewport_height: u16,
scroll_x_enabled: bool,
scroll_y_enabled: bool,
scroll_accumulator_x: f32,
scroll_accumulator_y: f32,
scroll_acceleration: Box<dyn ScrollAcceleration>,
sticky_scroll: bool,
sticky_start: Option<StickyEdge>,
has_manual_scroll: bool,
is_applying_sticky: bool,
auto_scrolling: bool,
auto_scroll_mouse_x: u16,
auto_scroll_mouse_y: u16,
auto_scroll_accumulator_x: f32,
auto_scroll_accumulator_y: f32,
auto_scroll_threshold: u16,
viewport_culling: bool,
}
impl ScrollBox {
pub fn new() -> Self {
let state = Rc::new(RefCell::new(ScrollState::new(
ScrollSize::new(0, 0),
ScrollSize::new(0, 0),
)));
Self {
root: Container::new(),
state,
viewport_x: 0,
viewport_y: 0,
viewport_width: 0,
viewport_height: 0,
scroll_x_enabled: false,
scroll_y_enabled: true,
scroll_accumulator_x: 0.0,
scroll_accumulator_y: 0.0,
scroll_acceleration: Box::new(LinearScrollAccel::new()),
sticky_scroll: false,
sticky_start: None,
has_manual_scroll: false,
is_applying_sticky: false,
auto_scrolling: false,
auto_scroll_mouse_x: 0,
auto_scroll_mouse_y: 0,
auto_scroll_accumulator_x: 0.0,
auto_scroll_accumulator_y: 0.0,
auto_scroll_threshold: 3,
viewport_culling: true,
}
}
pub fn state(&self) -> Rc<RefCell<ScrollState>> {
Rc::clone(&self.state)
}
pub fn with_state(mut self, state: Rc<RefCell<ScrollState>>) -> Self {
self.state = state;
self
}
pub fn with_position_and_size(mut self, x: u16, y: u16, width: u16, height: u16) -> Self {
self.root = self.root.with_position_and_size(x, y, width, height);
self
}
pub fn vertical() -> Self {
Self::new().with_scroll_y(true)
}
pub fn horizontal() -> Self {
Self::new().with_scroll_x(true)
}
pub fn both() -> Self {
Self::new().with_scroll_x(true).with_scroll_y(true)
}
pub fn with_layout_direction(mut self, direction: super::container::LayoutDirection) -> Self {
self.root = self.root.with_layout_direction(direction);
self
}
pub fn with_padding(mut self, padding: super::container::Padding) -> Self {
self.root = self.root.with_padding(padding);
self
}
pub fn with_gap(mut self, gap: super::container::Gap) -> Self {
self.root = self.root.with_gap(gap);
self
}
pub fn with_row_gap(mut self, gap: super::container::Gap) -> Self {
self.root = self.root.with_row_gap(gap);
self
}
pub fn with_column_gap(mut self, gap: super::container::Gap) -> Self {
self.root = self.root.with_column_gap(gap);
self
}
pub fn with_content_alignment(mut self, alignment: super::container::ContentAlignment) -> Self {
self.root = self.root.with_content_alignment(alignment);
self
}
pub fn with_border_sides(mut self, sides: Vec<super::container::BorderSide>) -> Self {
self.root = self.root.with_border_sides(sides);
self
}
pub fn with_border(mut self) -> Self {
self.root = self.root.with_border();
self
}
pub fn without_border(mut self) -> Self {
self.root = self.root.without_border();
self
}
pub fn with_border_chars(mut self, chars: BorderChars) -> Self {
self.root = self.root.with_border_chars(chars);
self
}
pub fn with_border_color(mut self, color: ColorPair) -> Self {
self.root = self.root.with_border_color(color);
self
}
pub fn with_focused_border_color(mut self, color: ColorPair) -> Self {
self.root = self.root.with_focused_border_color(color);
self
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.root = self.root.with_title(title);
self
}
pub fn with_title_alignment(mut self, alignment: super::container::TitleAlignment) -> Self {
self.root = self.root.with_title_alignment(alignment);
self
}
pub fn with_background_color(mut self, color: ColorPair) -> Self {
self.root = self.root.with_background_color(color);
self
}
pub fn add_child(mut self, child: impl Widget + 'static) -> Self {
self.root = self.root.add_child(child);
self
}
pub fn with_scroll_y(mut self, enabled: bool) -> Self {
self.scroll_y_enabled = enabled;
self
}
pub fn with_scroll_x(mut self, enabled: bool) -> Self {
self.scroll_x_enabled = enabled;
self
}
pub fn with_sticky_scroll(mut self, enabled: bool) -> Self {
self.sticky_scroll = enabled;
self
}
pub fn with_sticky_start(mut self, edge: StickyEdge) -> Self {
self.sticky_start = Some(edge);
self
}
pub fn with_viewport_culling(mut self, enabled: bool) -> Self {
self.viewport_culling = enabled;
self
}
pub fn with_auto_scroll_threshold(mut self, threshold: u16) -> Self {
self.auto_scroll_threshold = threshold;
self
}
pub fn with_scroll_acceleration(mut self, accel: Box<dyn ScrollAcceleration>) -> Self {
self.scroll_acceleration = accel;
self
}
pub fn scroll_y(&self) -> u16 {
self.state.borrow().offset_for(ScrollOrientation::Vertical)
}
pub fn set_scroll_y(&mut self, position: u16) {
self.state
.borrow_mut()
.set_offset_for(ScrollOrientation::Vertical, position);
self.mark_manual_scroll();
self.update_sticky_state();
}
pub fn scroll_x(&self) -> u16 {
self.state
.borrow()
.offset_for(ScrollOrientation::Horizontal)
}
pub fn set_scroll_x(&mut self, position: u16) {
self.state
.borrow_mut()
.set_offset_for(ScrollOrientation::Horizontal, position);
self.mark_manual_scroll();
self.update_sticky_state();
}
pub fn scrollable_width(&self) -> u16 {
self.state
.borrow()
.max_offset_for(ScrollOrientation::Horizontal)
}
pub fn scrollable_height(&self) -> u16 {
self.state
.borrow()
.max_offset_for(ScrollOrientation::Vertical)
}
pub fn scroll_by(&mut self, delta_x: i16, delta_y: i16) {
if delta_x != 0 && self.scroll_x_enabled {
self.state
.borrow_mut()
.scroll_by(ScrollOrientation::Horizontal, delta_x);
self.mark_manual_scroll();
self.update_sticky_state();
}
if delta_y != 0 && self.scroll_y_enabled {
self.state
.borrow_mut()
.scroll_by(ScrollOrientation::Vertical, delta_y);
self.mark_manual_scroll();
self.update_sticky_state();
}
}
pub fn scroll_to(&mut self, x: u16, y: u16) {
if self.scroll_x_enabled {
self.state
.borrow_mut()
.set_offset_for(ScrollOrientation::Horizontal, x);
}
if self.scroll_y_enabled {
self.state
.borrow_mut()
.set_offset_for(ScrollOrientation::Vertical, y);
}
self.mark_manual_scroll();
self.update_sticky_state();
}
pub fn scroll_to_top(&mut self) {
self.set_scroll_y(0);
}
pub fn scroll_to_bottom(&mut self) {
self.set_scroll_y(self.scrollable_height());
}
pub fn scroll_to_left(&mut self) {
self.set_scroll_x(0);
}
pub fn scroll_to_right(&mut self) {
self.set_scroll_x(self.scrollable_width());
}
pub fn handle_mouse_scroll(&mut self, delta_y: i16, delta_x: i16) {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis();
let multiplier = self.scroll_acceleration.tick(now_ms);
let scroll_y = (delta_y as f32) * multiplier;
let scroll_x = (delta_x as f32) * multiplier;
self.scroll_accumulator_y += scroll_y;
let integer_scroll_y = self.scroll_accumulator_y.trunc() as i16;
if integer_scroll_y != 0 {
self.scroll_by(0, integer_scroll_y);
self.scroll_accumulator_y -= integer_scroll_y as f32;
}
self.scroll_accumulator_x += scroll_x;
let integer_scroll_x = self.scroll_accumulator_x.trunc() as i16;
if integer_scroll_x != 0 {
self.scroll_by(integer_scroll_x, 0);
self.scroll_accumulator_x -= integer_scroll_x as f32;
}
}
pub fn handle_drag(&mut self, mouse_x: u16, mouse_y: u16) {
self.auto_scroll_mouse_x = mouse_x;
self.auto_scroll_mouse_y = mouse_y;
let scroll_dir_x = self.get_auto_scroll_direction_x(mouse_x);
let scroll_dir_y = self.get_auto_scroll_direction_y(mouse_y);
if scroll_dir_x == 0 && scroll_dir_y == 0 {
self.stop_auto_scroll();
} else if !self.auto_scrolling {
self.start_auto_scroll();
}
}
pub fn stop_auto_scroll(&mut self) {
self.auto_scrolling = false;
self.auto_scroll_accumulator_x = 0.0;
self.auto_scroll_accumulator_y = 0.0;
}
pub fn update_auto_scroll(&mut self, delta_time_s: f32) {
if !self.auto_scrolling {
return;
}
let dir_x = self.get_auto_scroll_direction_x(self.auto_scroll_mouse_x);
let dir_y = self.get_auto_scroll_direction_y(self.auto_scroll_mouse_y);
let speed = self.get_auto_scroll_speed(self.auto_scroll_mouse_x, self.auto_scroll_mouse_y);
let scroll_amount = speed * delta_time_s;
if dir_x != 0 {
self.auto_scroll_accumulator_x += (dir_x as f32) * scroll_amount;
let integer_scroll = self.auto_scroll_accumulator_x.trunc() as i16;
if integer_scroll != 0 {
self.scroll_by(integer_scroll, 0);
self.auto_scroll_accumulator_x -= integer_scroll as f32;
}
}
if dir_y != 0 {
self.auto_scroll_accumulator_y += (dir_y as f32) * scroll_amount;
let integer_scroll = self.auto_scroll_accumulator_y.trunc() as i16;
if integer_scroll != 0 {
self.scroll_by(0, integer_scroll);
self.auto_scroll_accumulator_y -= integer_scroll as f32;
}
}
if dir_x == 0 && dir_y == 0 {
self.stop_auto_scroll();
}
}
fn start_auto_scroll(&mut self) {
self.auto_scrolling = true;
}
fn get_auto_scroll_direction_x(&self, mouse_x: u16) -> i8 {
if mouse_x < self.viewport_x {
return 0;
}
if mouse_x > self.viewport_x + self.viewport_width {
return 0;
}
let relative_x = mouse_x - self.viewport_x;
let s = self.state.borrow();
let pos = s.offset_for(ScrollOrientation::Horizontal);
let max = s.max_offset_for(ScrollOrientation::Horizontal);
if relative_x < self.auto_scroll_threshold {
return if pos > 0 { -1 } else { 0 };
}
if relative_x
> self
.viewport_width
.saturating_sub(self.auto_scroll_threshold)
{
return if pos < max { 1 } else { 0 };
}
0
}
fn get_auto_scroll_direction_y(&self, mouse_y: u16) -> i8 {
if mouse_y < self.viewport_y {
return 0;
}
if mouse_y > self.viewport_y + self.viewport_height {
return 0;
}
let relative_y = mouse_y - self.viewport_y;
let s = self.state.borrow();
let pos = s.offset_for(ScrollOrientation::Vertical);
let max = s.max_offset_for(ScrollOrientation::Vertical);
if relative_y < self.auto_scroll_threshold {
return if pos > 0 { -1 } else { 0 };
}
if relative_y
> self
.viewport_height
.saturating_sub(self.auto_scroll_threshold)
{
return if pos < max { 1 } else { 0 };
}
0
}
fn get_auto_scroll_speed(&self, mouse_x: u16, mouse_y: u16) -> f32 {
let relative_x = if mouse_x > self.viewport_x {
(mouse_x - self.viewport_x) as i32
} else {
0
};
let relative_y = if mouse_y > self.viewport_y {
(mouse_y - self.viewport_y) as i32
} else {
0
};
let dist_to_left = relative_x;
let dist_to_right = (self.viewport_width as i32) - relative_x;
let dist_to_top = relative_y;
let dist_to_bottom = (self.viewport_height as i32) - relative_y;
let min_dist = std::cmp::min(
std::cmp::min(dist_to_left, dist_to_right),
std::cmp::min(dist_to_top, dist_to_bottom),
);
match min_dist {
d if d <= 1 => 72.0, d if d <= 2 => 36.0, _ => 6.0, }
}
fn mark_manual_scroll(&mut self) {
if !self.is_applying_sticky {
if !self.is_at_sticky_position()
&& self
.state
.borrow()
.max_offset_for(ScrollOrientation::Vertical)
> 1
{
self.has_manual_scroll = true;
}
}
}
fn is_at_sticky_position(&self) -> bool {
if !self.sticky_scroll || self.sticky_start.is_none() {
return false;
}
let s = self.state.borrow();
match self.sticky_start {
Some(StickyEdge::Top) => s.offset_for(ScrollOrientation::Vertical) == 0,
Some(StickyEdge::Bottom) => {
s.offset_for(ScrollOrientation::Vertical)
>= s.max_offset_for(ScrollOrientation::Vertical)
}
Some(StickyEdge::Left) => s.offset_for(ScrollOrientation::Horizontal) == 0,
Some(StickyEdge::Right) => {
s.offset_for(ScrollOrientation::Horizontal)
>= s.max_offset_for(ScrollOrientation::Horizontal)
}
None => false,
}
}
fn update_sticky_state(&mut self) {
if !self.sticky_scroll {
return;
}
let max_y = self.scrollable_height();
let max_x = self.scrollable_width();
let scroll_y = self.scroll_y();
let scroll_x = self.scroll_x();
if scroll_y == 0 {
} else if scroll_y >= max_y {
}
if scroll_x == 0 {
} else if scroll_x >= max_x {
}
}
#[allow(dead_code)]
fn apply_sticky_start(&mut self, edge: StickyEdge) {
self.is_applying_sticky = true;
match edge {
StickyEdge::Top => self
.state
.borrow_mut()
.scroll_to_start(ScrollOrientation::Vertical),
StickyEdge::Bottom => self
.state
.borrow_mut()
.scroll_to_end(ScrollOrientation::Vertical),
StickyEdge::Left => self
.state
.borrow_mut()
.scroll_to_start(ScrollOrientation::Horizontal),
StickyEdge::Right => self
.state
.borrow_mut()
.scroll_to_end(ScrollOrientation::Horizontal),
}
self.is_applying_sticky = false;
}
}
impl Default for ScrollBox {
fn default() -> Self {
Self::new()
}
}
impl Widget for ScrollBox {
fn draw(&self, window: &mut dyn Window) -> Result<()> {
let (px, py) = self.root.get_position();
let (pw, ph) = self.root.get_size();
self.root.draw_frame(window)?;
let (_cx_abs, _cy_abs, cw, ch) = self.root.content_area();
let viewport_size = ScrollSize::new(cw, ch);
let mut content_required_w: u16 = 0;
let mut content_required_h: u16 = 0;
let children = self.root.children();
if children.is_empty() {
content_required_w = viewport_size.width;
content_required_h = viewport_size.height;
} else {
let gap_pixels: u16 = self.root.autosize_gap_pixels();
match self.root.layout_direction() {
super::container::LayoutDirection::Vertical => {
for (idx, child) in children.iter().enumerate() {
let (cw, ch) = child.get_size();
content_required_w = content_required_w.max(cw);
content_required_h = content_required_h.saturating_add(ch);
if idx < children.len() - 1 {
content_required_h = content_required_h.saturating_add(gap_pixels);
}
}
}
super::container::LayoutDirection::Horizontal => {
for (idx, child) in children.iter().enumerate() {
let (cw, ch) = child.get_size();
content_required_w = content_required_w.saturating_add(cw);
content_required_h = content_required_h.max(ch);
if idx < children.len() - 1 {
content_required_w = content_required_w.saturating_add(gap_pixels);
}
}
}
}
content_required_w = content_required_w.max(viewport_size.width);
content_required_h = content_required_h.max(viewport_size.height);
}
let content_size = ScrollSize::new(content_required_w, content_required_h);
{
let mut s = self.state.borrow_mut();
s.set_viewport_size(viewport_size);
s.set_content_size(content_size);
}
let offset = self.state.borrow().offset();
let (content_x, content_y, content_w, content_h) = self.root.content_area();
let mut content_view = WindowView {
window,
x_offset: content_x,
y_offset: content_y,
scroll_x: if self.scroll_x_enabled { offset.x } else { 0 },
scroll_y: if self.scroll_y_enabled { offset.y } else { 0 },
width: content_w,
height: content_h,
};
self.root.draw_contents(&mut content_view)?;
let _ = (px, py, pw, ph);
Ok(())
}
fn get_size(&self) -> (u16, u16) {
self.root.get_size()
}
fn get_position(&self) -> (u16, u16) {
self.root.get_position()
}
}