use retroglyph_core::grid::Rect;
use retroglyph_core::{Backend, Style, Terminal};
use retroglyph_widgets::{Panel, Scrollbar, Theme, Widget};
use crate::PrintableGame;
pub const GUTTER: u16 = 1;
pub struct Layout {
pub viewport: Rect,
pub stats: Rect,
pub actions: Rect,
pub log: Rect,
pub status: Rect,
}
impl Layout {
pub const fn new(full: Rect) -> Self {
let width = full.width();
let height = full.height();
let body_height = height.saturating_sub(1);
let status = Rect::new(0, body_height, width, 1);
let left_width = width * 7 / 10;
let right_x = left_width.saturating_add(GUTTER);
let right_width = width.saturating_sub(right_x);
let log_height = body_height / 4;
let top_height = body_height
.saturating_sub(log_height)
.saturating_sub(GUTTER);
let log_y = top_height.saturating_add(GUTTER);
let log_h = body_height.saturating_sub(log_y);
let stats_height = top_height / 2;
let actions_y = stats_height.saturating_add(GUTTER);
let actions_height = top_height.saturating_sub(actions_y);
Self {
viewport: Rect::new(0, 0, left_width, top_height),
stats: Rect::new(right_x, 0, right_width, stats_height),
actions: Rect::new(right_x, actions_y, right_width, actions_height),
log: Rect::new(0, log_y, width, log_h),
status,
}
}
}
pub fn draw_menu<B: Backend>(
term: &mut Terminal<B>,
rect: Rect,
top_offset: u16,
items: &[String],
selected: usize,
) {
let capacity = usize::from(rect.height().saturating_sub(top_offset));
if capacity == 0 || items.is_empty() {
return;
}
let selected = selected.min(items.len() - 1);
let start = menu_start(capacity, items.len(), selected);
let rows = items[start..(start + capacity).min(items.len())]
.iter()
.enumerate()
.map(|(offset, label)| {
let marker = if start + offset == selected { '>' } else { ' ' };
format!("{marker} {label}")
});
print_rows(term, rect, top_offset, rows);
}
#[must_use]
pub fn menu_start(capacity: usize, len: usize, selected: usize) -> usize {
if capacity == 0 || len == 0 {
return 0;
}
selected
.min(len - 1)
.saturating_sub(capacity - 1)
.min(len.saturating_sub(capacity))
}
pub fn print_rows<B: Backend>(
term: &mut Terminal<B>,
rect: Rect,
top_offset: u16,
lines: impl IntoIterator<Item = String>,
) {
let max_chars = usize::from(rect.width());
let mut y = rect.top().saturating_add(top_offset);
for line in lines {
if y >= rect.bottom() {
break;
}
let clipped: String = line.chars().take(max_chars).collect();
term.print(rect.left(), y, &clipped);
y = y.saturating_add(1);
}
}
pub fn panel<B: Backend>(term: &mut Terminal<B>, rect: Rect, theme: Theme, title: &str) -> Rect {
if rect.width() >= 2 && rect.height() >= 2 {
Panel::new()
.border_style(Style::new().fg(theme.border))
.render(rect, term);
if !title.is_empty() && rect.width() > 4 {
let max = usize::from(rect.width().saturating_sub(2));
let label: String = format!(" {title} ").chars().take(max).collect();
term.reset_style().fg(theme.accent);
term.print(rect.left().saturating_add(1), rect.top(), &label);
term.reset_style();
}
}
panel_inner(rect)
}
#[must_use]
pub const fn panel_inner(rect: Rect) -> Rect {
Rect::new(
rect.left().saturating_add(1),
rect.top().saturating_add(1),
rect.width().saturating_sub(2),
rect.height().saturating_sub(2),
)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LogGeometry {
pub text: Rect,
pub bar: Option<Rect>,
pub visible: usize,
}
#[must_use]
pub fn log_geometry(rect: Rect, total: usize) -> LogGeometry {
let inner = panel_inner(rect);
let visible = usize::from(inner.height());
if visible == 0 || inner.width() == 0 || total <= visible {
return LogGeometry {
text: inner,
bar: None,
visible,
};
}
let bar = Rect::new(
inner.right().saturating_sub(1),
inner.top(),
1,
inner.height(),
);
let text = Rect::new(
inner.left(),
inner.top(),
inner.width().saturating_sub(1),
inner.height(),
);
LogGeometry {
text,
bar: Some(bar),
visible,
}
}
pub fn actions_panel<B: Backend>(
term: &mut Terminal<B>,
rect: Rect,
theme: Theme,
position: Option<(usize, usize)>,
) -> Rect {
let title = match position {
Some((selected, total)) => format!("Actions {selected}/{total}"),
None => "Actions".to_owned(),
};
panel(term, rect, theme, &title)
}
pub fn draw_log<B: Backend>(
log: &[String],
term: &mut Terminal<B>,
rect: Rect,
theme: Theme,
offset: usize,
) -> usize {
let title = if offset > 0 { "Log (scrolled)" } else { "Log" };
panel(term, rect, theme, title);
let total = log.len();
let geometry = log_geometry(rect, total);
let visible = geometry.visible;
if visible == 0 || geometry.text.width() == 0 {
return visible;
}
let start = log_start(total, visible, offset);
if let Some(bar) = geometry.bar {
Scrollbar::new(total, visible)
.offset(start)
.theme(theme)
.render(bar, term);
}
let end = (start + visible).min(total);
print_rows(term, geometry.text, 0, log[start..end].iter().cloned());
visible
}
#[must_use]
pub const fn log_start(total: usize, visible: usize, offset: usize) -> usize {
let max_back = total.saturating_sub(visible);
max_back.saturating_sub(if offset < max_back { offset } else { max_back })
}
pub fn draw_board_stats_log<G, B>(
game: &G,
view: &G::View,
log: &[String],
term: &mut Terminal<B>,
layout: &Layout,
theme: Theme,
log_offset: usize,
) -> usize
where
G: PrintableGame,
B: Backend,
{
let board = panel(term, layout.viewport, theme, "");
game.draw_viewport(view, term, board);
let stats = panel(term, layout.stats, theme, "Stats");
print_rows(
term,
stats,
0,
game.get_stats(view)
.into_iter()
.map(|(key, value)| format!("{key}: {value}")),
);
draw_log(log, term, layout.log, theme, log_offset)
}