use super::{Component, EventContext, RenderContext};
use crate::input::{Event, Key};
use crate::scroll::ScrollState;
mod items;
mod render;
pub use items::{ItemState, LoadingListItem};
#[derive(Clone, Debug, PartialEq)]
pub enum LoadingListMessage<T: Clone> {
SetItems(Vec<T>),
SetLoading(usize),
SetReady(usize),
SetError {
index: usize,
message: String,
},
ClearError(usize),
Up,
Down,
First,
Last,
Select,
Tick,
}
#[derive(Clone, Debug, PartialEq)]
pub enum LoadingListOutput<T: Clone> {
Selected(T),
SelectionChanged(usize),
ItemStateChanged {
index: usize,
state: ItemState,
},
}
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct LoadingListState<T: Clone> {
items: Vec<LoadingListItem<T>>,
selected: Option<usize>,
spinner_frame: usize,
title: Option<String>,
show_indicators: bool,
#[cfg_attr(feature = "serialization", serde(skip))]
scroll: ScrollState,
}
impl<T: Clone + PartialEq> PartialEq for LoadingListState<T> {
fn eq(&self, other: &Self) -> bool {
self.items == other.items
&& self.selected == other.selected
&& self.spinner_frame == other.spinner_frame
&& self.title == other.title
&& self.show_indicators == other.show_indicators
}
}
impl<T: Clone> Default for LoadingListState<T> {
fn default() -> Self {
Self {
items: Vec::new(),
selected: None,
spinner_frame: 0,
title: None,
show_indicators: true,
scroll: ScrollState::default(),
}
}
}
impl<T: Clone> LoadingListState<T> {
pub fn new() -> Self {
Self::default()
}
pub fn with_items<F>(items: Vec<T>, label_fn: F) -> Self
where
F: Fn(&T) -> String,
{
let list_items: Vec<LoadingListItem<T>> = items
.into_iter()
.map(|data| {
let label = label_fn(&data);
LoadingListItem::new(data, label)
})
.collect();
let scroll = ScrollState::new(list_items.len());
Self {
items: list_items,
selected: None,
spinner_frame: 0,
title: None,
show_indicators: true,
scroll,
}
}
pub fn with_selected(mut self, index: usize) -> Self {
if self.items.is_empty() {
return self;
}
self.selected = Some(index.min(self.items.len() - 1));
self
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_indicators(mut self, show: bool) -> Self {
self.show_indicators = show;
self
}
pub fn items(&self) -> &[LoadingListItem<T>] {
&self.items
}
pub fn items_mut(&mut self) -> &mut Vec<LoadingListItem<T>> {
&mut self.items
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn selected_index(&self) -> Option<usize> {
self.selected
}
pub fn selected(&self) -> Option<usize> {
self.selected_index()
}
pub fn selected_item(&self) -> Option<&LoadingListItem<T>> {
self.selected.and_then(|i| self.items.get(i))
}
pub fn selected_data(&self) -> Option<&T> {
self.selected_item().map(|item| item.data())
}
pub fn set_selected(&mut self, index: Option<usize>) {
self.selected = index.map(|i| i.min(self.items.len().saturating_sub(1)));
}
pub fn get(&self, index: usize) -> Option<&LoadingListItem<T>> {
self.items.get(index)
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut LoadingListItem<T>> {
self.items.get_mut(index)
}
pub fn set_loading(&mut self, index: usize) {
if let Some(item) = self.items.get_mut(index) {
item.state = ItemState::Loading;
}
}
pub fn set_ready(&mut self, index: usize) {
if let Some(item) = self.items.get_mut(index) {
item.state = ItemState::Ready;
}
}
pub fn set_error(&mut self, index: usize, message: impl Into<String>) {
if let Some(item) = self.items.get_mut(index) {
item.state = ItemState::Error(message.into());
}
}
pub fn is_loading(&self, index: usize) -> bool {
self.items
.get(index)
.is_some_and(|item| item.state == ItemState::Loading)
}
pub fn is_ready(&self, index: usize) -> bool {
self.items
.get(index)
.is_some_and(|item| item.state == ItemState::Ready)
}
pub fn is_error(&self, index: usize) -> bool {
self.items
.get(index)
.is_some_and(|item| item.state.is_error())
}
pub fn loading_count(&self) -> usize {
self.items.iter().filter(|i| i.is_loading()).count()
}
pub fn error_count(&self) -> usize {
self.items.iter().filter(|i| i.is_error()).count()
}
pub fn has_loading(&self) -> bool {
self.items.iter().any(|i| i.is_loading())
}
pub fn has_errors(&self) -> bool {
self.items.iter().any(|i| i.is_error())
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn set_title(&mut self, title: Option<String>) {
self.title = title;
}
pub fn show_indicators(&self) -> bool {
self.show_indicators
}
pub fn set_show_indicators(&mut self, show: bool) {
self.show_indicators = show;
}
pub fn spinner_frame(&self) -> usize {
self.spinner_frame
}
pub fn clear(&mut self) {
self.items.clear();
self.selected = None;
self.scroll.set_content_length(0);
}
}
impl<T: Clone + 'static> LoadingListState<T> {
pub fn update(&mut self, msg: LoadingListMessage<T>) -> Option<LoadingListOutput<T>> {
LoadingList::update(self, msg)
}
}
pub struct LoadingList<T: Clone>(std::marker::PhantomData<T>);
impl<T: Clone> Component for LoadingList<T> {
type State = LoadingListState<T>;
type Message = LoadingListMessage<T>;
type Output = LoadingListOutput<T>;
fn init() -> Self::State {
LoadingListState::default()
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
LoadingListMessage::SetItems(items) => {
state.items = items
.into_iter()
.enumerate()
.map(|(i, data)| LoadingListItem::new(data, format!("Item {}", i + 1)))
.collect();
state.selected = None;
state.scroll.set_content_length(state.items.len());
None
}
LoadingListMessage::SetLoading(index) => {
if let Some(item) = state.items.get_mut(index) {
item.state = ItemState::Loading;
Some(LoadingListOutput::ItemStateChanged {
index,
state: ItemState::Loading,
})
} else {
None
}
}
LoadingListMessage::SetReady(index) => {
if let Some(item) = state.items.get_mut(index) {
item.state = ItemState::Ready;
Some(LoadingListOutput::ItemStateChanged {
index,
state: ItemState::Ready,
})
} else {
None
}
}
LoadingListMessage::SetError { index, message } => {
if let Some(item) = state.items.get_mut(index) {
let new_state = ItemState::Error(message.clone());
item.state = new_state.clone();
Some(LoadingListOutput::ItemStateChanged {
index,
state: new_state,
})
} else {
None
}
}
LoadingListMessage::ClearError(index) => {
if let Some(item) = state.items.get_mut(index) {
if item.is_error() {
item.state = ItemState::Ready;
return Some(LoadingListOutput::ItemStateChanged {
index,
state: ItemState::Ready,
});
}
}
None
}
LoadingListMessage::Up => {
if state.items.is_empty() {
return None;
}
let new_index = match state.selected {
Some(i) if i > 0 => i - 1,
Some(_) => state.items.len() - 1, None => state.items.len() - 1,
};
state.selected = Some(new_index);
state.scroll.ensure_visible(new_index);
Some(LoadingListOutput::SelectionChanged(new_index))
}
LoadingListMessage::Down => {
if state.items.is_empty() {
return None;
}
let new_index = match state.selected {
Some(i) if i < state.items.len() - 1 => i + 1,
Some(_) => 0, None => 0,
};
state.selected = Some(new_index);
state.scroll.ensure_visible(new_index);
Some(LoadingListOutput::SelectionChanged(new_index))
}
LoadingListMessage::First => {
if state.items.is_empty() {
return None;
}
state.selected = Some(0);
state.scroll.ensure_visible(0);
Some(LoadingListOutput::SelectionChanged(0))
}
LoadingListMessage::Last => {
if state.items.is_empty() {
return None;
}
let last = state.items.len() - 1;
state.selected = Some(last);
state.scroll.ensure_visible(last);
Some(LoadingListOutput::SelectionChanged(last))
}
LoadingListMessage::Select => {
if let Some(index) = state.selected {
if let Some(item) = state.items.get(index) {
return Some(LoadingListOutput::Selected(item.data.clone()));
}
}
None
}
LoadingListMessage::Tick => {
state.spinner_frame = (state.spinner_frame + 1) % 4;
None
}
}
}
fn handle_event(
_state: &Self::State,
event: &Event,
ctx: &EventContext,
) -> Option<Self::Message> {
if !ctx.focused || ctx.disabled {
return None;
}
if let Some(key) = event.as_key() {
match key.code {
Key::Up | Key::Char('k') => Some(LoadingListMessage::Up),
Key::Down | Key::Char('j') => Some(LoadingListMessage::Down),
Key::Enter => Some(LoadingListMessage::Select),
_ => None,
}
} else {
None
}
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
render::render_loading_list(
state,
ctx.frame,
ctx.area,
ctx.theme,
ctx.focused,
ctx.disabled,
);
}
}
#[cfg(test)]
mod snapshot_tests;
#[cfg(test)]
mod tests;