use ratatui::layout::{Constraint, Direction, Layout, Position, Rect};
use crate::tui::state::Panel;
#[derive(Debug, Clone, Copy)]
pub struct AppLayout {
pub tables: Rect,
pub grid: Rect,
pub sql: Rect,
pub inspector: Rect,
pub status: Rect,
}
#[cfg(test)]
pub fn compute(area: Rect) -> AppLayout {
compute_with_sidebar(area, true)
}
#[cfg(test)]
pub fn compute_with_sidebar(area: Rect, sidebar_visible: bool) -> AppLayout {
compute_with_panels(area, sidebar_visible, false, true)
}
pub fn compute_with_panels(
area: Rect,
sidebar_visible: bool,
inspector_visible: bool,
editor_visible: bool,
) -> AppLayout {
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Length(1)])
.split(area);
let constraints = match (sidebar_visible, inspector_visible) {
(true, true) => [
Constraint::Percentage(24),
Constraint::Percentage(52),
Constraint::Percentage(24),
],
(true, false) => [
Constraint::Percentage(28),
Constraint::Percentage(72),
Constraint::Length(0),
],
(false, true) => [
Constraint::Length(0),
Constraint::Percentage(72),
Constraint::Percentage(28),
],
(false, false) => [
Constraint::Length(0),
Constraint::Percentage(100),
Constraint::Length(0),
],
};
let body = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.split(rows[0]);
let workspace_constraints = if editor_visible {
[Constraint::Percentage(40), Constraint::Percentage(60)]
} else {
[Constraint::Length(0), Constraint::Percentage(100)]
};
let workspace = Layout::default()
.direction(Direction::Vertical)
.constraints(workspace_constraints)
.split(body[1]);
AppLayout {
tables: body[0],
grid: workspace[1],
sql: workspace[0],
inspector: body[2],
status: rows[1],
}
}
pub fn hit_test(layout: &AppLayout, x: u16, y: u16) -> Option<Panel> {
let pos = Position::new(x, y);
if layout.tables.contains(pos) {
Some(Panel::Tables)
} else if layout.grid.contains(pos) {
Some(Panel::Grid)
} else if layout.sql.contains(pos) {
Some(Panel::Sql)
} else if layout.inspector.contains(pos) {
Some(Panel::Inspector)
} else {
None
}
}
pub fn filter_rows_area(panel: Rect) -> Rect {
let inner = Rect::new(
panel.x.saturating_add(1),
panel.y.saturating_add(1),
panel.width.saturating_sub(2),
panel.height.saturating_sub(2),
);
Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).split(inner)[0]
}
pub fn filter_control_constraints(width: u16) -> [Constraint; 7] {
let flexible = width.saturating_sub(16);
let column = flexible.saturating_mul(30) / 100;
let operator = flexible.saturating_mul(35) / 100;
let value = flexible.saturating_sub(column).saturating_sub(operator);
[
Constraint::Length(3),
Constraint::Length(column),
Constraint::Length(operator),
Constraint::Length(value),
Constraint::Length(5),
Constraint::Length(1),
Constraint::Length(1),
]
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
fn layout() -> AppLayout {
compute(Rect::new(0, 0, 80, 24))
}
#[test]
fn splits_area_into_four_regions() {
let l = layout();
assert_eq!(l.tables.x, 0);
assert_eq!(l.grid.x, l.tables.width);
assert_eq!(l.sql.x, l.tables.width);
assert_eq!(l.grid.y, l.sql.bottom());
assert_eq!(l.tables.height, l.sql.height + l.grid.height);
assert_eq!(l.status.height, 1);
assert_eq!(l.status.y, 23);
}
#[test]
fn hit_test_maps_coordinates_to_panels() {
let l = layout();
assert_eq!(hit_test(&l, 5, 5), Some(Panel::Tables));
assert_eq!(hit_test(&l, 60, 5), Some(Panel::Sql));
assert_eq!(hit_test(&l, 40, 21), Some(Panel::Grid));
assert_eq!(hit_test(&l, 40, 23), None);
}
#[test]
fn filter_controls_fit_the_available_width() {
for width in [30, 42, 80] {
let constraints = filter_control_constraints(width);
let controls = Layout::horizontal(constraints)
.spacing(1)
.split(Rect::new(0, 0, width, 1));
assert_eq!(controls[0].x, 0);
assert_eq!(controls[6].right(), width);
}
}
#[test]
fn collapsed_sidebar_gives_workspace_the_full_width() {
let l = compute_with_sidebar(Rect::new(0, 0, 80, 24), false);
assert_eq!(l.tables.width, 0);
assert_eq!(l.sql.x, 0);
assert_eq!(l.grid.x, 0);
assert_eq!(l.sql.width, 80);
}
#[test]
fn record_inspector_uses_the_right_side() {
let l = compute_with_panels(Rect::new(0, 0, 100, 30), true, true, true);
assert_eq!(l.tables.x, 0);
assert_eq!(l.inspector.right(), 100);
assert!(l.inspector.width > 0);
assert_eq!(l.grid.right(), l.inspector.x);
}
#[test]
fn hidden_editor_gives_data_the_full_workspace_height() {
let l = compute_with_panels(Rect::new(0, 0, 100, 30), true, true, false);
assert_eq!(l.sql.height, 0);
assert_eq!(l.grid.y, 0);
assert_eq!(l.grid.height, 29);
}
}