quinjet 0.0.52

A fast, live, keyboard-first Git source-control interface for the terminal
#[cfg_attr(not(test), expect(clippy::wildcard_imports, reason = "shared"))]
use super::*;

#[expect(
    clippy::integer_division,
    reason = "tab widths use whole terminal cells"
)]
#[expect(
    clippy::too_many_lines,
    reason = "tab layout and hit targets share one coordinate pass"
)]
pub(super) fn draw_repository_tabs(
    frame: &mut Frame<'_>,
    area: Rect,
    app: &App,
    theme: &Theme,
) -> (Vec<RepositoryTabHit>, Rect, Rect, Rect) {
    frame.render_widget(
        Block::default().style(Style::default().bg(theme.panel)),
        area,
    );
    let tab_row = Rect::new(area.x, area.y, area.width, 1_u16.min(area.height));
    let separator = Rect::new(
        area.x,
        tab_row.bottom(),
        area.width,
        area.bottom().saturating_sub(tab_row.bottom()).min(1),
    );
    frame.render_widget(
        Paragraph::new("".repeat(usize::from(separator.width)))
            .style(Style::default().fg(theme.border).bg(theme.panel)),
        separator,
    );
    let open_width = 5_u16.min(tab_row.width);
    let open = Rect::new(
        tab_row.right().saturating_sub(open_width),
        tab_row.y,
        open_width,
        tab_row.height,
    );
    frame.render_widget(
        Paragraph::new(" + ").alignment(Alignment::Center).style(
            Style::default()
                .fg(theme.accent)
                .bg(theme.panel)
                .add_modifier(Modifier::BOLD),
        ),
        open,
    );
    let tab_region_width = tab_row.width.saturating_sub(open_width);
    if app.repository_tabs.is_empty() || tab_region_width == 0 {
        return (Vec::new(), open, Rect::default(), Rect::default());
    }
    let minimum_width = 12_u16.min(tab_region_width.max(1));
    let base_capacity = usize::from((tab_region_width / minimum_width).max(1));
    let overflow = app.repository_tabs.len() > base_capacity;
    let control_width = if overflow { 3 } else { 0 };
    let (previous, next) = if overflow {
        (
            Rect::new(tab_row.x, tab_row.y, control_width, tab_row.height),
            Rect::new(
                open.x.saturating_sub(control_width),
                tab_row.y,
                control_width,
                tab_row.height,
            ),
        )
    } else {
        (Rect::default(), Rect::default())
    };
    if overflow {
        let control_style = Style::default()
            .fg(theme.accent)
            .bg(theme.panel)
            .add_modifier(Modifier::BOLD);
        frame.render_widget(
            Paragraph::new("")
                .alignment(Alignment::Center)
                .style(control_style),
            previous,
        );
        frame.render_widget(
            Paragraph::new("")
                .alignment(Alignment::Center)
                .style(control_style),
            next,
        );
    }
    let available = tab_region_width.saturating_sub(control_width.saturating_mul(2));
    let capacity = usize::from((available / minimum_width).max(1));
    let active = app
        .repository_tabs
        .iter()
        .position(|tab| tab.active)
        .unwrap_or_default();
    let start = active.saturating_add(1).saturating_sub(capacity);
    let visible_count = app
        .repository_tabs
        .len()
        .saturating_sub(start)
        .min(capacity);
    let tab_width = (available / u16::try_from(visible_count).unwrap_or(1)).min(24);
    let first_machine = app
        .repository_tabs
        .iter()
        .find_map(|tab| tab.machine.as_deref());
    let mixed_machines = first_machine.is_some_and(|first| {
        app.repository_tabs
            .iter()
            .filter_map(|tab| tab.machine.as_deref())
            .any(|machine| machine != first)
    });
    let mut hits = Vec::with_capacity(visible_count);
    for (offset, tab) in app
        .repository_tabs
        .iter()
        .skip(start)
        .take(visible_count)
        .enumerate()
    {
        let x = tab_row
            .x
            .saturating_add(control_width)
            .saturating_add(cells(offset.saturating_mul(usize::from(tab_width))));
        let tab_area = Rect::new(x, tab_row.y, tab_width, tab_row.height);
        let mut value = if tab.title.is_empty() {
            tab.root.display().to_string()
        } else {
            tab.title.clone()
        };
        if mixed_machines && let Some(machine) = tab.machine.as_deref() {
            value.push_str(" @");
            value.push_str(machine.strip_suffix(".local").unwrap_or(machine));
        }
        let close_width = if app.exit_locked() { 0 } else { 4 };
        let label_width = tab_width.saturating_sub(close_width);
        let label = format!(
            " {}",
            truncate_end(&value, usize::from(label_width.saturating_sub(1)))
        );
        let style = if tab.active {
            Style::default()
                .fg(theme.text)
                .bg(theme.accent_soft)
                .add_modifier(Modifier::BOLD)
        } else {
            Style::default().fg(theme.muted).bg(theme.panel_alt)
        };
        let label_area = Rect::new(tab_area.x, tab_area.y, label_width, tab_area.height);
        let close = if app.exit_locked() {
            Rect::default()
        } else {
            Rect::new(
                tab_area.right().saturating_sub(4),
                tab_area.y,
                3_u16.min(tab_area.width),
                tab_area.height,
            )
        };
        frame.render_widget(
            Paragraph::new(label)
                .alignment(Alignment::Left)
                .style(style),
            label_area,
        );
        if !app.exit_locked() {
            frame.render_widget(
                Paragraph::new("×")
                    .alignment(Alignment::Center)
                    .style(style),
                close,
            );
        }
        frame.render_widget(
            Paragraph::new("").style(Style::default().fg(theme.border).bg(if tab.active {
                theme.accent_soft
            } else {
                theme.panel_alt
            })),
            Rect::new(
                tab_area.right().saturating_sub(1),
                tab_area.y,
                1,
                tab_area.height,
            ),
        );
        hits.push(RepositoryTabHit {
            area: tab_area,
            close,
            id: tab.id,
        });
    }
    draw_repository_tab_drop_marker(frame, tab_row, app, &hits, theme);
    (hits, open, previous, next)
}

