mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 11:35:25 +00:00
291 lines
8.5 KiB
Rust
291 lines
8.5 KiB
Rust
use crate::{
|
|
channels::entry::Entry,
|
|
utils::{input::Input, strings::EMPTY_STRING},
|
|
};
|
|
use ratatui::widgets::ListState;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Hash)]
|
|
pub struct Picker {
|
|
pub(crate) state: ListState,
|
|
pub(crate) relative_state: ListState,
|
|
inverted: bool,
|
|
pub(crate) input: Input,
|
|
pub entries: Vec<Entry>,
|
|
pub total_items: u32,
|
|
}
|
|
|
|
impl Default for Picker {
|
|
fn default() -> Self {
|
|
Self::new(None)
|
|
}
|
|
}
|
|
|
|
impl Picker {
|
|
pub fn new(input: Option<String>) -> Self {
|
|
Self {
|
|
state: ListState::default(),
|
|
relative_state: ListState::default(),
|
|
inverted: false,
|
|
input: Input::new(input.unwrap_or(EMPTY_STRING.to_string())),
|
|
entries: Vec::new(),
|
|
total_items: 0,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn offset(&self) -> usize {
|
|
self.selected()
|
|
.unwrap_or(0)
|
|
.saturating_sub(self.relative_selected().unwrap_or(0))
|
|
}
|
|
|
|
pub(crate) fn inverted(mut self) -> Self {
|
|
self.inverted = !self.inverted;
|
|
self
|
|
}
|
|
|
|
pub(crate) fn reset_selection(&mut self) {
|
|
self.state.select(Some(0));
|
|
self.relative_state.select(Some(0));
|
|
}
|
|
|
|
pub(crate) fn reset_input(&mut self) {
|
|
self.input.reset();
|
|
}
|
|
|
|
pub(crate) fn selected(&self) -> Option<usize> {
|
|
self.state.selected()
|
|
}
|
|
|
|
pub(crate) fn select(&mut self, index: Option<usize>) {
|
|
self.state.select(index);
|
|
}
|
|
|
|
fn relative_selected(&self) -> Option<usize> {
|
|
self.relative_state.selected()
|
|
}
|
|
|
|
pub(crate) fn relative_select(&mut self, index: Option<usize>) {
|
|
self.relative_state.select(index);
|
|
}
|
|
|
|
pub(crate) fn select_next(
|
|
&mut self,
|
|
step: u32,
|
|
total_items: usize,
|
|
height: usize,
|
|
) {
|
|
if self.inverted {
|
|
for _ in 0..step {
|
|
self.inner_prev(total_items, height);
|
|
}
|
|
} else {
|
|
for _ in 0..step {
|
|
self.inner_next(total_items, height);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn select_prev(
|
|
&mut self,
|
|
step: u32,
|
|
total_items: usize,
|
|
height: usize,
|
|
) {
|
|
if self.inverted {
|
|
for _ in 0..step {
|
|
self.inner_next(total_items, height);
|
|
}
|
|
} else {
|
|
for _ in 0..step {
|
|
self.inner_prev(total_items, height);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn inner_next(&mut self, total_items: usize, height: usize) {
|
|
let selected = self.selected().unwrap_or(0);
|
|
let relative_selected = self.relative_selected().unwrap_or(0);
|
|
self.select(Some(selected.saturating_add(1) % total_items));
|
|
self.relative_select(Some((relative_selected + 1).min(height - 1)));
|
|
if self.selected().unwrap() == 0 {
|
|
self.relative_select(Some(0));
|
|
}
|
|
}
|
|
|
|
fn inner_prev(&mut self, total_items: usize, height: usize) {
|
|
let selected = self.selected().unwrap_or(0);
|
|
let relative_selected = self.relative_selected().unwrap_or(0);
|
|
self.select(Some((selected + (total_items - 1)) % total_items));
|
|
self.relative_select(Some(relative_selected.saturating_sub(1)));
|
|
if self.selected().unwrap() == total_items - 1 {
|
|
self.relative_select(Some((height - 1).min(total_items - 1)));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// - item 0 S R *
|
|
/// - item 1 next *
|
|
/// - item 2 * height
|
|
/// - item 3
|
|
#[test]
|
|
fn test_picker_select_next_default() {
|
|
let mut picker = Picker::default();
|
|
picker.select(Some(0));
|
|
picker.relative_select(Some(0));
|
|
picker.select_next(1, 4, 3);
|
|
assert_eq!(picker.selected(), Some(1), "selected");
|
|
assert_eq!(picker.relative_selected(), Some(1), "relative_selected");
|
|
}
|
|
|
|
/// - item 0 *
|
|
/// - item 1 S R *
|
|
/// - item 2 next * height
|
|
/// - item 3
|
|
#[test]
|
|
fn test_picker_select_next_before_relative_last() {
|
|
let mut picker = Picker::default();
|
|
picker.select(Some(1));
|
|
picker.relative_select(Some(1));
|
|
picker.select_next(1, 4, 3);
|
|
assert_eq!(picker.selected(), Some(2), "selected");
|
|
assert_eq!(picker.relative_selected(), Some(2), "relative_selected");
|
|
}
|
|
|
|
/// - item 0 *
|
|
/// - item 1 *
|
|
/// - item 2 S R * height
|
|
/// - item 3 next
|
|
#[test]
|
|
fn test_picker_select_next_relative_last() {
|
|
let mut picker = Picker::default();
|
|
picker.select(Some(2));
|
|
picker.relative_select(Some(2));
|
|
picker.select_next(1, 4, 3);
|
|
assert_eq!(picker.selected(), Some(3), "selected");
|
|
assert_eq!(picker.relative_selected(), Some(2), "relative_selected");
|
|
}
|
|
|
|
/// - item 0 next *
|
|
/// - item 1 *
|
|
/// - item 2 R * height
|
|
/// - item 3 S
|
|
#[test]
|
|
fn test_picker_select_next_last() {
|
|
let mut picker = Picker::default();
|
|
picker.select(Some(3));
|
|
picker.relative_select(Some(2));
|
|
picker.select_next(1, 4, 3);
|
|
assert_eq!(picker.selected(), Some(0), "selected");
|
|
assert_eq!(picker.relative_selected(), Some(0), "relative_selected");
|
|
}
|
|
|
|
/// - item 0 next *
|
|
/// - item 1 *
|
|
/// - item 2 S R *
|
|
/// * height
|
|
#[test]
|
|
fn test_picker_select_next_less_items_than_height_last() {
|
|
let mut picker = Picker::default();
|
|
picker.select(Some(2));
|
|
picker.relative_select(Some(2));
|
|
picker.select_next(1, 3, 4);
|
|
assert_eq!(picker.selected(), Some(0), "selected");
|
|
assert_eq!(picker.relative_selected(), Some(0), "relative_selected");
|
|
}
|
|
|
|
/// - item 0 prev *
|
|
/// - item 1 S R *
|
|
/// - item 2 * height
|
|
/// - item 3
|
|
#[test]
|
|
fn test_picker_select_prev_default() {
|
|
let mut picker = Picker::default();
|
|
picker.select(Some(1));
|
|
picker.relative_select(Some(1));
|
|
picker.select_prev(1, 4, 3);
|
|
assert_eq!(picker.selected(), Some(0), "selected");
|
|
assert_eq!(picker.relative_selected(), Some(0), "relative_selected");
|
|
}
|
|
|
|
/// - item 0 S R *
|
|
/// - item 1 * *
|
|
/// - item 2 * height *
|
|
/// - item 3 prev *
|
|
#[test]
|
|
fn test_picker_select_prev_first() {
|
|
let mut picker = Picker::default();
|
|
picker.select(Some(0));
|
|
picker.relative_select(Some(0));
|
|
picker.select_prev(1, 4, 3);
|
|
assert_eq!(picker.selected(), Some(3), "selected");
|
|
assert_eq!(picker.relative_selected(), Some(2), "relative_selected");
|
|
}
|
|
|
|
/// - item 0 *
|
|
/// - item 1 *
|
|
/// - item 2 prev R * height
|
|
/// - item 3 S
|
|
#[test]
|
|
fn test_picker_select_prev_relative_trailing() {
|
|
let mut picker = Picker::default();
|
|
picker.select(Some(3));
|
|
picker.relative_select(Some(2));
|
|
picker.select_prev(1, 4, 3);
|
|
assert_eq!(picker.selected(), Some(2), "selected");
|
|
assert_eq!(picker.relative_selected(), Some(1), "relative_selected");
|
|
}
|
|
|
|
/// - item 0 *
|
|
/// - item 1 prev *
|
|
/// - item 2 S R * height
|
|
/// - item 3
|
|
#[test]
|
|
fn test_picker_select_prev_relative_sync() {
|
|
let mut picker = Picker::default();
|
|
picker.select(Some(2));
|
|
picker.relative_select(Some(2));
|
|
picker.select_prev(1, 4, 3);
|
|
assert_eq!(picker.selected(), Some(1), "selected");
|
|
assert_eq!(picker.relative_selected(), Some(1), "relative_selected");
|
|
}
|
|
|
|
#[test]
|
|
fn test_picker_offset_default() {
|
|
let picker = Picker::default();
|
|
assert_eq!(picker.offset(), 0, "offset");
|
|
}
|
|
|
|
#[test]
|
|
fn test_picker_offset_none() {
|
|
let mut picker = Picker::default();
|
|
picker.select(None);
|
|
picker.relative_select(None);
|
|
assert_eq!(picker.offset(), 0, "offset");
|
|
}
|
|
|
|
#[test]
|
|
fn test_picker_offset() {
|
|
let mut picker = Picker::default();
|
|
picker.select(Some(1));
|
|
picker.relative_select(Some(2));
|
|
assert_eq!(picker.offset(), 0, "offset");
|
|
}
|
|
|
|
#[test]
|
|
fn test_picker_inverted() {
|
|
let mut picker = Picker::default();
|
|
picker.select(Some(0));
|
|
picker.relative_select(Some(0));
|
|
picker.select_next(1, 4, 2);
|
|
picker = picker.inverted();
|
|
picker.select_next(1, 4, 2);
|
|
assert!(picker.inverted, "inverted");
|
|
assert_eq!(picker.selected(), Some(0), "selected");
|
|
assert_eq!(picker.relative_selected(), Some(0), "relative_selected");
|
|
}
|
|
}
|