travelagent 1.10.3

Agent-first TUI code review tool
//! Reaction picker popup — rendered centered over the frame when
//! `InputMode::ReactionPicker` is active. The human browses 8 emoji with
//! left/right keys (or digits 1..=8) and picks one with Enter.

use ratatui::{
    Frame,
    layout::{Constraint, Flex, Layout, Rect},
    style::{Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Clear, Paragraph},
};
use travelagent_core::forge::ReactionContent;

use crate::app::App;
use crate::theme::Theme;
use crate::ui::styles;

/// The canonical list of reactions shown in the picker and submitted when the
/// human selects one. Order matches the digit shortcut (1..=8).
///
/// Tuple layout: `(ReactionContent, emoji, label)`.
///
/// **Do not reorder** — digit shortcuts (`ReactionPickerSelectAt(idx)`) and
/// saved cursor positions both index into this table. Append new entries at
/// the end if you must extend it.
pub const REACTIONS: [(ReactionContent, &str, &str); 8] = [
    (ReactionContent::ThumbsUp, "\u{1F44D}", "+1"),
    (ReactionContent::ThumbsDown, "\u{1F44E}", "-1"),
    (ReactionContent::Laugh, "\u{1F604}", "laugh"),
    (ReactionContent::Hooray, "\u{1F389}", "tada"),
    (ReactionContent::Confused, "\u{1F615}", "?"),
    (ReactionContent::Heart, "\u{2764}\u{FE0F}", "heart"),
    (ReactionContent::Rocket, "\u{1F680}", "rocket"),
    (ReactionContent::Eyes, "\u{1F440}", "eyes"),
];

/// Render the reaction picker popup centered over `area`. Callers should gate
/// on `app.nav.input_mode == InputMode::ReactionPicker` before invoking.
pub fn render(frame: &mut Frame, area: Rect, app: &App, theme: &Theme) {
    // 8 entries × ~11 cols each + borders + padding. Clamp to available width.
    let desired_width: u16 = 72;
    let popup_area = centered_rect(desired_width, 5, area);

    frame.render_widget(Clear, popup_area);

    let block = Block::default()
        .title(" Reactions ")
        .borders(Borders::ALL)
        .style(styles::popup_style(theme))
        .border_style(styles::border_style(theme, true));

    let inner = block.inner(popup_area);
    frame.render_widget(block, popup_area);

    if inner.height == 0 {
        return;
    }

    // Split into a body row and a hint row.
    let rows = Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).split(inner);

    // Body: 8 emoji + label pairs separated by spaces, highlighting the cursor.
    let mut spans: Vec<Span> = Vec::with_capacity(REACTIONS.len() * 3);
    for (idx, (_, emoji, label)) in REACTIONS.iter().enumerate() {
        if idx > 0 {
            spans.push(Span::raw(" "));
        }
        let entry = format!("{emoji} {label}");
        let style = if idx == app.reaction_picker_cursor {
            Style::default().add_modifier(Modifier::REVERSED)
        } else {
            Style::default()
        };
        spans.push(Span::styled(entry, style));
    }
    let body = Paragraph::new(Line::from(spans)).alignment(ratatui::layout::Alignment::Center);
    frame.render_widget(body, rows[0]);

    // Footer hint row.
    if rows.len() > 1 && rows[1].height > 0 {
        let hint = Paragraph::new(Line::from(Span::styled(
            "\u{2190}/\u{2192} move  Enter select  1-8 quick  Esc cancel",
            styles::dim_style(theme),
        )))
        .alignment(ratatui::layout::Alignment::Center);
        frame.render_widget(hint, rows[1]);
    }
}

#[must_use]
fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
    let width = width.min(area.width);
    let height = height.min(area.height);
    let vertical = Layout::vertical([Constraint::Length(height)]).flex(Flex::Center);
    let horizontal = Layout::horizontal([Constraint::Length(width)]).flex(Flex::Center);
    let [area] = vertical.areas(area);
    let [area] = horizontal.areas(area);
    area
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn reactions_table_has_eight_distinct_contents() {
        assert_eq!(REACTIONS.len(), 8);
        for (i, a) in REACTIONS.iter().enumerate() {
            for (j, b) in REACTIONS.iter().enumerate() {
                assert_eq!(i == j, a.0 == b.0);
            }
        }
    }
}