fn draw_repository_tab_drop_marker(
    frame: &mut Frame<'_>,
    area: Rect,
    app: &App,
    hits: &[RepositoryTabHit],
    theme: &Theme,
) {
    let Some(drag) = app.repository_tab_drag else {
        return;
    };
    let Some(target) = drag.target else {
        return;
    };
    let Some(source_index) = app.repository_tabs.iter().position(|tab| tab.id == drag.id) else {
        return;
    };
    let Some(target_index) = app.repository_tabs.iter().position(|tab| tab.id == target) else {
        return;
    };
    let Some(target_hit) = hits.iter().find(|hit| hit.id == target) else {
        return;
    };
    let column = if source_index < target_index {
        target_hit.area.right().min(area.right().saturating_sub(1))
    } else {
        target_hit.area.x
    };
    frame.render_widget(
        Paragraph::new("").style(Style::default().fg(Color::White).bg(theme.panel_alt)),
        Rect::new(column, area.y, 1, area.height),
    );
}

pub(super) fn draw_repository_tab_menu(frame: &mut Frame<'_>, app: &mut App, theme: &Theme) {
    app.geometry.repository_tab_menu_hits.clear();
    if app.exit_locked() {
        return;
    }
    let Some(menu) = app.repository_tab_menu else {
        return;
    };
    let outer = frame.area();
    let width = 22_u16.min(outer.width);
    let height = 6_u16.min(outer.height);
    let x = menu
        .column
        .min(outer.right().saturating_sub(width).max(outer.x));
    let y = menu
        .row
        .saturating_add(1)
        .min(outer.bottom().saturating_sub(height).max(outer.y));
    let area = Rect::new(x, y, width, height);
    frame.render_widget(Clear, area);
    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::default().fg(theme.border_focus))
        .style(Style::default().bg(theme.panel));
    let inner = block.inner(area);
    frame.render_widget(block, area);
    for (index, action) in RepositoryTabAction::ALL.iter().copied().enumerate() {
        let row = Rect::new(
            inner.x,
            inner.y.saturating_add(cells(index)),
            inner.width,
            1,
        );
        let style = if index == menu.selected {
            Style::default().fg(theme.text).bg(theme.selected)
        } else {
            Style::default().fg(theme.text).bg(theme.panel)
        };
        frame.render_widget(
            Paragraph::new(format!(" {}", action.label())).style(style),
            row,
        );
        app.geometry.repository_tab_menu_hits.push((row, action));
    }
}