use ratatui::{ Frame, layout::{Constraint, Layout, Rect}, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap}, }; use crate::app::{App, InputMode}; use crate::state::Stage; const HELP_TEXT: &str = "\ Parliamentary Process Tracker — Keybindings Presenting m New moción (opens $EDITOR, only when stack is empty) e New enmienda (opens $EDITOR, amends current item) E New enmienda sustitutiva (opens $EDITOR, replaces current item) r Re-edit current item's text in $EDITOR R Correct the last passed moción's text in $EDITOR Moción flow s Secundar current item n Not secundada — remove from stack o There is opposition → open voting O No opposition → item passes f / c +1 vote in favor / contra (optional — v opens editor instead) v Enter final vote counts in $EDITOR → auto-finalizes Overrides p Force pass current item (bypass stages) x Skip / fail current item Output w Save passed mociones to the output file ? Toggle this help q Quit (auto-saves) Press any key to close."; pub fn draw(frame: &mut Frame, app: &App) { let [title_area, main_area, bottom_area] = Layout::vertical([ Constraint::Length(1), Constraint::Min(0), Constraint::Length(3), ]) .areas(frame.area()); let [stack_area, passed_area] = Layout::horizontal([ Constraint::Percentage(55), Constraint::Percentage(45), ]) .areas(main_area); render_title(frame, app, title_area); render_stack(frame, app, stack_area); render_passed(frame, app, passed_area); render_bottom(frame, app, bottom_area); if matches!(app.mode, InputMode::ShowingHelp) { render_help_overlay(frame); } } fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect { let margin_v = (100 - percent_y) / 2; let margin_h = (100 - percent_x) / 2; let [_, mid_v, _] = Layout::vertical([ Constraint::Percentage(margin_v), Constraint::Percentage(percent_y), Constraint::Percentage(margin_v), ]) .areas(area); let [_, center, _] = Layout::horizontal([ Constraint::Percentage(margin_h), Constraint::Percentage(percent_x), Constraint::Percentage(margin_h), ]) .areas(mid_v); center } fn render_help_overlay(frame: &mut Frame) { let area = centered_rect(60, 85, frame.area()); frame.render_widget(Clear, area); let block = Block::default() .title(" Help ") .borders(Borders::ALL) .border_style(Style::default().fg(Color::Cyan)); let p = Paragraph::new(HELP_TEXT) .block(block) .wrap(Wrap { trim: false }); frame.render_widget(p, area); } fn render_title(frame: &mut Frame, app: &App, area: Rect) { let title = Line::from(vec![ Span::styled(" parl ", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)), Span::raw("─ "), Span::styled( format!("{} passed", app.passed.len()), Style::default().fg(Color::Green), ), if app.stack.is_empty() { Span::raw("") } else { Span::styled( format!(" {} on stack", app.stack.len()), Style::default().fg(Color::Yellow), ) }, ]); frame.render_widget(Paragraph::new(title), area); } fn render_stack(frame: &mut Frame, app: &App, area: Rect) { let block = Block::default() .title(" Stack (current on top) ") .borders(Borders::ALL) .border_style(Style::default().fg(Color::DarkGray)); if app.stack.is_empty() { let p = Paragraph::new("(empty — press 'm' to present a moción)") .style(Style::default().fg(Color::DarkGray)) .block(block); frame.render_widget(p, area); return; } let inner = block.inner(area); frame.render_widget(block, area); let list_height = u16::try_from(app.stack.len()) .unwrap_or(u16::MAX) .min(inner.height.saturating_sub(4)); let [list_area, text_area] = Layout::vertical([ Constraint::Length(list_height), Constraint::Min(0), ]) .areas(inner); let items: Vec = app .stack .iter() .rev() .enumerate() .map(|(i, item)| { let is_current = i == 0; let prefix = if is_current { "► " } else { " " }; let nesting: String = " ".repeat(i); let line = Line::from(vec![ Span::raw(format!("{prefix}{nesting}")), Span::styled( item.kind.label(), if is_current { Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) } else { Style::default().fg(Color::DarkGray) }, ), Span::styled( format!(" {}", item.stage.label()), if is_current { Style::default().fg(Color::White) } else { Style::default().fg(Color::DarkGray) }, ), ]); ListItem::new(line) }) .collect(); frame.render_widget(List::new(items), list_area); if let Some(current) = app.stack.last() { let text_block = Block::default() .title(format!(" {} text ", current.kind.label())) .borders(Borders::TOP) .border_style(Style::default().fg(Color::DarkGray)); let p = Paragraph::new(current.text.as_str()) .block(text_block) .wrap(Wrap { trim: false }) .style(Style::default().fg(Color::White)); frame.render_widget(p, text_area); } } fn render_passed(frame: &mut Frame, app: &App, area: Rect) { let block = Block::default() .title(" Passed Mociones ") .borders(Borders::ALL) .border_style(Style::default().fg(Color::DarkGray)); if app.passed.is_empty() { let p = Paragraph::new("(none yet)") .style(Style::default().fg(Color::DarkGray)) .block(block); frame.render_widget(p, area); return; } let items: Vec = app .passed .iter() .rev() .map(|m| { let preview: String = m.text.chars().take(60).collect(); let ellipsis = if m.text.len() > 60 { "…" } else { "" }; ListItem::new(Line::from(vec![ Span::styled( format!("{}. ", m.number), Style::default().fg(Color::Green).add_modifier(Modifier::BOLD), ), Span::raw(format!("{preview}{ellipsis}")), ])) }) .collect(); frame.render_widget(List::new(items).block(block), area); } fn render_bottom(frame: &mut Frame, app: &App, area: Rect) { let [status_area, hints_area] = Layout::vertical([Constraint::Length(1), Constraint::Length(2)]).areas(area); frame.render_widget( Paragraph::new(app.status.as_str()).style(Style::default().fg(Color::White)), status_area, ); frame.render_widget( Paragraph::new(context_hints(app)).style(Style::default().fg(Color::DarkGray)), hints_area, ); } fn context_hints(app: &App) -> String { let always = " r: edit R: fix w: save ?: help q: quit"; if app.stack.is_empty() { return format!("m: new moción{always}"); } let stage_hints = match app.stack.last().map(|i| &i.stage) { Some(Stage::AwaitingSecundar) => "s: sec. n: no sec. e: enm. E: susti. p: pasar x: skip", Some(Stage::AwaitingOpposition) => "o: opos. O: no opos. e: enm. E: susti. p: pasar x: skip", Some(Stage::Voting(_)) => "f: +1 c: -1 v: final p: pasar x: skip", None => "", }; format!("{stage_hints}{always}") }