xiangqi_tui 0.1.0

Chinese chess (Xiangqi) TUI client with UCI/UCCI engine and opening book support
use ratatui::{
    Frame,
    layout::{Constraint, Direction, Layout, Rect},
    text::{Line, Span},
    widgets::{Block, Borders, Paragraph},
};
use unicode_width::UnicodeWidthStr;

use super::style::{
    border_focused, border_normal, highlight, text as text_style, text_bold, text_dim,
};
use super::{display_or_placeholder, yes_no};
use crate::app::{App, SettingsField, settings_field::pick_mode_label};

const ROW_HEIGHT: u16 = 1;
const SEPARATOR_HEIGHT: u16 = 1;

fn label_column_width() -> usize {
    SettingsField::ALL
        .iter()
        .map(|field| field.label().width())
        .max()
        .unwrap_or(8)
        .max(8)
}

fn pad_label(label: &str, width: usize) -> String {
    let w = label.width();
    if w >= width {
        label.to_string()
    } else {
        format!("{label}{}", " ".repeat(width - w))
    }
}

fn settings_layout_constraints() -> Vec<Constraint> {
    let mut constraints = Vec::with_capacity(SettingsField::ALL.len() + 1);
    for field in SettingsField::ALL {
        if field == SettingsField::FIRST_BOOK {
            constraints.push(Constraint::Length(SEPARATOR_HEIGHT));
        }
        constraints.push(Constraint::Length(ROW_HEIGHT));
    }
    constraints
}

pub struct SettingsFormRegions {
    pub fields: [(SettingsField, Rect); SettingsField::ALL.len()],
    pub field_count: usize,
}

pub fn render_settings_form(frame: &mut Frame<'_>, area: Rect, app: &App) -> SettingsFormRegions {
    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints(settings_layout_constraints())
        .split(area);

    let mut fields = [(SettingsField::EnginePath, Rect::default()); SettingsField::ALL.len()];
    let mut field_count = 0usize;
    let label_w = label_column_width();
    let mut row_idx = 0usize;

    for field in SettingsField::ALL {
        if field == SettingsField::FIRST_BOOK {
            let sep_area = rows[row_idx];
            frame.render_widget(
                Paragraph::new(Line::from(Span::styled(
                    "──────────── 开局库 ────────────",
                    text_dim(),
                )))
                .block(Block::default().borders(Borders::NONE)),
                sep_area,
            );
            row_idx += 1;
        }

        let row_area = rows[row_idx];
        row_idx += 1;
        let focused = app.settings_field == field;
        let value = field_value(app, field);
        let label = pad_label(field.label(), label_w);
        let line = Line::from(vec![
            Span::styled(
                format!("{label}  "),
                if focused { highlight() } else { text_bold() },
            ),
            Span::styled(value, if focused { highlight() } else { text_style() }),
        ]);
        frame.render_widget(
            Paragraph::new(line).block(Block::default().borders(Borders::NONE)),
            row_area,
        );
        if field_count < fields.len() {
            fields[field_count] = (field, row_area);
            field_count += 1;
        }
    }

    SettingsFormRegions {
        fields,
        field_count,
    }
}

fn field_value(app: &App, field: SettingsField) -> String {
    match field {
        SettingsField::EnginePath => display_or_placeholder(&app.engine.path),
        SettingsField::EngineProtocol => app.engine.protocol.label().to_string(),
        SettingsField::EngineThreads => app.engine.threads.to_string(),
        SettingsField::EngineHashMb => app.engine.hash_mb.to_string(),
        SettingsField::EngineSkill => app.engine.skill_level.to_string(),
        SettingsField::EngineMultiPv => app.engine.multi_pv.to_string(),
        SettingsField::EngineSearchLimit => app.engine.search_limit.label().to_string(),
        SettingsField::EngineMovetimeMs => app.engine.movetime_ms.to_string(),
        SettingsField::EngineSearchDepth => app.engine.search_depth.to_string(),
        SettingsField::EngineSearchNodes => app.engine.search_nodes.to_string(),
        SettingsField::BookLocalPath => display_or_placeholder(&app.book.local_path),
        SettingsField::BookLocalEnabled => yes_no(app.book.local_enabled).to_string(),
        SettingsField::BookCloudEnabled => yes_no(app.book.cloud_enabled).to_string(),
        SettingsField::BookPickMode => pick_mode_label(&app.book.pick_mode).to_string(),
        SettingsField::BookMaxHalfmoves => app.book.max_halfmoves.to_string(),
    }
}

pub fn form_block(focused: bool) -> Block<'static> {
    Block::default()
        .borders(Borders::ALL)
        .border_style(if focused {
            border_focused()
        } else {
            border_normal()
        })
        .title(Span::styled(
            "设置(↑↓ 选行,空格/←→ 改值)".to_string(),
            text_bold(),
        ))
}

pub fn settings_hint(field: SettingsField) -> String {
    format!("{} {}", field.label(), field.hint())
}

pub fn settings_form_row_count() -> u16 {
    SettingsField::ALL.len() as u16 + 1
}