mlbt 0.1.0

A terminal user interface for the MLB stats API. Watch a baseball game in your terminal! ⚾
use crate::components::stats::{
    ActivePane, STATS_DEFAULT_COL_WIDTH, STATS_FIRST_COL_WIDTH, StatsState, TeamOrPlayer,
};
use mlbt_api::client::StatGroup;
use tui::prelude::*;
use tui::widgets::{Block, BorderType, Borders, Cell, Padding, Paragraph, Row, Table, Wrap};

pub const STATS_OPTIONS_WIDTH: u16 = 36;

pub struct StatsWidget {
    pub show_options: bool,
}

impl StatefulWidget for StatsWidget {
    type State = StatsState;

    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
        let constraints = match self.show_options {
            true => {
                vec![
                    Constraint::Length(area.width - STATS_OPTIONS_WIDTH), // stats
                    Constraint::Length(STATS_OPTIONS_WIDTH),              // options
                ]
            }
            false => vec![Constraint::Percentage(100)],
        };
        let chunks = Layout::horizontal(constraints).split(area);

        let (header, rows) = state.generate_table();

        // use the sort column to include up/down arrow in the column name
        let sort_column = state.sorting.column_name.as_deref().unwrap_or_default();
        let header = header
            .into_iter()
            .map(|name| {
                if name == sort_column {
                    Cell::from(format!("{name} {}", state.sorting.order.arrow_symbol()))
                        .style(Style::default().bg(Color::Blue))
                } else {
                    Cell::from(name)
                }
            })
            .collect::<Row>()
            .height(1)
            .style(Style::default().add_modifier(Modifier::BOLD | Modifier::UNDERLINED));

        let rows: Vec<Row> = rows.into_iter().map(Row::new).collect();

        // Create the options rows, e.g. ["[X]", "ERA", "earned run average"]
        let mut active = 0;
        let mut options = Vec::new();
        for (name, stat) in &state.stats {
            let selected = match stat.active {
                true => {
                    active += 1;
                    "[X]"
                }
                false => "[ ]",
            };
            options.push(Row::new(vec![
                selected.to_string(),
                name.clone(),
                stat.description.clone(),
            ]));
        }

        // Build the constraints. On first load the active will be 0, hence the check.
        let mut constraints = vec![Constraint::Length(STATS_DEFAULT_COL_WIDTH); active];
        if active == 0 {
            constraints.push(Constraint::Length(STATS_FIRST_COL_WIDTH));
        } else {
            constraints[0] = Constraint::Length(STATS_FIRST_COL_WIDTH);
        }

        // stats
        let mut t = Table::new(rows, constraints)
            .header(header)
            .column_spacing(0)
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .border_type(BorderType::Rounded)
                    .padding(Padding::new(1, 1, 0, 0))
                    .title(Span::styled(
                        state.date_selector.format_date_border_title(),
                        Style::default().fg(Color::Black).bg(Color::Blue),
                    )),
            );
        if state.active_pane == ActivePane::Data {
            t = t.row_highlight_style(Style::default().bg(Color::Blue).fg(Color::Black));
        }

        // borders (2) + header (1) = 3 rows of overhead
        state.visible_rows = chunks[0].height.saturating_sub(3) as usize;

        StatefulWidget::render(t, chunks[0], buf, &mut state.data_state);

        if self.show_options {
            let selected_style = Style::default().bg(Color::Blue).fg(Color::Black);

            let [stats_rect, options_rect] =
                Layout::vertical([Constraint::Length(4), Constraint::Percentage(100)])
                    .areas(chunks[1]);
            // hitting | pitching
            // team | player
            let (hitting_style, pitching_style) = match state.stat_type.group {
                StatGroup::Pitching => (Style::default(), selected_style),
                StatGroup::Hitting => (selected_style, Style::default()),
            };
            let (team_style, player_style) = match state.stat_type.team_player {
                TeamOrPlayer::Player => (Style::default(), selected_style),
                TeamOrPlayer::Team => (selected_style, Style::default()),
            };
            let text = vec![
                Line::from(vec![
                    Span::styled("hitting", hitting_style),
                    Span::raw(" | "),
                    Span::styled("pitching", pitching_style),
                ]),
                Line::from(vec![
                    Span::styled("team", team_style),
                    Span::raw(" | "),
                    Span::styled("player", player_style),
                ]),
            ];
            Paragraph::new(text)
                .block(
                    Block::default()
                        .borders(Borders::ALL)
                        .border_type(BorderType::Rounded),
                )
                .alignment(Alignment::Center)
                .wrap(Wrap { trim: true })
                .render(stats_rect, buf);

            // options
            let widths = [
                Constraint::Length(4),
                Constraint::Length(6),
                Constraint::Length(25),
            ];
            let mut t = Table::new(options, widths).column_spacing(0).block(
                Block::default()
                    .padding(Padding::new(1, 1, 0, 0))
                    .borders(Borders::ALL)
                    .border_type(BorderType::Rounded),
            );
            if state.active_pane == ActivePane::Options {
                t = t.row_highlight_style(selected_style);
            }
            StatefulWidget::render(t, options_rect, buf, &mut state.options_state);
        }
    }
}