use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::widgets::{Block, Padding};
use super::config::LayoutConfig;
pub const HERO_BOX_MIN_WIDTH: u16 = 90;
pub const PROMPT_HEIGHT: u16 = 3;
const VERSION_GAP: u16 = 1;
const H_MARGIN: u16 = 2;
const H_MARGIN_COMPACT: u16 = 1;
fn logo_height_default() -> u16 {
7
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WelcomePromptFocus {
#[default]
Prompt,
SessionPicker,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct WelcomeLayout {
pub logo: Rect,
pub error: Rect,
pub menu: Rect,
pub changelog: Rect,
pub tip: Rect,
pub prompt: Rect,
pub version: Rect,
pub hero_box: Rect,
pub hero_logo: Rect,
pub hero_version: Rect,
pub hero_subtitle: Rect,
pub hero_info: Rect,
pub hero_menu: Rect,
}
impl WelcomeLayout {
#[must_use]
pub fn has_hero_box(&self) -> bool {
self.hero_box.width > 0 && self.hero_box.height > 0
}
#[must_use]
pub fn fixed_below(tip_height: u16) -> u16 {
let tip_gap = u16::from(tip_height > 0);
tip_height + tip_gap + PROMPT_HEIGHT + VERSION_GAP + 1
}
#[must_use]
pub fn min_hero_box_height(error_height: u16, menu_height: u16, tip_height: u16) -> u16 {
let inner = menu_height.max(logo_height_default())
+ error_height
+ tip_height
+ Self::fixed_below(tip_height);
inner + 2
}
#[must_use]
pub fn compute(
content_area: Rect,
logo_height: u16,
error_height: u16,
menu_height: u16,
tip_height: u16,
changelog_height: u16,
compact: bool,
) -> Self {
let use_hero_box = !compact
&& content_area.width >= HERO_BOX_MIN_WIDTH
&& menu_height > 0
&& content_area.height
>= Self::min_hero_box_height(error_height, menu_height, tip_height);
if use_hero_box {
return Self::compute_hero_box(content_area, error_height, menu_height, tip_height);
}
Self::compute_stacked(
content_area,
logo_height,
error_height,
menu_height,
tip_height,
changelog_height,
compact,
)
}
fn compute_stacked(
content_area: Rect,
logo_height: u16,
error_height: u16,
menu_height: u16,
tip_height: u16,
changelog_height: u16,
compact: bool,
) -> Self {
let zero = Rect::default();
let logo_rows = if compact { 0 } else { logo_height };
let gap_after_logo = u16::from(error_height > 0);
let tip_gap = u16::from(tip_height > 0);
let fixed_below = Self::fixed_below(tip_height);
let fixed_above = logo_rows + 1 + gap_after_logo + error_height;
let (eff_cl_h, eff_cl_gap) = if !compact && changelog_height > 0 {
let cg = 1u16;
let needed = fixed_above + menu_height + cg + changelog_height + 1 + fixed_below;
if content_area.height >= needed {
(changelog_height, cg)
} else {
(0, 0)
}
} else {
(0, 0)
};
let top_pad = if compact {
0
} else {
let rem = content_area.height.saturating_sub(fixed_above);
rem.saturating_sub(menu_height)
.saturating_sub(eff_cl_gap + eff_cl_h)
.saturating_sub(fixed_below)
/ 2
};
let [
_,
logo,
_,
_,
error,
menu,
_,
changelog,
_,
tip,
_,
prompt,
_,
version,
] = Layout::vertical([
Constraint::Length(top_pad),
Constraint::Length(logo_rows),
Constraint::Length(1),
Constraint::Length(gap_after_logo),
Constraint::Length(error_height),
Constraint::Length(menu_height),
Constraint::Length(eff_cl_gap),
Constraint::Length(eff_cl_h),
Constraint::Min(1),
Constraint::Length(tip_height),
Constraint::Length(tip_gap),
Constraint::Length(PROMPT_HEIGHT),
Constraint::Length(VERSION_GAP),
Constraint::Length(1),
])
.areas(content_area);
Self {
logo,
error,
menu,
changelog,
tip,
prompt,
version,
hero_box: zero,
hero_logo: zero,
hero_version: zero,
hero_subtitle: zero,
hero_info: zero,
hero_menu: zero,
}
}
fn compute_hero_box(
content_area: Rect,
error_height: u16,
menu_height: u16,
tip_height: u16,
) -> Self {
let zero = Rect::default();
let hero_inner_h = menu_height.max(logo_height_default()) + error_height + tip_height;
let hero_h = hero_inner_h + 2;
let hero_w = content_area.width.min(HERO_BOX_MIN_WIDTH);
let total_needed = hero_h + 1 + PROMPT_HEIGHT + VERSION_GAP + 1;
let top_pad = content_area.height.saturating_sub(total_needed) / 2;
let hero_y = content_area.y + top_pad.min(content_area.height.saturating_sub(hero_h));
let hero_x = content_area.x + (content_area.width.saturating_sub(hero_w)) / 2;
let hero_box = Rect::new(hero_x, hero_y, hero_w, hero_h);
let hero_inner = Block::default()
.padding(Padding::symmetric(1, 0))
.inner(hero_box);
let left_w = (hero_inner.width / 3).max(20);
let [hero_logo, hero_menu] =
Layout::horizontal([Constraint::Length(left_w), Constraint::Min(1)]).areas(hero_inner);
let below_y = hero_box.y + hero_box.height;
let prompt = Rect::new(
content_area.x + H_MARGIN,
below_y + 1,
content_area.width.saturating_sub(H_MARGIN * 2),
PROMPT_HEIGHT,
);
let version = Rect::new(
content_area.x + H_MARGIN,
prompt.y + PROMPT_HEIGHT + VERSION_GAP,
content_area.width.saturating_sub(H_MARGIN * 2),
1,
);
Self {
logo: zero,
error: Rect::new(hero_inner.x, hero_logo.y, hero_inner.width, error_height),
menu: zero,
changelog: zero,
tip: Rect::new(
hero_inner.x,
hero_logo.y + hero_logo.height.saturating_sub(tip_height),
hero_inner.width,
tip_height,
),
prompt,
version,
hero_box,
hero_logo,
hero_version: zero,
hero_subtitle: zero,
hero_info: zero,
hero_menu,
}
}
#[must_use]
pub fn h_margin(compact: bool) -> u16 {
if compact { H_MARGIN_COMPACT } else { H_MARGIN }
}
}
#[must_use]
pub fn welcome_outer_padding(layout_cfg: &LayoutConfig, compact: bool) -> Padding {
let h = if compact { 1 } else { layout_cfg.hpad_left };
let v = if compact { 0 } else { layout_cfg.outer_vpad };
Padding::new(h, h, v, v)
}
#[cfg(test)]
mod tests {
use super::*;
fn screen(w: u16, h: u16) -> Rect {
Rect::new(0, 0, w, h)
}
#[test]
fn stacked_layout_basic() {
let l = WelcomeLayout::compute(screen(80, 30), 7, 0, 7, 0, 0, false);
assert!(l.logo.height > 0);
assert!(l.menu.height > 0);
assert_eq!(l.prompt.height, PROMPT_HEIGHT);
assert!(l.logo.y < l.menu.y);
assert!(!l.has_hero_box());
}
#[test]
fn hero_box_on_wide() {
let l = WelcomeLayout::compute(screen(120, 30), 7, 0, 7, 0, 0, false);
assert!(l.has_hero_box());
assert!(l.hero_logo.width > 0);
}
#[test]
fn compact_skips_logo() {
let l = WelcomeLayout::compute(screen(80, 20), 7, 0, 7, 0, 0, true);
assert_eq!(l.logo.height, 0);
}
#[test]
fn version_below_prompt() {
let l = WelcomeLayout::compute(screen(80, 30), 7, 0, 7, 0, 0, false);
assert!(l.version.y > l.prompt.y);
}
}