trnovel 0.10.0

Terminal reader for novel
Documentation
use crate::{
    components::{
        Loading,
        list_view::{ListView, RenderItem},
    },
    hooks::UseThemeConfig,
};
use crossterm::event::{Event, KeyCode, KeyEventKind};
use ratatui::{
    layout::{Alignment, Constraint},
    widgets::Block,
};
use ratatui_kit::prelude::*;
use tui_widget_list::ListState;

#[derive(Props)]
pub struct ListSelectProps<T>
where
    T: Sync + Send + Clone + 'static,
{
    pub items: Vec<T>,
    pub on_select: ratatui_kit::Handler<'static, T>,
    pub default_value: Option<usize>,
    pub top_title: Option<ratatui::text::Line<'static>>,
    pub bottom_title: Option<ratatui::text::Line<'static>>,
    pub is_editing: bool,
    pub render_item: RenderItem<'static>,
    pub state: Option<ratatui_kit::State<ListState>>,
    pub empty_message: String,
    pub loading: bool,
    pub loading_tip: String,
}

impl<T> Default for ListSelectProps<T>
where
    T: Sync + Send + Clone + 'static,
{
    fn default() -> Self {
        Self {
            items: vec![],
            on_select: Handler::default(),
            default_value: None,
            top_title: None,
            bottom_title: None,
            is_editing: false,
            render_item: RenderItem::default(),
            state: None,
            empty_message: String::default(),
            loading: false,
            loading_tip: String::from("加载中..."),
        }
    }
}

#[component]
pub fn ListSelect<T>(
    props: &mut ListSelectProps<T>,
    mut hooks: Hooks,
) -> impl Into<AnyElement<'static>>
where
    T: Unpin + Sync + Clone + Send + 'static,
{
    let theme = hooks.use_theme_config();
    let state = hooks.use_state(|| {
        let mut state = ListState::default();
        state.select(props.default_value);
        state
    });
    let is_empty = props.items.is_empty();

    let state = props.state.unwrap_or(state);

    hooks.use_events({
        let is_editing = props.is_editing;
        let mut on_select = props.on_select.take();
        let data = props.items.clone();
        move |event| {
            if let Event::Key(key) = event
                && key.kind == KeyEventKind::Press
                && is_editing
            {
                match key.code {
                    KeyCode::Char('j') | KeyCode::Down => {
                        state.write().next();
                    }
                    KeyCode::Char('k') | KeyCode::Up => {
                        state.write().previous();
                    }
                    KeyCode::Enter => {
                        let res = state.read().selected;
                        if let Some(path) = res {
                            on_select(data[path].clone());
                        }
                    }
                    _ => {}
                }
            }
        }
    });

    let mut border = Block::bordered().border_style(theme.basic.border);

    if let Some(title) = props.top_title.clone() {
        border = border.title_top(title);
    }
    if let Some(title) = props.bottom_title.clone() {
        border = border.title_bottom(title);
    }

    if props.loading {
        return element!(
            Border(
                top_title: props.top_title.clone(),
                bottom_title: props.bottom_title.clone(),
                border_style: theme.basic.border,
            ){
                Loading(tip: props.loading_tip.clone())
            }
        )
        .into_any();
    }

    if is_empty {
        return element!(
            Border(
                top_title: props.top_title.clone(),
                bottom_title: props.bottom_title.clone(),
                border_style: theme.basic.border,
            ){
                Center(
                    height:Constraint::Length(5),
                    width:Constraint::Percentage(50)
                ){
                    Text(
                        text: props.empty_message.clone(),
                        alignment: Alignment::Center,
                        style: theme.colors.warning_color,
                        wrap: true,
                    )
                }
            }
        )
        .into_any();
    }

    element!(ListView(
        state: state,
        block: border,
        render_item: props.render_item.take(),
        item_count: props.items.len(),
    ))
    .into_any()
}