use crate::ui::base::{HandleEvent, Height, Pos};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use std::num::NonZeroUsize;
pub struct Paging {
top: usize,
bottom: usize,
page_height: Height,
total_length: NonZeroUsize,
selected: Pos,
}
impl Default for Paging {
fn default() -> Self {
Self {
top: 0,
bottom: 0,
page_height: NonZeroUsize::new(1).unwrap(),
total_length: NonZeroUsize::new(1).unwrap(),
selected: 0,
}
}
}
impl Paging {
#[cfg(test)]
#[cfg(not(tarpaulin_include))]
#[allow(clippy::arithmetic)]
pub const fn new(page_height: Height, total_length: NonZeroUsize) -> Self {
let bottom = page_height.get() - 1;
Self {
top: 0,
bottom,
page_height,
total_length,
selected: 0,
}
}
pub const fn top(&self) -> usize {
self.top
}
pub const fn bottom(&self) -> usize {
self.bottom
}
fn next_page(&mut self) {
if let Some(top) = self.top.checked_add(self.page_height.get()) {
if top >= self.total_length.get() {
self.selected = self.bottom;
} else {
self.top = top;
self.bottom = self.top.saturating_add(self.page_height.get());
self.selected = self.selected.saturating_add(self.page_height.get());
#[allow(clippy::arithmetic)]
if self.bottom >= self.total_length.get() {
self.bottom = self.total_length.get() - 1;
}
#[allow(clippy::arithmetic)]
if self.selected >= self.total_length.get() {
self.selected = self.total_length.get() - 1;
}
}
} else {
self.selected = self.bottom;
}
}
fn prev_page(&mut self) {
self.top = self.top.saturating_sub(self.page_height.get());
self.bottom = self
.top
.saturating_add(self.page_height.get().saturating_sub(1));
self.selected = self.selected.saturating_sub(self.page_height.get());
}
pub fn page_height(&mut self, height: Height, total_length: NonZeroUsize) {
self.page_height = height;
self.total_length = total_length;
self.bottom = self
.top
.saturating_add(self.page_height.get().saturating_sub(1));
#[allow(clippy::arithmetic)]
if self.bottom >= self.total_length.get() {
self.bottom = self.total_length.get() - 1;
}
}
pub const fn selected(&self) -> usize {
self.selected
}
pub fn set_selected(&mut self, i: usize) {
if self.total_length.get() <= i {
log::error!(
"Expected selected({}) < total_length({})",
i,
self.total_length
);
return;
}
if i < self.top {
while i < self.top {
self.prev_page();
}
self.selected = i;
} else if i > self.bottom {
while i > self.bottom {
self.next_page();
}
} else {
log::trace!("No paging needed");
}
self.selected = i;
}
fn select_next(&mut self) {
self.selected = self.selected.saturating_add(1);
#[allow(clippy::arithmetic)]
if self.selected >= self.total_length.get() {
self.selected = self.total_length.get() - 1;
}
if self.bottom < self.selected {
self.bottom = self.selected;
self.top = self
.bottom
.saturating_sub(self.page_height.get())
.saturating_add(1);
}
}
fn select_prev(&mut self) {
self.selected = self.selected.saturating_sub(1);
if self.selected < self.top {
self.top = self.selected;
self.bottom = self
.top
.saturating_add(self.page_height.get())
.saturating_sub(1);
}
}
pub fn on_event(&mut self, event: &Event) -> HandleEvent {
match event {
Event::Key(KeyEvent {
code: KeyCode::Up,
modifiers: KeyModifiers::NONE,
..
}) => {
self.select_prev();
HandleEvent::Handled
}
Event::Key(KeyEvent {
code: KeyCode::Down,
modifiers: KeyModifiers::NONE,
..
}) => {
self.select_next();
HandleEvent::Handled
}
Event::Key(KeyEvent {
code: KeyCode::PageDown,
modifiers: KeyModifiers::NONE,
..
}) => {
self.next_page();
HandleEvent::Handled
}
Event::Key(KeyEvent {
code: KeyCode::PageUp,
modifiers: KeyModifiers::NONE,
..
}) => {
self.prev_page();
HandleEvent::Handled
}
_ => HandleEvent::Ignored,
}
}
pub fn set_total_length(&mut self, len: NonZeroUsize) {
self.total_length = len;
}
}
#[cfg(test)]
mod test_paging {
use std::num::NonZeroUsize;
use crate::ui::base::paging::Paging;
use crate::ui::base::HandleEvent;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
use pretty_assertions::assert_eq;
#[test]
fn next_page() {
let pager = &mut Paging::new(
NonZeroUsize::new(25).unwrap(),
NonZeroUsize::new(30).unwrap(),
);
assert_eq!(pager.top(), 0);
assert_eq!(pager.selected(), 0);
assert_eq!(pager.bottom(), 24);
handle_event(pager, KeyCode::PageDown);
assert_eq!(pager.top(), 25);
assert_eq!(pager.selected(), 25, "First visible row is selected");
assert_eq!(pager.bottom(), 29, "bottom should be eq to length - 1");
handle_event(pager, KeyCode::PageDown);
assert_eq!(pager.top(), 25, "Top should not change");
assert_eq!(pager.selected(), 29, "Selection should go to last");
assert_eq!(pager.bottom(), 29, "Bottom should not change");
}
#[test]
fn prev_page() {
let pager = &mut Paging::new(
NonZeroUsize::new(25).unwrap(),
NonZeroUsize::new(30).unwrap(),
);
assert_eq!(pager.top(), 0);
assert_eq!(pager.selected(), 0, "First visible row is selected");
handle_event(pager, KeyCode::PageUp);
assert_eq!(pager.selected(), 0, "First visible row is still selected");
handle_event(pager, KeyCode::PageDown);
assert_eq!(pager.selected(), 25, "First visible row should not change");
handle_event(pager, KeyCode::PageUp);
assert_eq!(pager.selected(), 0, "First visible row is selected");
}
#[test]
fn selected() {
let pager = &mut Paging::new(
NonZeroUsize::new(25).unwrap(),
NonZeroUsize::new(30).unwrap(),
);
assert_eq!(pager.selected(), 0, "Start with selection at position 0");
handle_event(pager, KeyCode::Down);
assert_eq!(pager.selected(), 1, "Next pos should be 1");
handle_event(pager, KeyCode::Up);
assert_eq!(pager.selected(), 0, "Next pos should be 1");
handle_event(pager, KeyCode::Up);
assert_eq!(pager.selected(), 0, "Position should not change");
for _ in 0..10 {
pager.select_next();
}
assert_eq!(
pager.selected(),
10,
"Position should be one not existing on next page"
);
handle_event(pager, KeyCode::PageDown);
assert_eq!(
pager.selected(),
29,
"Position should be one not existing on next page"
);
handle_event(pager, KeyCode::PageUp);
assert_eq!(pager.selected(), 4);
for _ in 0..20 {
pager.select_next();
}
assert_eq!(
pager.selected(),
pager.bottom(),
"Last position on first page is selected"
);
handle_event(pager, KeyCode::Down);
assert_eq!(
pager.selected(),
pager.bottom(),
"Whole view scrolls one down"
);
assert_eq!(pager.top(), 1, "Whole view scrolls one down");
assert_eq!(pager.bottom(), 25, "Whole view scrolls one down");
for _ in 0..24 {
pager.select_prev();
}
assert_eq!(
pager.selected(),
pager.top(),
"The top visible row is selected"
);
assert_eq!(pager.top(), 1);
handle_event(pager, KeyCode::Up);
assert_eq!(pager.selected(), pager.top(), "Whole view scrolls one up");
assert_eq!(pager.top(), 0, "Whole view scrolls one up");
assert_eq!(pager.bottom(), 24, "Whole view scrolls one up");
}
#[test]
fn page_height() {
let pager = &mut Paging::new(
NonZeroUsize::new(25).unwrap(),
NonZeroUsize::new(30).unwrap(),
);
assert_eq!(pager.top(), 0);
assert_eq!(pager.selected(), 0, "First visible row is selected");
assert_eq!(pager.bottom(), 24);
pager.page_height(
NonZeroUsize::new(6).unwrap(),
NonZeroUsize::new(30).unwrap(),
);
assert_eq!(pager.top(), 0, "Top did not change");
assert_eq!(pager.selected(), 0, "First visible row is still selected");
assert_eq!(pager.bottom(), 5, "Bottom shrinked");
}
#[test]
fn move_selection_to_top() {
let pager = &mut Paging::new(
NonZeroUsize::new(10).unwrap(),
NonZeroUsize::new(20).unwrap(),
);
assert_eq!(pager.selected(), 0, "Start with selection at position 0");
assert_eq!(pager.top, 0);
assert_eq!(pager.bottom, 9);
pager.top = 4;
pager.selected = 4;
pager.bottom = 13;
handle_event(pager, KeyCode::PageUp);
assert_eq!(pager.top, 0);
assert_eq!(pager.selected, 0);
assert_eq!(pager.bottom, 9);
}
fn handle_event(pager: &mut Paging, code: KeyCode) {
let event = Event::Key(KeyEvent {
code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
});
assert_eq!(pager.on_event(&event), HandleEvent::Handled);
}
}