Readable Fullscreen Modals for Stats, Standings/Bracket, and History

This commit is contained in:
markmental 2026-02-10 18:38:32 -05:00
commit dcdd06c8c3
5 changed files with 179 additions and 45 deletions

View file

@ -1,6 +1,6 @@
use ratatui::layout::{Constraint, Direction, Layout};
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Tabs};
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
use crate::app::App;
use crate::instance::SimStatus;
@ -17,8 +17,8 @@ pub fn render(f: &mut Frame<'_>, area: Rect, app: &App) {
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(6),
Constraint::Length(12),
Constraint::Min(8),
Constraint::Length(2),
])
.split(area);
@ -69,41 +69,7 @@ pub fn render(f: &mut Frame<'_>, area: Rect, app: &App) {
);
f.render_widget(side, middle[1]);
let bottom = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(34),
Constraint::Percentage(33),
Constraint::Percentage(33),
])
.split(chunks[2]);
let stats = List::new(
inst.stats_lines
.iter()
.map(|s| ListItem::new(s.clone()))
.collect::<Vec<_>>(),
)
.block(Block::default().title("Stats").borders(Borders::ALL));
let comp = List::new(
inst.competition_lines
.iter()
.map(|s| ListItem::new(s.clone()))
.collect::<Vec<_>>(),
)
.block(Block::default().title("Competition").borders(Borders::ALL));
let hist = List::new(
inst.history_lines
.iter()
.map(|s| ListItem::new(s.clone()))
.collect::<Vec<_>>(),
)
.block(Block::default().title("History").borders(Borders::ALL));
f.render_widget(stats, bottom[0]);
f.render_widget(comp, bottom[1]);
f.render_widget(hist, bottom[2]);
let tabs = Tabs::new(vec!["Dashboard", "Detail"]).select(1);
let _ = tabs;
let help = Paragraph::new("Open readable panels: t=Stats, g=Standings/Bracket, h=History")
.block(Block::default().borders(Borders::ALL).title("Panels"));
f.render_widget(help, chunks[2]);
}

View file

@ -19,7 +19,7 @@ pub fn draw(f: &mut Frame<'_>, app: &App) {
])
.split(f.area());
let header = Paragraph::new("MentalNet SoccerCloud | n/l/o create | s start | c clone | d delete | e export | v detail | q quit")
let header = Paragraph::new("MentalNet SoccerCloud | n/l/o create | s start | c clone | d delete | e export | v detail | t stats | g standings | h history | q quit")
.block(Block::default().title("Dashboard").borders(Borders::ALL))
.style(Style::default().fg(Color::Cyan));
f.render_widget(header, areas[0]);
@ -31,7 +31,7 @@ pub fn draw(f: &mut Frame<'_>, app: &App) {
}
let footer = Paragraph::new(format!(
"{} | speed={} (1/2/4/0) | modal: m=manual p=cpu [ ] team Enter=create Esc=cancel",
"{} | speed={} (1/2/4/0) | create modal: m=manual p=cpu [ ] team Enter=create Esc=cancel | view modal: j/k scroll Esc/q close",
app.status_line,
app.speed.label()
))
@ -40,6 +40,10 @@ pub fn draw(f: &mut Frame<'_>, app: &App) {
f.render_widget(footer, areas[2]);
if let Some(draft) = &app.create_draft {
modal::render(f, f.area(), app, draft);
modal::render_create(f, f.area(), app, draft);
}
if let Some(kind) = app.overlay_modal {
modal::render_overlay(f, f.area(), app, kind);
}
}

View file

@ -2,10 +2,10 @@ use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph};
use crate::app::{App, CreateDraft};
use crate::app::{App, CreateDraft, OverlayModal};
use crate::data::TEAMS;
pub fn render(f: &mut Frame<'_>, area: Rect, app: &App, draft: &CreateDraft) {
pub fn render_create(f: &mut Frame<'_>, area: Rect, app: &App, draft: &CreateDraft) {
let popup = centered_rect(70, 70, area);
f.render_widget(Clear, popup);
@ -59,6 +59,82 @@ pub fn render(f: &mut Frame<'_>, area: Rect, app: &App, draft: &CreateDraft) {
f.render_widget(help, inner[2]);
}
pub fn render_overlay(f: &mut Frame<'_>, area: Rect, app: &App, modal: OverlayModal) {
let popup = centered_rect(90, 86, area);
f.render_widget(Clear, popup);
let frame = Block::default()
.title(modal.title())
.borders(Borders::ALL)
.style(Style::default().fg(Color::White).bg(Color::Black));
f.render_widget(frame, popup);
let inner = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([
Constraint::Length(1),
Constraint::Min(4),
Constraint::Length(1),
])
.split(popup);
let Some(inst) = app.selected_instance() else {
f.render_widget(Paragraph::new("No selected instance"), inner[1]);
return;
};
let all_lines: Vec<String> = match modal {
OverlayModal::Stats => {
if inst.stats_lines.is_empty() {
vec!["No stats available yet. Start a simulation first.".to_string()]
} else {
inst.stats_lines.clone()
}
}
OverlayModal::Competition => {
if inst.competition_lines.is_empty() {
vec!["No standings/bracket available for this simulation yet.".to_string()]
} else {
inst.competition_lines.clone()
}
}
OverlayModal::History => {
if inst.history_lines.is_empty() {
vec!["No history entries yet.".to_string()]
} else {
inst.history_lines.clone()
}
}
};
let viewport = (inner[1].height as usize).saturating_sub(2).max(1);
let max_start = all_lines.len().saturating_sub(viewport);
let start = app.overlay_scroll.min(max_start);
let end = (start + viewport).min(all_lines.len());
let visible = &all_lines[start..end];
let items: Vec<ListItem> = visible
.iter()
.map(|line| ListItem::new(line.clone()))
.collect();
let top = Paragraph::new(format!(
"sim-{} [{}] lines {}-{} / {}",
inst.id,
inst.sim_type.as_str(),
start + 1,
end,
all_lines.len()
));
f.render_widget(top, inner[0]);
let list = List::new(items).block(Block::default().borders(Borders::ALL));
f.render_widget(list, inner[1]);
let help = Paragraph::new("j/k or up/down to scroll, Esc or q to close");
f.render_widget(help, inner[2]);
}
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
let vertical = Layout::default()
.direction(Direction::Vertical)