use std::{
cell::Cell,
hash::{DefaultHasher, Hash, Hasher},
marker::PhantomData,
rc::Rc,
};
use crate::{
buffer::Buffer,
prelude::{Direction, KeyModifiers, MouseEvent, Rect, Vec2},
term::backend::MouseEventKind,
widgets::{
Element, EventResult, LayoutNode, Scrollbar, ScrollbarState, Widget,
},
};
pub struct Scrollable<M: 'static = (), W = Element<M>> {
horizontal: Option<Element<M>>,
hor_state: Option<Rc<Cell<ScrollbarState>>>,
vertical: Option<Element<M>>,
ver_state: Option<Rc<Cell<ScrollbarState>>>,
child: Element<M>,
handle_scroll: bool,
scroll_dist: Vec2,
on_scroll_ver: Option<Box<dyn Fn(isize) -> M>>,
on_scroll_hor: Option<Box<dyn Fn(isize) -> M>>,
child_type: PhantomData<W>,
}
impl<M, W> Scrollable<M, W>
where
M: Clone + 'static,
{
pub fn new<T>(
child: T,
state: Rc<Cell<ScrollbarState>>,
dir: Direction,
) -> Self
where
T: Into<Element<M>>,
{
match dir {
Direction::Vertical => Self::vertical(child, state),
Direction::Horizontal => Self::horizontal(child, state),
}
}
pub fn vertical<T>(child: T, state: Rc<Cell<ScrollbarState>>) -> Self
where
T: Into<Element<M>>,
{
Self {
vertical: Some(Scrollbar::vertical(state.clone()).into()),
ver_state: Some(state),
horizontal: None,
hor_state: None,
child: child.into(),
handle_scroll: true,
scroll_dist: Vec2::new(1, 1),
on_scroll_ver: None,
on_scroll_hor: None,
child_type: PhantomData,
}
}
pub fn horizontal<T>(child: T, state: Rc<Cell<ScrollbarState>>) -> Self
where
T: Into<Element<M>>,
{
Self {
vertical: None,
ver_state: None,
horizontal: Some(Scrollbar::horizontal(state.clone()).into()),
hor_state: Some(state),
child: child.into(),
handle_scroll: true,
scroll_dist: Vec2::new(1, 1),
on_scroll_ver: None,
on_scroll_hor: None,
child_type: PhantomData,
}
}
pub fn both<T>(
child: T,
ver_state: Rc<Cell<ScrollbarState>>,
hor_state: Rc<Cell<ScrollbarState>>,
) -> Self
where
T: Into<Element<M>>,
{
Self {
vertical: Some(Scrollbar::vertical(ver_state.clone()).into()),
ver_state: Some(ver_state),
horizontal: Some(Scrollbar::horizontal(hor_state.clone()).into()),
hor_state: Some(hor_state),
child: child.into(),
handle_scroll: true,
scroll_dist: Vec2::new(1, 1),
on_scroll_ver: None,
on_scroll_hor: None,
child_type: PhantomData,
}
}
#[must_use]
pub fn scrollable(mut self, enabled: bool) -> Self {
self.handle_scroll = enabled;
self
}
#[must_use]
pub fn scroll_distance(mut self, distance: usize) -> Self {
self.scroll_dist.x = distance;
self.scroll_dist.y = distance;
self
}
#[must_use]
pub fn scroll_distance_x(mut self, distance: usize) -> Self {
self.scroll_dist.x = distance;
self
}
#[must_use]
pub fn scroll_distance_y(mut self, distance: usize) -> Self {
self.scroll_dist.y = distance;
self
}
#[must_use]
pub fn on_scroll<F>(mut self, response: F) -> Self
where
F: Fn(isize) -> M + 'static,
{
self.on_scroll_ver = Some(Box::new(response));
self
}
#[must_use]
pub fn on_scroll_horizontal<F>(mut self, response: F) -> Self
where
F: Fn(isize) -> M + 'static,
{
self.on_scroll_hor = Some(Box::new(response));
self
}
}
impl<M, W> Widget<M> for Scrollable<M, W>
where
M: Clone + 'static,
W: Widget<M>,
{
fn render(&self, buffer: &mut Buffer, layout: &LayoutNode) {
if layout.area.is_empty() {
return;
}
let child_node = &layout.children[0];
let ver_active = child_node.area.height() > layout.area.height();
let hor_active = child_node.area.width() > layout.area.width();
let mut viewport = layout.area;
if ver_active {
viewport.size.x = viewport.size.x.saturating_sub(1);
}
if hor_active {
viewport.size.y = viewport.size.y.saturating_sub(1);
}
let h = child_node.area.height();
let offset_y = Self::process_state(&self.ver_state, ver_active, h);
let w = child_node.area.height();
let offset_x = Self::process_state(&self.hor_state, hor_active, w);
let mut cid = 1;
let mut draw_scrollbar = |a: bool, e: &Option<Element<M>>, r: Rect| {
if a && let Some(e) = e {
let snode = &layout.children[cid];
Self::scrollbar(buffer, snode, e, r);
cid += 1;
}
};
let vrect = Rect::new(
layout.area.right(),
layout.area.y(),
1,
viewport.height(),
);
draw_scrollbar(ver_active, &self.vertical, vrect);
let hrect = Rect::new(
layout.area.x(),
layout.area.bottom(),
viewport.width(),
1,
);
draw_scrollbar(hor_active, &self.horizontal, hrect);
self.render_content(buffer, viewport, child_node, offset_x, offset_y);
}
fn height(&self, size: &Vec2) -> usize {
match (self.vertical.is_some(), self.horizontal.is_some()) {
(true, true) => self.child.height(&Vec2::new(
size.x.saturating_sub(1),
size.y.saturating_sub(1),
)),
(true, false) => size.y.min(
self.child
.height(&Vec2::new(size.x.saturating_sub(1), size.y))
+ 1,
),
(false, true) => {
self.child
.height(&Vec2::new(size.x, size.y.saturating_sub(1)))
+ 1
}
(false, false) => self.child.height(size),
}
}
fn width(&self, size: &Vec2) -> usize {
match (self.vertical.is_some(), self.horizontal.is_some()) {
(true, true) => self.child.width(&Vec2::new(
size.x.saturating_sub(1),
size.y.saturating_sub(1),
)),
(true, false) => {
self.child
.width(&Vec2::new(size.x.saturating_sub(1), size.y))
+ 1
}
(false, true) => size.x.min(
self.child
.width(&Vec2::new(size.x, size.y.saturating_sub(1)))
+ 1,
),
(false, false) => self.child.width(size),
}
}
fn children(&self) -> Vec<&Element<M>> {
std::iter::once(&self.child)
.chain(self.vertical.iter())
.chain(self.horizontal.iter())
.collect()
}
fn layout_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.horizontal.is_some().hash(&mut hasher);
self.vertical.is_some().hash(&mut hasher);
if let Some(state) = &self.hor_state {
state.get().hash(&mut hasher);
}
if let Some(state) = &self.ver_state {
state.get().hash(&mut hasher);
}
hasher.finish()
}
fn layout(&self, node: &mut LayoutNode, area: Rect) {
let has_ver = self.ver_state.is_some();
let has_hor = self.hor_state.is_some();
if !has_ver && !has_hor {
node.children[0].layout(&self.child, area);
return;
}
let mut size = area.size;
let mut ver_scroll = false;
let mut hor_scroll = false;
let calc_size = |size: &mut Vec2| {
if has_ver {
size.y = self.child.height(size);
}
if has_hor {
size.x = self.child.width(size);
}
};
let mut test_size = Vec2::new(
if has_hor { usize::MAX } else { size.x },
if has_ver { usize::MAX } else { size.y },
);
calc_size(&mut test_size);
if has_ver && test_size.y > size.y {
ver_scroll = true;
size.x = size.x.saturating_sub(1);
}
if has_hor && test_size.x > size.x {
hor_scroll = true;
size.y = size.y.saturating_sub(1);
if has_ver && !ver_scroll && test_size.y > size.y {
ver_scroll = true;
size.x = size.x.saturating_sub(1);
}
}
test_size = Vec2::new(
if has_hor { usize::MAX } else { size.x },
if has_ver { usize::MAX } else { size.y },
);
if ver_scroll || hor_scroll {
calc_size(&mut test_size);
}
let child_rect = Rect::new(
area.x(),
area.y(),
if hor_scroll { test_size.x } else { size.x },
if ver_scroll { test_size.y } else { size.y },
);
node.children[0].layout(&self.child, child_rect);
}
fn on_event(&self, node: &LayoutNode, e: &MouseEvent) -> EventResult<M> {
if !node.area.contains_pos(&e.pos) {
return EventResult::None;
}
self.child
.on_event(&node.children[0], e)
.or_else(|| self.handle_mouse(node.area, e))
}
}
impl<M, W> Scrollable<M, W>
where
M: Clone + 'static,
W: Widget<M>,
{
fn render_content(
&self,
buffer: &mut Buffer,
rect: Rect,
node: &LayoutNode,
offset_x: usize,
offset_y: usize,
) {
let mut cbuffer = Buffer::empty(node.area);
let mut mask = rect;
mask.pos.x += offset_x;
mask.pos.y += offset_y;
let mut cutout = buffer.subset(rect);
cutout.move_to(*mask.pos());
cbuffer.merge(cutout);
self.child.render(&mut cbuffer, node);
mask = mask.intersection(cbuffer.rect());
let mut cutout = cbuffer.subset(mask);
cutout.move_to(*rect.pos());
buffer.merge(cutout);
}
fn scrollbar(
buffer: &mut Buffer,
layout: &LayoutNode,
scroll: &Element<M>,
rect: Rect,
) {
let mut temp_node = layout.clone();
temp_node.area = rect;
scroll.render(buffer, &temp_node);
}
fn process_state(
state: &Option<Rc<Cell<ScrollbarState>>>,
is_active: bool,
content_len: usize,
) -> usize {
let Some(st) = state else {
return 0;
};
if is_active {
let mut s = st.get();
s.content_len = content_len;
st.set(s);
}
st.get().offset
}
fn handle_mouse(&self, area: Rect, event: &MouseEvent) -> EventResult<M> {
if !self.handle_scroll {
return EventResult::None;
}
use MouseEventKind::*;
let dx = self.scroll_dist.x as isize;
let dy = self.scroll_dist.y as isize;
match &event.kind {
ScrollDown if event.modifiers.contains(KeyModifiers::SHIFT) => {
self.hor_move_offset(area, dx)
}
ScrollUp if event.modifiers.contains(KeyModifiers::SHIFT) => {
self.hor_move_offset(area, -dx)
}
ScrollDown => self.ver_move_offset(area, dy),
ScrollUp => self.ver_move_offset(area, -dy),
ScrollLeft => self.hor_move_offset(area, -dx),
ScrollRight => self.hor_move_offset(area, dx),
_ => EventResult::None,
}
}
fn ver_move_offset(&self, area: Rect, delta: isize) -> EventResult<M> {
let scroll = || {
let height = area
.height()
.saturating_sub(self.horizontal.is_some() as usize);
self.apply_scroll(&self.ver_state, height, delta);
};
self.handle_scroll(&self.on_scroll_ver, scroll, delta)
}
fn hor_move_offset(&self, area: Rect, delta: isize) -> EventResult<M> {
let scroll = || {
let width = area
.width()
.saturating_sub(self.vertical.is_some() as usize);
self.apply_scroll(&self.hor_state, width, delta);
};
self.handle_scroll(&self.on_scroll_hor, scroll, delta)
}
fn handle_scroll<F>(
&self,
handler: &Option<Box<dyn Fn(isize) -> M>>,
scroll: F,
delta: isize,
) -> EventResult<M>
where
F: Fn(),
{
if let Some(handler) = handler {
return EventResult::Response(handler(delta));
}
if !self.handle_scroll {
return EventResult::None;
}
scroll();
EventResult::Consumed
}
fn apply_scroll(
&self,
scrollbar: &Option<Rc<Cell<ScrollbarState>>>,
size: usize,
delta: isize,
) {
if let Some(state) = &scrollbar {
let s = state.get();
state.set(s.offset(Self::get_offset(&s, delta, size)));
};
}
fn get_offset(state: &ScrollbarState, delta: isize, size: usize) -> usize {
if delta < 0 {
state.offset.saturating_sub(delta.unsigned_abs())
} else {
(state.offset + delta as usize)
.min(state.content_len.saturating_sub(size))
}
}
}
impl<M, W> From<Scrollable<M, W>> for Box<dyn Widget<M>>
where
M: Clone + 'static,
W: Widget<M> + 'static,
{
fn from(value: Scrollable<M, W>) -> Self {
Box::new(value)
}
}
impl<M, W> From<Scrollable<M, W>> for Element<M>
where
M: Clone + 'static,
W: Widget<M> + 'static,
{
fn from(value: Scrollable<M, W>) -> Self {
Element::new(value)
}
}