use ratatui::layout::{Constraint, Direction as RatatuiDirection, Layout, Rect};
use crate::SkimOptions;
use crate::tui::options::TuiLayout;
use crate::tui::statusline::InfoDisplay;
use crate::tui::{Direction, Size};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PreviewPlacement {
Left,
Right,
Up,
Down,
None,
}
#[derive(Debug, Clone)]
pub struct LayoutTemplate {
show_header: bool,
preview_placement: PreviewPlacement,
work_layout_reversed: bool,
preview_layout: Option<Layout>,
collapse_borders: bool,
work_layout: Layout,
}
impl LayoutTemplate {
#[must_use]
pub fn from_options(options: &SkimOptions, header_height: u16) -> Self {
let has_border = options.border.is_some();
let collapse_borders = has_border && !options.border_no_collapse;
let overlap = u16::from(collapse_borders);
let input_rows: u16 = if has_border {
3 } else {
1 + u16::from(matches!(
options.info.display,
InfoDisplay::Default | InfoDisplay::Left | InfoDisplay::Right
))
};
let show_header = options.header.is_some() || options.header_lines > 0;
let header_rows: u16 = if show_header {
if has_border { header_height + 2 } else { header_height }
} else {
0
};
let preview_visible = (options.preview.is_some() || options.preview_fn.is_some())
&& !options.preview_window.hidden
&& !matches!(options.preview_window.size, Size::Fixed(0));
let (preview_placement, preview_layout) = if preview_visible {
let (preview_c, rest_c) = size_to_constraint(options.preview_window.size);
let placement = match options.preview_window.direction {
Direction::Left => PreviewPlacement::Left,
Direction::Right => PreviewPlacement::Right,
Direction::Up => PreviewPlacement::Up,
Direction::Down => PreviewPlacement::Down,
};
let layout = match placement {
PreviewPlacement::Left => Layout::new(RatatuiDirection::Horizontal, [preview_c, rest_c]),
PreviewPlacement::Right => Layout::new(RatatuiDirection::Horizontal, [rest_c, preview_c]),
PreviewPlacement::Up => Layout::new(RatatuiDirection::Vertical, [preview_c, rest_c]),
PreviewPlacement::Down => Layout::new(RatatuiDirection::Vertical, [rest_c, preview_c]),
PreviewPlacement::None => unreachable!(),
};
(placement, Some(layout))
} else {
(PreviewPlacement::None, None)
};
let work_layout_reversed = options.layout == TuiLayout::Reverse;
let work_layout = if show_header {
match options.layout {
TuiLayout::Default | TuiLayout::ReverseList => Layout::vertical([
Constraint::Fill(1),
Constraint::Length(header_rows.saturating_sub(overlap)),
Constraint::Length(input_rows.saturating_sub(overlap)),
]),
TuiLayout::Reverse => Layout::vertical([
Constraint::Length(input_rows),
Constraint::Length(header_rows.saturating_sub(overlap)),
Constraint::Fill(1),
]),
}
} else {
match options.layout {
TuiLayout::Default | TuiLayout::ReverseList => Layout::vertical([
Constraint::Fill(1),
Constraint::Length(0),
Constraint::Length(input_rows.saturating_sub(overlap)),
]),
TuiLayout::Reverse => Layout::vertical([
Constraint::Length(input_rows),
Constraint::Length(0),
Constraint::Fill(1),
]),
}
};
Self {
show_header,
preview_placement,
work_layout_reversed,
preview_layout,
collapse_borders,
work_layout,
}
}
#[must_use]
pub fn apply(&self, area: Rect) -> AppLayout {
let (work_area, preview_area): (Rect, Option<Rect>) = match &self.preview_layout {
Some(layout) => {
let [a, mut b]: [Rect; 2] = layout.areas(area);
if self.collapse_borders {
b = match self.preview_placement {
PreviewPlacement::Left | PreviewPlacement::Right => extend_left(b, area.x),
PreviewPlacement::Up | PreviewPlacement::Down => extend_up(b, area.y),
PreviewPlacement::None => unreachable!(),
};
}
match self.preview_placement {
PreviewPlacement::Left | PreviewPlacement::Up => (b, Some(a)),
_ => (a, Some(b)),
}
}
None => (area, None),
};
let [slot0, slot1, slot2]: [Rect; 3] = self.work_layout.areas(work_area);
let (mut list_area, mut header_slot, mut input_area) = if self.work_layout_reversed {
(slot2, slot1, slot0)
} else {
(slot0, slot1, slot2)
};
if self.collapse_borders {
if self.work_layout_reversed {
if self.show_header {
header_slot = extend_up(header_slot, work_area.y);
}
list_area = extend_up(list_area, work_area.y);
} else {
if self.show_header {
header_slot = extend_up(header_slot, work_area.y);
}
input_area = extend_up(input_area, work_area.y);
}
}
let header_area = if self.show_header { Some(header_slot) } else { None };
AppLayout {
list_area,
input_area,
header_area,
preview_area,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AppLayout {
pub list_area: Rect,
pub input_area: Rect,
pub header_area: Option<Rect>,
pub preview_area: Option<Rect>,
}
impl AppLayout {
#[must_use]
pub fn compute(area: Rect, options: &SkimOptions, header_height: u16) -> Self {
LayoutTemplate::from_options(options, header_height).apply(area)
}
}
fn extend_up(mut rect: Rect, top: u16) -> Rect {
if rect.y > top {
rect.y -= 1;
rect.height = rect.height.saturating_add(1);
}
rect
}
fn extend_left(mut rect: Rect, left: u16) -> Rect {
if rect.x > left {
rect.x -= 1;
rect.width = rect.width.saturating_add(1);
}
rect
}
fn size_to_constraint(size: Size) -> (Constraint, Constraint) {
match size {
Size::Fixed(n) => (Constraint::Length(n), Constraint::Fill(1)),
Size::Percent(p) => (Constraint::Percentage(p), Constraint::Fill(1)),
Size::Neg(n) => (Constraint::Fill(1), Constraint::Length(n)),
}
}
#[cfg(test)]
#[path = "layout_tests.rs"]
mod tests;