ghr-cli 0.6.0

A fast terminal dashboard for GitHub pull requests, issues, and notifications.
use ratatui::layout::{Constraint, Direction, Layout, Rect};

use crate::state::{DEFAULT_LIST_WIDTH_PERCENT, clamp_list_width_percent};

use super::AppState;

const CENTERED_RECT_MIN_WIDTH: u16 = 48;
const CENTERED_RECT_MAX_WIDTH: u16 = 112;

#[cfg(test)]
pub(super) fn body_area(area: Rect) -> Rect {
    page_areas(area)[2]
}

pub(super) fn details_area_for(app: &AppState, area: Rect) -> Rect {
    let chunks = page_areas(area);
    if app.mouse_capture_enabled {
        body_areas_with_ratio(chunks[2], app.list_width_percent)[1]
    } else {
        chunks[2]
    }
}

pub(super) fn page_areas(area: Rect) -> std::rc::Rc<[Rect]> {
    Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(3),
            Constraint::Length(3),
            Constraint::Min(4),
            Constraint::Length(2),
        ])
        .split(area)
}

#[cfg(test)]
pub(super) fn body_areas(area: Rect) -> std::rc::Rc<[Rect]> {
    body_areas_with_ratio(area, DEFAULT_LIST_WIDTH_PERCENT)
}

pub(super) fn body_areas_with_ratio(area: Rect, list_width_percent: u16) -> std::rc::Rc<[Rect]> {
    let list_width_percent = clamp_list_width_percent(list_width_percent);
    Layout::default()
        .direction(Direction::Horizontal)
        .constraints([
            Constraint::Percentage(list_width_percent),
            Constraint::Percentage(100 - list_width_percent),
        ])
        .split(area)
}

pub(super) fn block_inner(area: Rect) -> Rect {
    Rect {
        x: area.x.saturating_add(1),
        y: area.y.saturating_add(1),
        width: area.width.saturating_sub(2),
        height: area.height.saturating_sub(2),
    }
}

pub(super) fn rect_contains(area: Rect, x: u16, y: u16) -> bool {
    x >= area.x
        && x < area.x.saturating_add(area.width)
        && y >= area.y
        && y < area.y.saturating_add(area.height)
}

pub(super) fn splitter_contains(body: Rect, list: Rect, details: Rect, x: u16, y: u16) -> bool {
    if !rect_contains(body, x, y) {
        return false;
    }

    let list_border = list.x.saturating_add(list.width).saturating_sub(1);
    x == list_border || x == details.x
}

pub(super) fn split_percent_from_column(body: Rect, column: u16) -> u16 {
    if body.width == 0 {
        return DEFAULT_LIST_WIDTH_PERCENT;
    }

    let left_width = column.saturating_sub(body.x).min(body.width);
    let percent = (u32::from(left_width) * 100 + u32::from(body.width) / 2) / u32::from(body.width);
    clamp_list_width_percent(percent as u16)
}

pub(super) fn centered_rect(width_percent: u16, height: u16, area: Rect) -> Rect {
    let width = centered_rect_width(width_percent, area);
    let height = height.min(area.height);
    centered_rect_with_size(width, height, area)
}

pub(super) fn centered_rect_width(width_percent: u16, area: Rect) -> u16 {
    let mut width = area.width.saturating_mul(width_percent).saturating_div(100);
    width = width
        .max(CENTERED_RECT_MIN_WIDTH.min(area.width))
        .min(CENTERED_RECT_MAX_WIDTH.min(area.width));
    width
}

pub(super) fn centered_rect_with_size(width: u16, height: u16, area: Rect) -> Rect {
    let x = area.x + area.width.saturating_sub(width) / 2;
    let y = area.y + area.height.saturating_sub(height) / 2;
    Rect::new(x, y, width, height)
}

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

    #[test]
    fn centered_rect_width_caps_wide_terminals() {
        let area = Rect::new(0, 0, 240, 40);

        assert_eq!(centered_rect_width(78, area), CENTERED_RECT_MAX_WIDTH);
        assert_eq!(centered_rect(78, 14, area), Rect::new(64, 13, 112, 14));
    }

    #[test]
    fn centered_rect_width_keeps_percentage_before_cap() {
        let area = Rect::new(0, 0, 120, 40);

        assert_eq!(centered_rect_width(76, area), 91);
    }
}