use super::state::{MissionControlState, ModalKind, RightColumnTab};
use ratatui::layout::{Constraint, Direction, Layout, Rect};
pub(crate) const RIGHT_COLUMN_RAIL_WIDTH: u16 = 5;
const TRANSCRIPT_RAIL_LABEL: &str = "Transcript";
pub(crate) const RIGHT_COLUMN_TAB_VERTICAL_PADDING: u16 = 1;
const RIGHT_COLUMN_RAIL_SEPARATOR_ROWS: u16 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TuiPane {
Transcript,
ActivityTree,
Detail,
Summary,
RightColumnRail,
Prompt,
Help,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct TuiLayout {
pub(super) header: Rect,
pub(super) transcript: Rect,
pub(super) activity_tree: Option<Rect>,
pub(super) detail: Option<Rect>,
pub(super) summary: Option<Rect>,
pub(super) right_column_rail: Option<Rect>,
pub(super) prompt: Rect,
pub(super) help: Option<Rect>,
}
impl TuiLayout {
pub(crate) fn pane_at(&self, column: u16, row: u16) -> Option<TuiPane> {
if self.help.is_some_and(|area| contains(area, column, row)) {
return Some(TuiPane::Help);
}
if self.summary.is_some_and(|area| contains(area, column, row)) {
return Some(TuiPane::Summary);
}
[
(TuiPane::ActivityTree, self.activity_tree),
(TuiPane::Detail, self.detail),
(TuiPane::Transcript, Some(self.transcript)),
(TuiPane::RightColumnRail, self.right_column_rail),
(TuiPane::Prompt, Some(self.prompt)),
]
.into_iter()
.find_map(|(pane, area)| {
area.filter(|area| contains(*area, column, row))
.map(|_| pane)
})
}
pub(crate) fn pane_area(&self, pane: TuiPane) -> Option<Rect> {
match pane {
TuiPane::Transcript => Some(self.transcript),
TuiPane::ActivityTree => self.activity_tree,
TuiPane::Detail => self.detail,
TuiPane::Summary => self.summary,
TuiPane::RightColumnRail => self.right_column_rail,
TuiPane::Prompt => Some(self.prompt),
TuiPane::Help => self.help,
}
}
pub(crate) fn header_modal_area(&self, modal: ModalKind) -> Option<Rect> {
match modal {
ModalKind::Summary => self.summary,
ModalKind::ActivityTree => self.activity_tree,
ModalKind::SessionFiles => self.detail,
_ => None,
}
}
pub(crate) fn header_modal_at(&self, column: u16, row: u16) -> Option<ModalKind> {
[
ModalKind::Summary,
ModalKind::ActivityTree,
ModalKind::SessionFiles,
]
.into_iter()
.find(|&modal| {
self.header_modal_area(modal)
.is_some_and(|area| contains(area, column, row))
})
}
pub(crate) fn right_column_tab_at(&self, column: u16, row: u16) -> Option<RightColumnTab> {
[RightColumnTab::Activity, RightColumnTab::Diff]
.into_iter()
.find(|&tab| {
self.right_column_tab_area(tab)
.is_some_and(|area| contains(area, column, row))
})
}
pub(crate) fn right_column_tab_area(&self, tab: RightColumnTab) -> Option<Rect> {
let rail = self.right_column_rail?;
if rail.height < right_column_rail_height() {
let index = match tab {
RightColumnTab::Activity => 0,
RightColumnTab::Diff => 1,
};
return (index < rail.height).then(|| Rect::new(rail.x, rail.y + index, rail.width, 1));
}
let y = match tab {
RightColumnTab::Activity => rail.y,
RightColumnTab::Diff => rail
.y
.saturating_add(right_column_tab_height(RightColumnTab::Activity))
.saturating_add(RIGHT_COLUMN_RAIL_SEPARATOR_ROWS),
};
Some(Rect::new(
rail.x,
y,
rail.width,
right_column_tab_height(tab),
))
}
}
pub(crate) fn right_column_tab_label(tab: RightColumnTab) -> &'static str {
match tab {
RightColumnTab::Activity => TRANSCRIPT_RAIL_LABEL,
RightColumnTab::Diff => "Changes",
}
}
fn right_column_tab_height(tab: RightColumnTab) -> u16 {
(right_column_tab_label(tab).chars().count() as u16)
.saturating_add(RIGHT_COLUMN_TAB_VERTICAL_PADDING.saturating_mul(2))
}
fn right_column_rail_height() -> u16 {
right_column_tab_height(RightColumnTab::Activity)
.saturating_add(RIGHT_COLUMN_RAIL_SEPARATOR_ROWS)
.saturating_add(right_column_tab_height(RightColumnTab::Diff))
}
fn right_column_rail_usable(area: Rect) -> bool {
area.width >= RIGHT_COLUMN_RAIL_WIDTH + 20 && area.height >= 3
}
pub(crate) fn layout_for(area: Rect, state: &MissionControlState) -> TuiLayout {
let prompt_wrap_width = area.width.saturating_sub(5).max(1);
let prompt_content_rows = state.prompt_visible_rows(prompt_wrap_width);
let prompt_height = prompt_content_rows.saturating_add(2);
let root = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(3), Constraint::Length(prompt_height)])
.split(area);
let [header, body_area] =
Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).areas(root[0]);
let right_column_rail = (!state.transcript_only_layout && right_column_rail_usable(body_area))
.then(|| {
Rect::new(
body_area.right().saturating_sub(RIGHT_COLUMN_RAIL_WIDTH),
body_area.y,
RIGHT_COLUMN_RAIL_WIDTH,
body_area.height,
)
});
let mut transcript = Rect {
width: body_area
.width
.saturating_sub(right_column_rail.map_or(0, |rail| rail.width)),
..body_area
};
let pinned = state.modals.header_modals_pinned && state.header_modals_allowed_on_tab();
let mut modal_width = (transcript.width / 3).max(27).min(transcript.width);
if pinned {
modal_width = modal_width.min(transcript.width / 2);
}
let modal = Rect::new(
transcript.right().saturating_sub(modal_width),
transcript.y,
modal_width,
transcript.height,
);
if pinned {
transcript.width = transcript.width.saturating_sub(modal_width);
}
let mut layout = TuiLayout {
header,
transcript,
activity_tree: None,
detail: None,
summary: None,
right_column_rail,
prompt: root[1],
help: state
.show_help
.then_some(super::modal_container::modal_area(area, 60, 60)),
};
if state.header_modals_allowed_on_tab() && !state.modals.header_modals.is_empty() {
let count = state.modals.header_modals.len() as u16;
let mut y = modal.y;
for (index, kind) in state.modals.header_modals.iter().enumerate() {
let height = modal.height / count + u16::from((index as u16) < modal.height % count);
let area = non_empty(Rect::new(modal.x, y, modal.width, height));
match kind {
ModalKind::Summary => layout.summary = area,
ModalKind::ActivityTree => layout.activity_tree = area,
ModalKind::SessionFiles => layout.detail = area,
_ => {}
}
y = y.saturating_add(height);
}
}
layout
}
fn non_empty(area: Rect) -> Option<Rect> {
(area.width > 0 && area.height > 0).then_some(area)
}
fn contains(area: Rect, column: u16, row: u16) -> bool {
column >= area.x
&& column < area.x.saturating_add(area.width)
&& row >= area.y
&& row < area.y.saturating_add(area.height)
}