use std::cell::RefCell;
use ratatui::Frame;
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::Modifier;
use ratatui::text::Line;
use ratatui::widgets::{Block, Clear, Padding, Paragraph};
use crate::theme::Skin;
pub const BRAND: &str = "hop";
pub const TAB_LABELS: [(&str, &str); 3] = [
("1", "Git Repos"),
("2", "Files and Folders"),
("3", "Archiv"),
];
const SCRIM_FACTOR: f32 = 0.4;
const POPUP_LIGHTEN: f32 = 0.08;
thread_local! {
static LAST_FRAME: RefCell<Option<Buffer>> = const { RefCell::new(None) };
}
pub fn snapshot_frame(frame: &mut Frame<'_>) {
let snapshot = frame.buffer_mut().clone();
LAST_FRAME.with(|last| *last.borrow_mut() = Some(snapshot));
}
pub fn blit_backdrop(frame: &mut Frame<'_>) {
LAST_FRAME.with(|last| {
let borrow = last.borrow();
if let Some(buf) = borrow.as_ref() {
let dst = frame.buffer_mut();
if buf.area == dst.area {
dst.content.clone_from(&buf.content);
}
}
});
}
pub fn dim_backdrop(frame: &mut Frame<'_>) {
blit_backdrop(frame);
ratada::overlay::dim(frame, SCRIM_FACTOR);
}
pub fn popup_backdrop(frame: &mut Frame<'_>, skin: &Skin, area: Rect) {
dim_backdrop(frame);
frame.render_widget(Clear, area);
let bg = skin.palette.surface.lighten(POPUP_LIGHTEN);
frame.render_widget(Block::default().style(ratada::style::bg(bg)), area);
}
const PROGRESS_HEIGHT: u16 = 1;
pub struct FrameAreas {
pub content: Rect,
pub progress: Option<Rect>,
}
pub fn render_frame(
frame: &mut Frame<'_>,
skin: &Skin,
active: usize,
status: Vec<Line>,
hints: &[(String, Vec<(String, String)>)],
show_progress: bool,
) -> FrameAreas {
let area = frame.area();
frame.render_widget(
Block::default().style(ratada::style::base(
skin.palette.foreground,
skin.palette.background,
)),
area,
);
let width = area.width.saturating_sub(2) as usize;
let header_h = header_height(width);
let status_h = status.len() as u16;
let progress_h = if show_progress { PROGRESS_HEIGHT } else { 0 };
let hints_h = if hints.is_empty() {
0
} else {
hints_height(hints, width)
};
let chunks = Layout::vertical([
Constraint::Length(header_h),
Constraint::Length(1),
Constraint::Min(1),
Constraint::Length(progress_h),
Constraint::Length(status_h),
Constraint::Length(hints_h),
])
.split(area);
fill(frame, chunks[1], skin.palette.surface);
fill(frame, chunks[2], skin.palette.surface);
render_header(frame, chunks[0], skin, active);
if status_h > 0 {
fill(frame, chunks[4], skin.palette.footer);
frame.render_widget(
Paragraph::new(status)
.block(Block::default().padding(Padding::horizontal(1))),
chunks[4],
);
}
if hints_h > 0 {
render_hints(frame, chunks[5], skin, hints);
}
let content = Block::default()
.padding(Padding::new(1, 1, 0, 0))
.inner(chunks[2]);
let progress = (progress_h > 0).then_some(chunks[3]);
FrameAreas { content, progress }
}
fn header_height(width: usize) -> u16 {
ratada::tabs::height(BRAND, &TAB_LABELS, width) + 2
}
fn render_header(
frame: &mut Frame<'_>,
area: Rect,
skin: &Skin,
active: usize,
) {
let block = Block::default()
.style(ratada::style::bg(skin.palette.header))
.padding(Padding::uniform(1));
let inner = block.inner(area);
frame.render_widget(block, area);
let tab_height =
ratada::tabs::height(BRAND, &TAB_LABELS, inner.width as usize);
let tab_area = Rect {
height: tab_height.min(inner.height),
..inner
};
ratada::tabs::render(frame, tab_area, skin, BRAND, &TAB_LABELS, active);
}
fn fill(frame: &mut Frame<'_>, area: Rect, color: crate::theme::Color) {
frame.render_widget(Block::default().style(ratada::style::bg(color)), area);
}
fn hint_style(skin: &Skin) -> ratada::shortcut_hints::HintStyle {
ratada::shortcut_hints::HintStyle {
key: ratada::style::fg(skin.palette.accent)
.add_modifier(Modifier::BOLD),
label: ratada::style::dim(),
description: ratada::style::secondary(&skin.palette),
top_margin: 1,
background: None,
}
}
fn to_hint_groups(
groups: &[(String, Vec<(String, String)>)],
) -> Vec<ratada::shortcut_hints::HintGroup<'_, String>> {
groups
.iter()
.map(|(label, pairs)| ratada::shortcut_hints::HintGroup {
label: label.as_str(),
hints: pairs.as_slice(),
})
.collect()
}
fn hints_height(
hints: &[(String, Vec<(String, String)>)],
width: usize,
) -> u16 {
ratada::shortcut_hints::height(&to_hint_groups(hints), width, 1)
}
fn render_hints(
frame: &mut Frame<'_>,
area: Rect,
skin: &Skin,
hints: &[(String, Vec<(String, String)>)],
) {
let opts = hint_style(skin);
let hint_area = Rect {
x: area.x + 1,
width: area.width.saturating_sub(2),
..area
};
ratada::shortcut_hints::render(
frame,
hint_area,
&to_hint_groups(hints),
&opts,
);
}
#[cfg(test)]
mod tests {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use super::*;
fn skin() -> Skin {
crate::config::Config::default().skin()
}
fn areas(show_progress: bool) -> FrameAreas {
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
let mut out = None;
terminal
.draw(|frame| {
out = Some(render_frame(
frame,
&skin(),
0,
vec![Line::raw("info")],
&[(
"App".to_string(),
vec![("q".to_string(), "quit".to_string())],
)],
show_progress,
));
})
.unwrap();
out.unwrap()
}
#[test]
fn reserves_a_progress_region_when_shown() {
let with = areas(true);
let progress = with.progress.expect("progress region when shown");
assert_eq!(progress.height, PROGRESS_HEIGHT);
let without = areas(false);
assert!(without.progress.is_none());
assert_eq!(
without.content.height - with.content.height,
PROGRESS_HEIGHT,
);
}
}