use std::cell::Cell;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Padding, Paragraph, Wrap};
use tui_scrollview::{ScrollView, ScrollViewState, ScrollbarVisibility};
use crate::party::PartyEntry;
use crate::state::State;
use crate::tui::widgets::{PaletteSelector, ShimmerBlock};
use super::{Action, View, ViewResult};
const ITEM_HEIGHT: u16 = 5;
const SCROLL_PADDING: u16 = ITEM_HEIGHT;
const PALETTE_SELECTOR_WIDTH: u16 = 22;
struct PartyItem<'a> {
party: &'static PartyEntry,
enabled: bool,
selected: bool,
selecting_palette: bool,
palette_idx: usize,
palettes: Option<&'a Vec<String>>,
tick: u32,
}
impl<'a> PartyItem<'a> {
fn new(
party: &'static PartyEntry,
enabled: bool,
selected: bool,
selecting_palette: bool,
palette_idx: usize,
palettes: Option<&'a Vec<String>>,
tick: u32,
) -> Self {
Self {
party,
enabled,
selected,
selecting_palette,
palette_idx,
palettes,
tick,
}
}
}
impl<'a> Widget for PartyItem<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let inner = if self.selected {
let block = ShimmerBlock::new(self.tick);
let inner = block.inner(area).inner(Margin::new(1, 0));
block.render(area, buf);
inner
} else {
let block = Block::default()
.borders(Borders::ALL)
.padding(Padding::horizontal(1))
.border_style(Style::default().gray());
let inner = block.inner(area);
block.render(area, buf);
inner
};
let [left, right] = inner.layout(&Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(PALETTE_SELECTOR_WIDTH),
]));
let [top, bottom] = left.layout(&Layout::vertical([
Constraint::Length(1), Constraint::Length(2), ]));
let (status_symbol, status_text) = if self.enabled {
("✓".fg(Color::Green).dim(), "Enabled".dark_gray())
} else {
("✗".fg(Color::Red).dim(), "Disabled".dark_gray())
};
let title_and_status = Line::from(vec![
self.party.info.name.reset().bold(),
" (".dark_gray().dim(),
status_symbol,
" ".into(),
status_text,
")".dark_gray().dim(),
]);
title_and_status.render(top, buf);
let desc =
Paragraph::new(self.party.info.description.reset().dim()).wrap(Wrap { trim: true });
desc.render(bottom, buf);
let Some(palettes) = self.palettes else {
return;
};
if !self.party.info.supports_color || palettes.len() == 1 {
return;
}
let widget = PaletteSelector::new(palettes, self.palette_idx, self.selecting_palette);
widget.render(right, buf);
}
}
#[derive(Default, Clone, Copy)]
enum Mode {
#[default]
SelectingParty,
SelectingPalette {
palette_idx: usize,
},
}
#[derive(Default)]
pub struct PartyView {
selection: usize,
mode: Mode,
scroll_state: ScrollViewState,
viewport_height: Cell<u16>,
}
impl PartyView {
fn item_count(state: &State) -> usize {
state.unlocked_parties().count()
}
fn selected_party(&self, state: &State) -> Option<&'static PartyEntry> {
state.unlocked_parties().nth(self.selection)
}
fn update_scroll(&mut self) {
let viewport_height = self.viewport_height.get();
let selected_top = self.selection as u16 * ITEM_HEIGHT;
let selected_bottom = selected_top + ITEM_HEIGHT;
let current_offset = self.scroll_state.offset().y;
let viewport_bottom = current_offset + viewport_height;
if selected_bottom + SCROLL_PADDING > viewport_bottom {
let new_offset = (selected_bottom + SCROLL_PADDING).saturating_sub(viewport_height);
self.scroll_state.set_offset(Position::new(0, new_offset));
} else if selected_top < current_offset + SCROLL_PADDING {
let new_offset = selected_top.saturating_sub(SCROLL_PADDING);
self.scroll_state.set_offset(Position::new(0, new_offset));
}
}
fn palettes_for_selected_party<'b>(&'_ self, state: &'b State) -> Option<&'b Vec<String>> {
self.selected_party(state)
.map(|party| party.info.id)
.and_then(|id| state.unlocked_palettes(id))
}
}
impl View for PartyView {
fn render(&self, frame: &mut Frame, area: Rect, state: &State, tick: u32) {
self.viewport_height.set(area.height);
let content_area = area.inner(Margin::new(1, 0));
let content_width = content_area.width.saturating_sub(1); let content_height = Self::item_count(state) as u16 * ITEM_HEIGHT;
let mut scroll_view = ScrollView::new(Size::new(content_width, content_height))
.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
for (i, party) in state.unlocked_parties().enumerate() {
let enabled = state.is_party_enabled(party.info.id);
let selected = self.selection == i;
let (selecting_palette, palette_idx) = match self.mode {
Mode::SelectingParty => (false, state.selected_palette_idx(party.info.id)),
Mode::SelectingPalette { palette_idx } if selected => (true, palette_idx),
Mode::SelectingPalette { .. } => (false, state.selected_palette_idx(party.info.id)),
};
let item = PartyItem::new(
party,
enabled,
selected,
selecting_palette,
palette_idx,
state.unlocked_palettes(party.info.id),
tick,
);
let item_rect = Rect::new(0, i as u16 * ITEM_HEIGHT, content_width, ITEM_HEIGHT);
scroll_view.render_widget(item, item_rect);
}
frame.render_stateful_widget(scroll_view, content_area, &mut self.scroll_state.clone());
}
fn handle(&mut self, action: Action, state: &mut State) -> ViewResult {
let mode = self.mode;
match (action, mode) {
(Action::Up, Mode::SelectingParty) => {
let count = Self::item_count(state);
self.selection = (self.selection + count - 1) % count;
self.update_scroll();
ViewResult::Redraw
}
(Action::Up, Mode::SelectingPalette { palette_idx }) => {
let count = self.palettes_for_selected_party(state).map(|v| v.len());
if let Some(count) = count {
let palette_idx = (palette_idx + count) % (count + 1); self.mode = Mode::SelectingPalette { palette_idx };
}
ViewResult::Redraw
}
(Action::Down, Mode::SelectingParty) => {
self.selection = (self.selection + 1) % Self::item_count(state);
self.update_scroll();
ViewResult::Redraw
}
(Action::Down, Mode::SelectingPalette { palette_idx }) => {
let count = self.palettes_for_selected_party(state).map(|v| v.len());
if let Some(count) = count {
let palette_idx = (palette_idx + 1) % (count + 1); self.mode = Mode::SelectingPalette { palette_idx };
}
ViewResult::Redraw
}
(Action::Select, Mode::SelectingParty) => {
if let Some(party) = self.selected_party(state) {
state.toggle_party(party.info.id);
}
ViewResult::Redraw
}
(Action::Select, Mode::SelectingPalette { palette_idx }) => {
if let Some(party) = self.selected_party(state) {
state.set_selected_palette(party.info.id, palette_idx);
}
self.mode = Mode::SelectingParty;
ViewResult::Redraw
}
(Action::Back, Mode::SelectingPalette { .. }) => {
self.mode = Mode::SelectingParty;
ViewResult::Redraw
}
(Action::Palette, Mode::SelectingParty) => {
if let Some(party) = self.selected_party(state)
&& party.info.supports_color
&& let Some(palettes) = state.unlocked_palettes(party.info.id)
&& palettes.len() > 1
{
let palette_idx = state.selected_palette_idx(party.info.id);
self.mode = Mode::SelectingPalette { palette_idx };
}
ViewResult::Redraw
}
(Action::Palette, Mode::SelectingPalette { .. }) => {
self.mode = Mode::SelectingParty;
ViewResult::Redraw
}
_ => ViewResult::None,
}
}
fn key_hints(&self) -> Vec<(&'static str, &'static str)> {
match self.mode {
Mode::SelectingParty => vec![
("↑↓", "select"),
("enter", "toggle"),
("p", "change palette"),
("q", "quit"),
],
Mode::SelectingPalette { .. } => vec![
("↑↓", "select"),
("enter", "set palette"),
("esc", "cancel"),
("q", "quit"),
],
}
}
}