tododo 0.1.2

A minimal terminal todo manager built with Rust and Ratatui
Documentation
use ratatui::{
    layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
    style::{Color, Style},
    widgets::{Block, Borders, Clear, Paragraph},
};

use crate::actions::AppAction;

const MODAL_WIDTH: u16 = 36;
const MODAL_HEIGHT: u16 = 7;
const YES_LABEL: &str = "[y] Yes";
const NO_LABEL: &str = "[n] No";
const BUTTON_GAP_WIDTH: u16 = 4;

fn centered_modal(area: Rect) -> Rect {
    let vertical = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Fill(1),
            Constraint::Length(MODAL_HEIGHT),
            Constraint::Fill(1),
        ])
        .split(area);

    let horizontal = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([
            Constraint::Fill(1),
            Constraint::Length(MODAL_WIDTH),
            Constraint::Fill(1),
        ])
        .split(vertical[1]);

    horizontal[1]
}

pub fn render_delete_confirm(frame: &mut ratatui::Frame, todo_title: Option<&str>) {
    let modal = centered_modal(frame.area());
    frame.render_widget(Clear, modal);

    let block = Block::default()
        .title(" Confirm Delete ")
        .title_alignment(Alignment::Left)
        .borders(Borders::ALL)
        .border_style(Style::new().fg(Color::White));

    frame.render_widget(block, modal);

    let title = todo_title.unwrap_or("(unknown)");
    let inner = modal.inner(Margin {
        vertical: 1,
        horizontal: 1,
    });
    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(1),
            Constraint::Length(1),
            Constraint::Fill(1),
            Constraint::Length(1),
        ])
        .split(inner);

    frame.render_widget(
        Paragraph::new("Delete this todo?").alignment(Alignment::Center),
        rows[0],
    );
    frame.render_widget(Paragraph::new(title).alignment(Alignment::Center), rows[1]);
    if let Some((yes_rect, no_rect)) = button_rects(modal) {
        frame.render_widget(Paragraph::new(YES_LABEL), yes_rect);
        frame.render_widget(Paragraph::new(NO_LABEL), no_rect);
    }
}

pub fn modal_rect(area: Rect) -> Rect {
    centered_modal(area)
}

pub fn click_to_action(
    row: u16,
    col: u16,
    area: Rect,
    selected_id: Option<&str>,
) -> Option<AppAction> {
    let modal = modal_rect(area);
    let (yes_rect, no_rect) = button_rects(modal)?;
    if row != yes_rect.y {
        return None;
    }
    if col >= yes_rect.x && col < yes_rect.x.saturating_add(yes_rect.width) {
        selected_id.map(|id| AppAction::DeleteTodo(id.to_string()))
    } else if col >= no_rect.x && col < no_rect.x.saturating_add(no_rect.width) {
        Some(AppAction::CloseDeleteConfirm)
    } else {
        None
    }
}

fn button_rects(modal: Rect) -> Option<(Rect, Rect)> {
    if modal.width < 3 || modal.height < 3 {
        return None;
    }
    let inner = modal.inner(Margin {
        vertical: 1,
        horizontal: 1,
    });
    let yes_width = YES_LABEL.chars().count() as u16;
    let no_width = NO_LABEL.chars().count() as u16;
    let buttons_width = yes_width + BUTTON_GAP_WIDTH + no_width;
    if inner.width < buttons_width || inner.height == 0 {
        return None;
    }

    let button_row = inner.y + inner.height.saturating_sub(1);
    let buttons_start = inner.x + (inner.width - buttons_width) / 2;
    let yes_rect = Rect::new(buttons_start, button_row, yes_width, 1);
    let no_rect = Rect::new(
        buttons_start + yes_width + BUTTON_GAP_WIDTH,
        button_row,
        no_width,
        1,
    );
    Some((yes_rect, no_rect))
}