tazuna 0.1.0

TUI tool for managing multiple Claude Code sessions in parallel
Documentation
//! Popup widgets for TUI.
//!
//! Provides session list popup, workspace popup, and notification popups.

mod action_select;
mod confirm;
mod error;
mod issue_loading;
mod items;
mod state;
mod workspace;

use ratatui::layout::Rect;

/// Sizing hints for popup layout.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SizeHint {
    pub percent_x: u16,
    pub percent_y: u16,
    pub min_width: Option<u16>,
    pub min_height: Option<u16>,
}

impl SizeHint {
    /// Create size hint with percentages only.
    #[must_use]
    pub const fn percent(x: u16, y: u16) -> Self {
        Self {
            percent_x: x,
            percent_y: y,
            min_width: None,
            min_height: None,
        }
    }

    /// Add minimum width constraint.
    #[must_use]
    pub const fn with_min_width(mut self, w: u16) -> Self {
        self.min_width = Some(w);
        self
    }

    /// Add minimum height constraint.
    #[must_use]
    pub const fn with_min_height(mut self, h: u16) -> Self {
        self.min_height = Some(h);
        self
    }
}

/// Trait for popups that compute their own sizing.
pub trait PopupSizing {
    /// Return sizing hints for this popup.
    fn size_hint(&self) -> SizeHint;

    /// Compute centered area within container using size hints.
    fn compute_area(&self, container: Rect) -> Rect {
        centered_rect_with_hint(self.size_hint(), container)
    }
}

/// Calculate centered rectangle with size hints.
#[must_use]
pub fn centered_rect_with_hint(hint: SizeHint, r: Rect) -> Rect {
    let percent_width = r.width * hint.percent_x / 100;
    let percent_height = r.height * hint.percent_y / 100;

    let width = hint
        .min_width
        .map_or(percent_width, |min| percent_width.max(min))
        .min(r.width);
    let height = hint
        .min_height
        .map_or(percent_height, |min| percent_height.max(min))
        .min(r.height);

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

// Re-export all public types
pub use action_select::ActionSelectPopup;
pub use confirm::{ConfirmPermissionsPopup, ConfirmQuitPopup};
pub use error::{ErrorPopup, SessionTerminatedPopup};
pub use issue_loading::IssueLoadingPopup;
// SizeHint and PopupSizing are exported at module level (no need for re-export)
pub use items::{IssueItem, SessionItem, WorktreeItem};
pub use state::{ActionSelectPopupState, LoadingOperation, PopupSection, WorkspacePopupState};
pub use workspace::WorkspacePopup;

/// Calculate centered rectangle
#[must_use]
pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
    let width = r.width * percent_x / 100;
    let height = r.height * percent_y / 100;
    let x = r.x + (r.width.saturating_sub(width)) / 2;
    let y = r.y + (r.height.saturating_sub(height)) / 2;
    Rect::new(x, y, width, height)
}

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

    #[test]
    fn test_centered_rect() {
        let outer = Rect::new(0, 0, 100, 50);
        let inner = centered_rect(50, 40, outer);

        assert_eq!(inner.width, 50);
        assert_eq!(inner.height, 20);
        assert_eq!(inner.x, 25);
        assert_eq!(inner.y, 15);
    }

    #[test]
    fn test_centered_rect_small() {
        let outer = Rect::new(10, 10, 20, 10);
        let inner = centered_rect(50, 50, outer);

        assert_eq!(inner.width, 10);
        assert_eq!(inner.height, 5);
        assert_eq!(inner.x, 15);
        assert_eq!(inner.y, 12);
    }

    #[test]
    fn test_size_hint_percent() {
        let hint = SizeHint::percent(50, 30);
        assert_eq!(hint.percent_x, 50);
        assert_eq!(hint.percent_y, 30);
        assert_eq!(hint.min_width, None);
        assert_eq!(hint.min_height, None);
    }

    #[test]
    fn test_size_hint_builder() {
        let hint = SizeHint::percent(50, 30)
            .with_min_width(40)
            .with_min_height(10);
        assert_eq!(hint.percent_x, 50);
        assert_eq!(hint.percent_y, 30);
        assert_eq!(hint.min_width, Some(40));
        assert_eq!(hint.min_height, Some(10));
    }

    #[test]
    fn test_centered_rect_with_hint_basic() {
        let outer = Rect::new(0, 0, 100, 50);
        let hint = SizeHint::percent(50, 40);
        let inner = centered_rect_with_hint(hint, outer);

        // Same behavior as centered_rect when no min constraints
        assert_eq!(inner.width, 50);
        assert_eq!(inner.height, 20);
        assert_eq!(inner.x, 25);
        assert_eq!(inner.y, 15);
    }

    #[test]
    fn test_centered_rect_with_hint_min_width_enforced() {
        let outer = Rect::new(0, 0, 100, 50);
        // 20% of 100 = 20, but min_width = 40
        let hint = SizeHint::percent(20, 40).with_min_width(40);
        let inner = centered_rect_with_hint(hint, outer);

        assert_eq!(inner.width, 40); // min enforced
        assert_eq!(inner.height, 20);
        assert_eq!(inner.x, 30); // centered with width 40
    }

    #[test]
    fn test_centered_rect_with_hint_min_height_enforced() {
        let outer = Rect::new(0, 0, 100, 50);
        // 10% of 50 = 5, but min_height = 15
        let hint = SizeHint::percent(50, 10).with_min_height(15);
        let inner = centered_rect_with_hint(hint, outer);

        assert_eq!(inner.width, 50);
        assert_eq!(inner.height, 15); // min enforced
        assert_eq!(inner.y, 17); // centered with height 15
    }

    #[test]
    fn test_centered_rect_with_hint_clamp_to_container() {
        let outer = Rect::new(0, 0, 30, 20);
        // min_width = 50 exceeds container width 30
        let hint = SizeHint::percent(50, 50)
            .with_min_width(50)
            .with_min_height(25);
        let inner = centered_rect_with_hint(hint, outer);

        assert_eq!(inner.width, 30); // clamped to container
        assert_eq!(inner.height, 20); // clamped to container
        assert_eq!(inner.x, 0);
        assert_eq!(inner.y, 0);
    }

    #[test]
    fn test_popup_sizing_trait() {
        struct TestPopup;
        impl PopupSizing for TestPopup {
            fn size_hint(&self) -> SizeHint {
                SizeHint::percent(60, 40).with_min_width(30)
            }
        }

        let popup = TestPopup;
        let outer = Rect::new(0, 0, 100, 50);
        let area = popup.compute_area(outer);

        assert_eq!(area.width, 60);
        assert_eq!(area.height, 20);
    }
}