use crate::event::ScrollOutcome;
use crate::{Scroll, ScrollState, ScrollbarPolicy};
use rat_event::{ct_event, flow, HandleEvent, MouseOnly};
use ratatui::buffer::Buffer;
use ratatui::layout::{Position, Rect};
#[cfg(feature = "unstable-widget-ref")]
use ratatui::widgets::StatefulWidgetRef;
use ratatui::widgets::{Block, ScrollbarOrientation, StatefulWidget, Widget};
#[derive(Debug, Default, Clone)]
pub struct ScrollArea<'a> {
block: Option<&'a Block<'a>>,
h_scroll: Option<&'a Scroll<'a>>,
v_scroll: Option<&'a Scroll<'a>>,
}
#[derive(Debug, Default)]
pub struct ScrollAreaState<'a> {
area: Rect,
h_scroll: Option<&'a mut ScrollState>,
v_scroll: Option<&'a mut ScrollState>,
}
impl<'a> ScrollArea<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn block(mut self, block: Option<&'a Block<'a>>) -> Self {
self.block = block;
self
}
pub fn h_scroll(mut self, scroll: Option<&'a Scroll<'a>>) -> Self {
self.h_scroll = scroll;
self
}
pub fn v_scroll(mut self, scroll: Option<&'a Scroll<'a>>) -> Self {
self.v_scroll = scroll;
self
}
pub fn inner(
&self,
area: Rect,
hscroll_state: Option<&ScrollState>,
vscroll_state: Option<&ScrollState>,
) -> Rect {
layout(
self.block,
self.h_scroll,
self.v_scroll,
area,
hscroll_state,
vscroll_state,
)
.0
}
}
impl<'a> StatefulWidget for ScrollArea<'a> {
type State = ScrollAreaState<'a>;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
render_scroll_area(&self, area, buf, state);
}
}
#[cfg(feature = "unstable-widget-ref")]
impl<'a> StatefulWidgetRef for ScrollArea<'a> {
type State = ScrollAreaState<'a>;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
render_scroll_area(self, area, buf, state);
}
}
fn render_scroll_area(
widget: &ScrollArea<'_>,
area: Rect,
buf: &mut Buffer,
state: &mut ScrollAreaState<'_>,
) {
let (_, hscroll_area, vscroll_area) = layout(
widget.block,
widget.h_scroll,
widget.v_scroll,
area,
state.h_scroll.as_deref(),
state.v_scroll.as_deref(),
);
if let Some(block) = widget.block {
block.render(area, buf);
}
if let Some(h) = widget.h_scroll {
if let Some(hstate) = &mut state.h_scroll {
h.render(hscroll_area, buf, hstate);
} else {
panic!("no horizontal scroll state");
}
}
if let Some(v) = widget.v_scroll {
if let Some(vstate) = &mut state.v_scroll {
v.render(vscroll_area, buf, vstate)
} else {
panic!("no vertical scroll state");
}
}
}
fn layout<'a>(
block: Option<&Block<'a>>,
hscroll: Option<&Scroll<'a>>,
vscroll: Option<&Scroll<'a>>,
area: Rect,
hscroll_state: Option<&ScrollState>,
vscroll_state: Option<&ScrollState>,
) -> (Rect, Rect, Rect) {
let mut inner = area;
if let Some(block) = block {
inner = block.inner(area);
}
if let Some(hscroll) = hscroll {
if let Some(hscroll_state) = hscroll_state {
let show = match hscroll.get_policy() {
ScrollbarPolicy::Always => true,
ScrollbarPolicy::Minimize => true,
ScrollbarPolicy::Collapse => hscroll_state.max_offset > 0,
};
if show {
match hscroll.get_orientation() {
ScrollbarOrientation::VerticalRight => {
unimplemented!(
"ScrollbarOrientation::VerticalRight not supported for horizontal scrolling."
);
}
ScrollbarOrientation::VerticalLeft => {
unimplemented!(
"ScrollbarOrientation::VerticalLeft not supported for horizontal scrolling."
);
}
ScrollbarOrientation::HorizontalBottom => {
if inner.bottom() == area.bottom() {
inner.height = inner.height.saturating_sub(1);
}
}
ScrollbarOrientation::HorizontalTop => {
if inner.top() == area.top() {
inner.y += 1;
inner.height = inner.height.saturating_sub(1);
}
}
}
}
} else {
panic!("no horizontal scroll state");
}
}
if let Some(vscroll) = vscroll {
if let Some(vscroll_state) = vscroll_state {
let show = match vscroll.get_policy() {
ScrollbarPolicy::Always => true,
ScrollbarPolicy::Minimize => true,
ScrollbarPolicy::Collapse => vscroll_state.max_offset > 0,
};
if show {
match vscroll.get_orientation() {
ScrollbarOrientation::VerticalRight => {
if inner.right() == area.right() {
inner.width = inner.width.saturating_sub(1);
}
}
ScrollbarOrientation::VerticalLeft => {
if inner.left() == area.left() {
inner.x += 1;
inner.width = inner.width.saturating_sub(1);
}
}
ScrollbarOrientation::HorizontalBottom => {
unimplemented!(
"ScrollbarOrientation::HorizontalBottom not supported for vertical scrolling."
);
}
ScrollbarOrientation::HorizontalTop => {
unimplemented!(
"ScrollbarOrientation::HorizontalTop not supported for vertical scrolling."
);
}
}
}
} else {
panic!("no horizontal scroll state");
}
}
let h_area = if let Some(hscroll) = hscroll {
if let Some(hscroll_state) = hscroll_state {
let show = match hscroll.get_policy() {
ScrollbarPolicy::Always => true,
ScrollbarPolicy::Minimize => true,
ScrollbarPolicy::Collapse => hscroll_state.max_offset > 0,
};
if show {
match hscroll.get_orientation() {
ScrollbarOrientation::HorizontalBottom => Rect::new(
inner.x + hscroll.get_start_margin(),
area.bottom().saturating_sub(1),
inner
.width
.saturating_sub(hscroll.get_start_margin() + hscroll.get_end_margin()),
if area.height > 0 { 1 } else { 0 },
),
ScrollbarOrientation::HorizontalTop => Rect::new(
inner.x + hscroll.get_start_margin(),
area.y,
inner
.width
.saturating_sub(hscroll.get_start_margin() + hscroll.get_end_margin()),
if area.height > 0 { 1 } else { 0 },
),
_ => unreachable!(),
}
} else {
Rect::new(area.x, area.y, 0, 0)
}
} else {
panic!("no horizontal scroll state");
}
} else {
Rect::new(area.x, area.y, 0, 0)
};
let v_area = if let Some(vscroll) = vscroll {
if let Some(vscroll_state) = vscroll_state {
let show = match vscroll.get_policy() {
ScrollbarPolicy::Always => true,
ScrollbarPolicy::Minimize => true,
ScrollbarPolicy::Collapse => vscroll_state.max_offset > 0,
};
if show {
match vscroll.get_orientation() {
ScrollbarOrientation::VerticalRight => Rect::new(
area.right().saturating_sub(1),
inner.y + vscroll.get_start_margin(),
if area.width > 0 { 1 } else { 0 },
inner
.height
.saturating_sub(vscroll.get_start_margin() + vscroll.get_end_margin()),
),
ScrollbarOrientation::VerticalLeft => Rect::new(
area.x,
inner.y + vscroll.get_start_margin(),
if area.width > 0 { 1 } else { 0 },
inner
.height
.saturating_sub(vscroll.get_start_margin() + vscroll.get_end_margin()),
),
_ => unreachable!(),
}
} else {
Rect::new(area.x, area.y, 0, 0)
}
} else {
panic!("no horizontal scroll state");
}
} else {
Rect::new(area.x, area.y, 0, 0)
};
(inner, h_area, v_area)
}
impl<'a> ScrollAreaState<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn area(mut self, area: Rect) -> Self {
self.area = area;
self
}
pub fn v_scroll(mut self, v_scroll: &'a mut ScrollState) -> Self {
self.v_scroll = Some(v_scroll);
self
}
pub fn v_scroll_opt(mut self, v_scroll: Option<&'a mut ScrollState>) -> Self {
self.v_scroll = v_scroll;
self
}
pub fn h_scroll(mut self, h_scroll: &'a mut ScrollState) -> Self {
self.h_scroll = Some(h_scroll);
self
}
pub fn h_scroll_opt(mut self, h_scroll: Option<&'a mut ScrollState>) -> Self {
self.h_scroll = h_scroll;
self
}
}
impl<'a> HandleEvent<crossterm::event::Event, MouseOnly, ScrollOutcome> for ScrollAreaState<'a> {
fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> ScrollOutcome {
if let Some(h_scroll) = &mut self.h_scroll {
flow!(match event {
ct_event!(scroll ALT down for column, row) => {
if self.area.contains(Position::new(*column, *row)) {
ScrollOutcome::Right(h_scroll.scroll_by())
} else {
ScrollOutcome::Continue
}
}
ct_event!(scroll ALT up for column, row) => {
if self.area.contains(Position::new(*column, *row)) {
ScrollOutcome::Left(h_scroll.scroll_by())
} else {
ScrollOutcome::Continue
}
}
_ => ScrollOutcome::Continue,
});
flow!(h_scroll.handle(event, MouseOnly));
}
if let Some(v_scroll) = &mut self.v_scroll {
flow!(match event {
ct_event!(scroll down for column, row) => {
if self.area.contains(Position::new(*column, *row)) {
ScrollOutcome::Down(v_scroll.scroll_by())
} else {
ScrollOutcome::Continue
}
}
ct_event!(scroll up for column, row) => {
if self.area.contains(Position::new(*column, *row)) {
ScrollOutcome::Up(v_scroll.scroll_by())
} else {
ScrollOutcome::Continue
}
}
_ => ScrollOutcome::Continue,
});
flow!(v_scroll.handle(event, MouseOnly));
}
ScrollOutcome::Continue
}
}