debtmap 0.16.3

Code complexity and technical debt analyzer
Documentation
//! Detail view rendering for selected debt item.

use super::detail_pages;
use super::{app::ResultsApp, detail_page::DetailPage};
use crate::tui::theme::Theme;
use ratatui::{
    layout::{Constraint, Direction, Layout, Rect},
    style::Style,
    text::{Line, Span},
    widgets::{Block, Borders, Paragraph},
    Frame,
};

/// Render detail view for selected item
pub fn render(frame: &mut Frame, app: &ResultsApp) {
    let theme = Theme::default();

    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(3), // Header with page indicator
            Constraint::Min(0),    // Content
            Constraint::Length(2), // Footer
        ])
        .split(frame.area());

    // Render header with page indicator
    render_header(frame, app, chunks[0], &theme);

    // Apply margins to content area per DESIGN.md:
    // - 1-character margin on layout edges (horizontal)
    // - 1 line margin above/below content (vertical)
    let content_area = apply_content_margins(chunks[1]);

    // Route to appropriate page renderer
    if let Some(item) = app.selected_item() {
        match app.nav().detail_page {
            DetailPage::Overview => {
                detail_pages::overview::render(frame, app, item, content_area, &theme)
            }
            DetailPage::ScoreBreakdown => {
                detail_pages::score_breakdown::render(frame, app, item, content_area, &theme)
            }
            DetailPage::Context => {
                detail_pages::context::render(frame, app, item, content_area, &theme)
            }
            DetailPage::Dependencies => {
                detail_pages::dependencies::render(frame, app, item, content_area, &theme)
            }
            DetailPage::GitContext => {
                detail_pages::git_context::render(frame, app, item, content_area, &theme)
            }
            DetailPage::Patterns => {
                detail_pages::patterns::render(frame, app, item, content_area, &theme)
            }
            DetailPage::DataFlow => detail_pages::data_flow::render(
                frame,
                app,
                item,
                &app.analysis().data_flow_graph,
                content_area,
                &theme,
            ),
            DetailPage::Responsibilities => {
                detail_pages::responsibilities::render(frame, app, item, content_area, &theme)
            }
        }
    } else {
        let empty = Paragraph::new("No item selected").style(Style::default().fg(theme.muted));
        frame.render_widget(empty, content_area);
    }

    // Render footer
    render_footer(frame, app, chunks[2], &theme);
}

/// Horizontal margin constant per DESIGN.md spacing rules.
const HORIZONTAL_MARGIN: u16 = 1;

/// Apply content margins per DESIGN.md spacing rules.
///
/// Creates breathing room around content with:
/// - 1 character horizontal margin on each side
/// - 1 line vertical margin top and bottom
fn apply_content_margins(area: Rect) -> Rect {
    const VERTICAL_MARGIN: u16 = 1;

    Rect {
        x: area.x.saturating_add(HORIZONTAL_MARGIN),
        y: area.y.saturating_add(VERTICAL_MARGIN),
        width: area.width.saturating_sub(HORIZONTAL_MARGIN * 2),
        height: area.height.saturating_sub(VERTICAL_MARGIN * 2),
    }
}

/// Apply horizontal margin only (for header/footer areas).
///
/// Creates consistent horizontal padding without vertical margin.
fn apply_horizontal_margin(area: Rect) -> Rect {
    Rect {
        x: area.x.saturating_add(HORIZONTAL_MARGIN),
        y: area.y,
        width: area.width.saturating_sub(HORIZONTAL_MARGIN * 2),
        height: area.height,
    }
}

/// Render header with page indicator
fn render_header(frame: &mut Frame, app: &ResultsApp, area: Rect, theme: &Theme) {
    use super::page_availability;

    let available =
        page_availability::available_pages(app.selected_item(), &app.analysis().data_flow_graph);
    let current_page = available
        .iter()
        .position(|&p| p == app.nav().detail_page)
        .map(|i| i + 1)
        .unwrap_or(1); // 1-based for display
    let total_pages = available.len();
    let page_name = app.nav().detail_page.name();

    let position = format!(
        "Detail View ({}/{})  [Page {}/{}] {}",
        app.list().selected_index() + 1,
        app.item_count(),
        current_page,
        total_pages,
        page_name
    );

    // Apply horizontal margin per DESIGN.md
    let header_area = apply_horizontal_margin(area);

    let header = Paragraph::new(vec![Line::from(vec![Span::styled(
        position,
        Style::default().fg(theme.accent()),
    )])])
    .block(Block::default().borders(Borders::BOTTOM));

    frame.render_widget(header, header_area);
}

/// Render footer with condensed action hints and status message.
///
/// Uses a minimal footer with essential hints only.
/// Full keybindings are available via `?` help overlay.
fn render_footer(frame: &mut Frame, app: &ResultsApp, area: Rect, theme: &Theme) {
    // Condensed shortcuts - essential actions only, `?` for full help
    let shortcuts = Line::from(vec![
        Span::styled("q/Esc", Style::default().fg(theme.accent())),
        Span::raw(":Back  "),
        Span::styled("←/→", Style::default().fg(theme.accent())),
        Span::raw(":Pages  "),
        Span::styled("j/k", Style::default().fg(theme.accent())),
        Span::raw(":Items  "),
        Span::styled("^D/^U", Style::default().fg(theme.accent())),
        Span::raw(":Scroll  "),
        Span::styled("?", Style::default().fg(theme.accent())),
        Span::raw(":Help"),
    ]);

    // If there's a status message, show it on first line, shortcuts on second
    let lines = if let Some(status) = app.status_message() {
        let status_color = if status.starts_with('') {
            theme.success()
        } else {
            theme.warning()
        };

        vec![
            Line::from(vec![Span::styled(
                status,
                Style::default().fg(status_color),
            )]),
            shortcuts,
        ]
    } else {
        vec![shortcuts]
    };

    // Apply horizontal margin per DESIGN.md
    let footer_area = apply_horizontal_margin(area);

    let footer = Paragraph::new(lines).block(Block::default().borders(Borders::TOP));

    frame.render_widget(footer, footer_area);
}