use crate::_private::NonExhaustive;
use crate::event::ScrollOutcome;
use crate::inner::{InnerStatefulOwned, InnerStatefulRef, InnerWidget};
use crate::util::copy_buffer;
use crate::{ScrollingState, ScrollingWidget};
use rat_event::{ConsumedEvent, FocusKeys, HandleEvent, MouseOnly};
use ratatui::buffer::Buffer;
use ratatui::layout::{Rect, Size};
use ratatui::prelude::StatefulWidget;
use ratatui::style::Style;
use ratatui::widgets::StatefulWidgetRef;
#[derive(Debug, Default, Clone)]
pub struct Viewport<T> {
widget: T,
viewport: ViewportImpl,
}
#[derive(Debug, Default, Clone)]
struct ViewportImpl {
view_size: Size,
style: Style,
}
#[derive(Debug, Clone)]
pub struct ViewportState<S> {
pub widget: S,
pub area: Rect,
pub view_area: Rect,
pub h_offset: usize,
pub v_offset: usize,
pub non_exhaustive: NonExhaustive,
}
impl<T> Viewport<T> {
pub fn new(inner: T) -> Self {
Self {
widget: inner,
viewport: ViewportImpl::default(),
}
}
pub fn view_size(mut self, size: Size) -> Self {
self.viewport.view_size = size;
self
}
pub fn style(mut self, style: Style) -> Self {
self.viewport.style = style;
self
}
}
impl<T> StatefulWidgetRef for Viewport<T>
where
T: StatefulWidgetRef,
{
type State = ViewportState<T::State>;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let inner = InnerStatefulRef {
inner: &self.widget,
};
render_ref(&self.viewport, inner, area, buf, state);
}
}
impl<T> StatefulWidget for Viewport<T>
where
T: StatefulWidget,
{
type State = ViewportState<T::State>;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let inner = InnerStatefulOwned { inner: self.widget };
render_ref(&self.viewport, inner, area, buf, state);
}
}
fn render_ref<W, S>(
viewport: &ViewportImpl,
inner: impl InnerWidget<W, S>,
area: Rect,
buf: &mut Buffer,
state: &mut ViewportState<S>,
) {
state.area = area;
state.view_area = Rect::new(
area.x,
area.y,
viewport.view_size.width,
viewport.view_size.height,
);
let mut tmp = Buffer::empty(state.view_area);
inner.render_inner(state.view_area, &mut tmp, &mut state.widget);
copy_buffer(
state.view_area,
tmp,
state.v_offset,
state.h_offset,
viewport.style,
area,
buf,
);
}
impl<State, T> ScrollingWidget<State> for Viewport<T>
where
T: StatefulWidget,
{
fn need_scroll(&self, area: Rect, _state: &mut State) -> (bool, bool) {
(
area.width < self.viewport.view_size.width,
area.height < self.viewport.view_size.height,
)
}
}
impl<S: Default> Default for ViewportState<S> {
fn default() -> Self {
Self {
widget: S::default(),
area: Default::default(),
view_area: Default::default(),
h_offset: 0,
v_offset: 0,
non_exhaustive: NonExhaustive,
}
}
}
impl<S> ViewportState<S> {
pub fn relocate_crossterm(&self, event: &crossterm::event::Event) -> crossterm::event::Event {
match event {
crossterm::event::Event::FocusGained => event.clone(),
crossterm::event::Event::FocusLost => event.clone(),
crossterm::event::Event::Key(_) => event.clone(),
crossterm::event::Event::Mouse(m) => {
let mut m = *m;
m.column += self.h_offset as u16;
m.row += self.v_offset as u16;
crossterm::event::Event::Mouse(m)
}
crossterm::event::Event::Paste(_) => event.clone(),
crossterm::event::Event::Resize(_, _) => event.clone(),
}
}
}
impl<S> ScrollingState for ViewportState<S> {
fn vertical_max_offset(&self) -> usize {
self.view_area.height.saturating_sub(self.area.height) as usize
}
fn vertical_offset(&self) -> usize {
self.v_offset
}
fn vertical_page(&self) -> usize {
self.area.height as usize
}
fn horizontal_max_offset(&self) -> usize {
self.view_area.width.saturating_sub(self.area.width) as usize
}
fn horizontal_offset(&self) -> usize {
self.h_offset
}
fn horizontal_page(&self) -> usize {
self.area.width as usize
}
fn set_vertical_offset(&mut self, offset: usize) -> bool {
let old_offset = self.v_offset;
if self.v_offset < self.view_area.height as usize {
self.v_offset = offset;
} else if self.v_offset >= self.view_area.height as usize {
self.v_offset = self.view_area.height.saturating_sub(1) as usize;
}
old_offset != self.v_offset
}
fn set_horizontal_offset(&mut self, offset: usize) -> bool {
let old_offset = self.h_offset;
if self.h_offset < self.view_area.width as usize {
self.h_offset = offset;
} else if self.h_offset >= self.view_area.width as usize {
self.h_offset = self.view_area.width.saturating_sub(1) as usize;
}
old_offset != self.h_offset
}
}
impl<R, S> HandleEvent<crossterm::event::Event, FocusKeys, ScrollOutcome<R>> for ViewportState<S>
where
S: HandleEvent<crossterm::event::Event, FocusKeys, R>,
R: ConsumedEvent,
{
fn handle(&mut self, event: &crossterm::event::Event, _keymap: FocusKeys) -> ScrollOutcome<R> {
let event = self.relocate_crossterm(event);
let r = self.widget.handle(&event, FocusKeys);
ScrollOutcome::Inner(r)
}
}
impl<R, S> HandleEvent<crossterm::event::Event, MouseOnly, ScrollOutcome<R>> for ViewportState<S>
where
S: HandleEvent<crossterm::event::Event, MouseOnly, R>,
R: ConsumedEvent,
{
fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> ScrollOutcome<R> {
let event = self.relocate_crossterm(event);
let r = self.widget.handle(&event, MouseOnly);
ScrollOutcome::Inner(r)
}
}