tix-tracker 0.10.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
//! Shared modal layout helper (FR-015, T011 / WP02).
//!
//! One pure layout helper — `modal_rect` — reused by the filter modal (this
//! WP) and the form modal (wired by WP03). It computes a centered region
//! ≈75% of the available `area`, clamped so it **never exceeds** `area` and
//! **never shrinks below a usable minimum** on tiny terminals; if even the
//! minimum can't fit (≤ ~20x5), it falls back to a fit — or full-area when the
//! minimum can't fit at all.

use ratatui::layout::Rect;

/// The criterion rows the filter modal renders, in the order Tab cycles them
/// (mirrors `FormFocus::ALL` ordering, FR-015).
pub const MODAL_FRACTION_W: u32 = 3; // 3/4
pub const MODAL_FRACTION_H: u32 = 3; // 3/4
pub const MODAL_DIVISOR: u32 = 4;

/// Minimum usable modal size; on terminals smaller than this the modal falls
/// back to the full area (or a fit-clamped area on tiny ≤ ~20x5 terminals).
pub const MIN_MODAL_W: u16 = 24;
pub const MIN_MODAL_H: u16 = 9;

/// A centered modal rect ≈75% of `area` width/height, clamped so it never
/// overflows `area` and never shrinks below the usable minimum; if even the
/// minimum can't fit (tiny ≤ ~20x5 → full-area fallback).
pub fn modal_rect(area: Rect) -> Rect {
    if area.width == 0 || area.height == 0 {
        return area;
    }

    // Tiny terminal (area smaller than the usable minimum): a centered
    // min-sized modal would overflow, so fall back to the full area — never
    // overflow, never panic on `clamp` (FR-015, T011).
    if area.width < MIN_MODAL_W || area.height < MIN_MODAL_H {
        return area;
    }

    let mut w = (area.width as u32 * MODAL_FRACTION_W / MODAL_DIVISOR) as u16;
    let mut h = (area.height as u32 * MODAL_FRACTION_H / MODAL_DIVISOR) as u16;
    w = w.clamp(MIN_MODAL_W, area.width);
    h = h.clamp(MIN_MODAL_H, area.height);

    let x = area.x + (area.width.saturating_sub(w)) / 2;
    let y = area.y + (area.height.saturating_sub(h)) / 2;
    Rect::new(x, y, w, h)
}

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

    fn modal_at(w: u16, h: u16) -> (u16, u16, u16, u16) {
        let r = modal_rect(Rect::new(0, 0, w, h));
        (r.x, r.y, r.width, r.height)
    }

    #[test]
    fn modal_120x40_is_centered_and_75_percent() {
        let (x, y, w, h) = modal_at(120, 40);
        assert_eq!((w, h), (90, 30));
        assert_eq!((x, y), (15, 5));
    }

    #[test]
    fn modal_80x24_is_centered_and_75_percent() {
        let (x, y, w, h) = modal_at(80, 24);
        assert_eq!((w, h), (60, 18));
        assert_eq!((x, y), (10, 3));
    }

    #[test]
    fn modal_tiny_20x5_does_not_overflow() {
        let (x, y, w, h) = modal_at(20, 5);
        assert!(w <= 20 && h <= 5, "tiny modal must not overflow: {w}x{h}");
        assert!(x + w <= 20 && y + h <= 5);
    }
}