use ratatui::layout::{Constraint, Margin, Rect};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverlayAnchor {
Center,
TopLeft,
TopCenter,
TopRight,
LeftCenter,
RightCenter,
BottomLeft,
BottomCenter,
BottomRight,
}
#[derive(Debug, Clone, Copy)]
pub enum SizeValue {
Fixed(u16),
Percent(f32),
}
#[derive(Debug, Clone)]
pub struct OverlayLayout {
pub anchor: OverlayAnchor,
pub width: SizeValue,
pub min_width: Option<u16>,
pub max_height: Option<u16>,
pub offset_x: i16,
pub offset_y: i16,
pub margin: u16,
}
impl Default for OverlayLayout {
fn default() -> Self {
OverlayLayout {
anchor: OverlayAnchor::Center,
width: SizeValue::Percent(0.6),
min_width: Some(30),
max_height: Some(20),
offset_x: 0,
offset_y: 0,
margin: 1,
}
}
}
pub fn resolve_overlay_layout(
layout: &OverlayLayout,
term_w: u16,
term_h: u16,
) -> ratatui::layout::Rect {
let terminal = Rect::new(0, 0, term_w, term_h);
let area = terminal.inner(Margin::new(layout.margin, layout.margin));
let width = match layout.width {
SizeValue::Fixed(w) => w.min(area.width),
SizeValue::Percent(pct) => ((area.width as f32 * pct).ceil() as u16).min(area.width),
};
let width = layout
.min_width
.map_or(width, |min| width.max(min))
.min(area.width);
let height = layout
.max_height
.map_or(area.height / 2, |max| max.min(area.height));
let overlay = match layout.anchor {
OverlayAnchor::Center => {
area.centered(Constraint::Length(width), Constraint::Length(height))
}
OverlayAnchor::TopLeft => Rect::new(area.x, area.y, width, height),
OverlayAnchor::TopCenter => {
let base = area.centered_horizontally(Constraint::Length(width));
Rect::new(base.x, area.y, width, height)
}
OverlayAnchor::TopRight => {
Rect::new(area.right().saturating_sub(width), area.y, width, height)
}
OverlayAnchor::LeftCenter => {
let base = area.centered_vertically(Constraint::Length(height));
Rect::new(area.x, base.y, width, height)
}
OverlayAnchor::RightCenter => {
let base = area.centered_vertically(Constraint::Length(height));
Rect::new(area.right().saturating_sub(width), base.y, width, height)
}
OverlayAnchor::BottomLeft => {
Rect::new(area.x, area.bottom().saturating_sub(height), width, height)
}
OverlayAnchor::BottomCenter => {
let base = area.centered_horizontally(Constraint::Length(width));
Rect::new(base.x, area.bottom().saturating_sub(height), width, height)
}
OverlayAnchor::BottomRight => Rect::new(
area.right().saturating_sub(width),
area.bottom().saturating_sub(height),
width,
height,
),
};
let x = ((overlay.x as i16) + layout.offset_x)
.max(0)
.min(term_w.saturating_sub(width) as i16) as u16;
let y = ((overlay.y as i16) + layout.offset_y)
.max(0)
.min(term_h.saturating_sub(height) as i16) as u16;
Rect {
x,
y,
width,
height,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_center_layout() {
let layout = OverlayLayout {
anchor: OverlayAnchor::Center,
width: SizeValue::Fixed(40),
max_height: Some(10),
..Default::default()
};
let rect = resolve_overlay_layout(&layout, 80, 24);
assert_eq!(rect.width, 40);
assert_eq!(rect.height, 10);
assert!(rect.x >= 10);
assert!(rect.y >= 2);
}
#[test]
fn test_top_left_layout() {
let layout = OverlayLayout {
anchor: OverlayAnchor::TopLeft,
width: SizeValue::Fixed(30),
max_height: Some(5),
..Default::default()
};
let rect = resolve_overlay_layout(&layout, 80, 24);
assert_eq!(rect.x, 1); assert_eq!(rect.y, 1); assert_eq!(rect.width, 30);
assert_eq!(rect.height, 5);
}
#[test]
fn test_bottom_right_layout() {
let layout = OverlayLayout {
anchor: OverlayAnchor::BottomRight,
width: SizeValue::Fixed(20),
min_width: None,
max_height: Some(5),
..Default::default()
};
let rect = resolve_overlay_layout(&layout, 80, 24);
assert_eq!(rect.width, 20);
assert_eq!(rect.height, 5);
assert!(rect.x >= 50);
assert!(rect.y >= 15);
}
#[test]
fn test_percent_width() {
let layout = OverlayLayout {
anchor: OverlayAnchor::Center,
width: SizeValue::Percent(0.5),
max_height: Some(10),
..Default::default()
};
let rect = resolve_overlay_layout(&layout, 80, 24);
assert!(rect.width >= 38 && rect.width <= 41);
}
#[test]
fn test_min_width_enforced() {
let layout = OverlayLayout {
anchor: OverlayAnchor::Center,
width: SizeValue::Fixed(10),
min_width: Some(30),
max_height: Some(5),
..Default::default()
};
let rect = resolve_overlay_layout(&layout, 80, 24);
assert_eq!(rect.width, 30); }
#[test]
fn test_offset_applied() {
let layout = OverlayLayout {
anchor: OverlayAnchor::TopLeft,
width: SizeValue::Fixed(20),
max_height: Some(5),
offset_x: 5,
offset_y: 3,
..Default::default()
};
let rect = resolve_overlay_layout(&layout, 80, 24);
assert_eq!(rect.x, 6); assert_eq!(rect.y, 4); }
#[test]
fn test_small_terminal() {
let layout = OverlayLayout {
anchor: OverlayAnchor::Center,
width: SizeValue::Percent(0.8),
max_height: Some(10),
..Default::default()
};
let rect = resolve_overlay_layout(&layout, 20, 10);
assert!(rect.width <= 18); assert!(rect.height <= 8);
}
}