use ratatui::style::Style;
use ratatui::text::{Line, Span};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
pub const MAX_CONTENT_WIDTH: u16 = 100;
const SIDE_GUTTER: u16 = 2;
pub const MIN_LIST_ROWS: usize = 3;
pub const MAX_LIST_ROWS: usize = 8;
pub const LOGO_MIN_WIDTH: u16 = 60;
pub const LOGO_MIN_HEIGHT: u16 = 30;
pub fn content_width(area_width: u16) -> u16 {
area_width
.saturating_sub(SIDE_GUTTER * 2)
.min(MAX_CONTENT_WIDTH)
.max(area_width.min(20))
}
pub fn shows_logo(area_width: u16, area_height: u16) -> bool {
area_width >= LOGO_MIN_WIDTH && area_height >= LOGO_MIN_HEIGHT
}
pub const TIMELINE_WIDTH: u16 = 28;
pub const TIMELINE_MIN_AREA_WIDTH: u16 = 110;
pub fn shows_timeline(area_width: u16) -> bool {
area_width >= TIMELINE_MIN_AREA_WIDTH
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeState {
Done,
Current,
Upcoming,
}
pub fn node_state(index: usize, current: usize) -> NodeState {
match (index + 1).cmp(¤t) {
std::cmp::Ordering::Less => NodeState::Done,
std::cmp::Ordering::Equal => NodeState::Current,
std::cmp::Ordering::Greater => NodeState::Upcoming,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimelineFit {
Spacious,
Compact,
Hidden,
}
const TIMELINE_HEADING_ROWS: usize = 3;
pub fn timeline_fit(steps: usize, rows: u16) -> TimelineFit {
let rows = rows as usize;
if steps == 0 {
TimelineFit::Hidden
} else if TIMELINE_HEADING_ROWS + steps * 2 - 1 <= rows {
TimelineFit::Spacious
} else if TIMELINE_HEADING_ROWS + steps <= rows {
TimelineFit::Compact
} else {
TimelineFit::Hidden
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BandPadding {
pub outer: u16,
pub inner: u16,
}
pub const BAND_FULL_PADDING_HEIGHT: u16 = 30;
pub const BAND_EDGE_PADDING_HEIGHT: u16 = 20;
pub fn header_spacing(area_height: u16) -> bool {
area_height >= BAND_FULL_PADDING_HEIGHT
}
pub fn band_padding(area_height: u16) -> BandPadding {
if area_height >= BAND_FULL_PADDING_HEIGHT {
BandPadding { outer: 1, inner: 1 }
} else if area_height >= BAND_EDGE_PADDING_HEIGHT {
BandPadding { outer: 1, inner: 0 }
} else {
BandPadding { outer: 0, inner: 0 }
}
}
pub fn scroll_offset(
focus_row: usize,
total_rows: usize,
visible: usize,
user_extra: usize,
) -> usize {
let max_scroll = total_rows.saturating_sub(visible);
let focus = if focus_row > 2 && total_rows > visible {
focus_row.saturating_sub(2)
} else {
0
};
focus.saturating_add(user_extra).min(max_scroll)
}
pub fn wrap_line(line: Line<'static>, width: usize) -> Vec<Line<'static>> {
let total: usize = line.spans.iter().map(|s| s.content.width()).sum();
if width == 0 || total <= width {
return vec![line];
}
let cells: Vec<(char, Style)> = line
.spans
.iter()
.flat_map(|s| s.content.chars().map(move |c| (c, s.style)))
.collect();
let mut out = Vec::new();
let mut start = 0;
while start < cells.len() {
let mut used = 0;
let mut end = start;
while end < cells.len() {
let w = cells[end].0.width().unwrap_or(0);
if used + w > width {
break;
}
used += w;
end += 1;
}
if end == start {
end = start + 1;
}
if end < cells.len()
&& let Some(sp) = cells[start..end].iter().rposition(|(c, _)| *c == ' ')
&& sp > 0
{
end = start + sp + 1;
}
let mut piece = cells_to_line(&cells[start..end]);
piece.style = line.style;
piece.alignment = line.alignment;
out.push(piece);
start = end;
while start < cells.len() && cells[start].0 == ' ' {
start += 1;
}
}
out
}
pub fn wrap_lines(lines: Vec<Line<'static>>, width: usize) -> (Vec<Line<'static>>, Vec<usize>) {
let mut rows = Vec::with_capacity(lines.len());
let mut starts = Vec::with_capacity(lines.len());
for line in lines {
starts.push(rows.len());
rows.extend(wrap_line(line, width));
}
(rows, starts)
}
fn cells_to_line(cells: &[(char, Style)]) -> Line<'static> {
let mut spans: Vec<Span<'static>> = Vec::new();
let mut buf = String::new();
let mut cur: Option<Style> = None;
for &(c, style) in cells {
if cur.is_some_and(|s| s != style) {
spans.push(Span::styled(
std::mem::take(&mut buf),
cur.unwrap_or_default(),
));
}
cur = Some(style);
buf.push(c);
}
if !buf.is_empty() {
spans.push(Span::styled(buf, cur.unwrap_or_default()));
}
Line::from(spans)
}