use ratatui::style::Style;
use ratatui::text::{Line, Span};
use super::{text_width, Theme};
pub(super) const ALWAYS: u8 = u8::MAX;
#[derive(Debug, Clone)]
pub(super) struct Part {
forms: Vec<(u8, Vec<Span<'static>>)>,
}
impl Part {
pub(super) fn new(keep: u8, spans: Vec<Span<'static>>) -> Self {
Self {
forms: vec![(keep, spans)],
}
}
pub(super) fn or(mut self, keep: u8, spans: Vec<Span<'static>>) -> Self {
self.forms.push((keep, spans));
self
}
fn width(spans: &[Span]) -> usize {
spans.iter().map(|span| text_width(&span.content)).sum()
}
}
pub(super) fn fit(parts: Vec<Part>, separator: &Span<'static>, room: usize) -> Vec<Span<'static>> {
let mut at: Vec<Option<usize>> = parts.iter().map(|_| Some(0)).collect();
let gap = text_width(&separator.content);
let width = |at: &[Option<usize>]| {
let (mut total, mut shown) = (0, 0_usize);
for (part, form) in parts.iter().zip(at) {
if let Some(form) = form {
total += Part::width(&part.forms[*form].1);
shown += 1;
}
}
total + gap * shown.saturating_sub(1)
};
let mut steps = Vec::new();
while width(&at) > room {
let weakest = parts
.iter()
.zip(&at)
.enumerate()
.filter_map(|(index, (part, form))| {
let keep = part.forms[(*form)?].0;
(keep != ALWAYS).then_some((keep, index))
})
.min_by_key(|(keep, index)| (*keep, usize::MAX - index));
let Some((_, index)) = weakest else { break };
steps.push((index, at[index]));
let next = at[index].map(|form| form + 1);
at[index] = next.filter(|form| *form < parts[index].forms.len());
}
let mut kept: Vec<usize> = Vec::new();
for (index, before) in steps.into_iter().rev() {
if kept.contains(&index) {
continue;
}
let after = std::mem::replace(&mut at[index], before);
if width(&at) > room {
at[index] = after;
kept.push(index);
}
}
let mut out = Vec::new();
for (part, form) in parts.into_iter().zip(at) {
let Some(form) = form else { continue };
if !out.is_empty() {
out.push(separator.clone());
}
out.extend(
part.forms
.into_iter()
.nth(form)
.map(|f| f.1)
.unwrap_or_default(),
);
}
out
}
pub(super) fn hint(theme: &Theme, keep: u8, key: &str, what: &str) -> Part {
Part::new(keep, hint_spans(theme, key, what))
}
fn hint_spans(theme: &Theme, key: &str, what: &str) -> Vec<Span<'static>> {
vec![
Span::styled(key.to_string(), theme.accent()),
Span::styled(format!(" {what}"), theme.dim()),
]
}
impl Part {
pub(super) fn or_hint(self, theme: &Theme, keep: u8, key: &str, what: &str) -> Self {
self.or(keep, hint_spans(theme, key, what))
}
}
pub(super) fn hints(parts: Vec<Part>, room: usize) -> Line<'static> {
Line::from(fit(parts, &Span::raw(" "), room))
}
pub(super) fn reserve(parts: &[Part], keep: u8) -> usize {
let widths: Vec<usize> = parts
.iter()
.filter_map(|part| part.forms.last())
.filter(|(at, _)| *at >= keep)
.map(|(_, spans)| Part::width(spans))
.collect();
widths.iter().sum::<usize>() + 2 * widths.len().saturating_sub(1)
}
pub(super) fn span(text: impl Into<String>, style: Style) -> Span<'static> {
Span::styled(text.into(), style)
}
#[cfg(test)]
mod tests {
use super::*;
fn text(spans: &[Span]) -> String {
spans.iter().map(|span| span.content.as_ref()).collect()
}
fn part(keep: u8, text: &str) -> Part {
Part::new(keep, vec![Span::raw(text.to_string())])
}
#[test]
fn the_lowest_priority_goes_first_and_always_stays() {
let parts = || {
vec![
part(3, "aaaa"),
part(1, "bbbb"),
part(2, "cccc"),
part(ALWAYS, "help"),
]
};
let sep = Span::raw(" ");
assert_eq!(text(&fit(parts(), &sep, 19)), "aaaa bbbb cccc help");
assert_eq!(text(&fit(parts(), &sep, 18)), "aaaa cccc help");
assert_eq!(text(&fit(parts(), &sep, 13)), "aaaa help");
assert_eq!(text(&fit(parts(), &sep, 2)), "help");
}
#[test]
fn a_part_comes_back_when_a_wider_one_went_after_it() {
let sep = Span::raw(" ");
let parts = vec![part(1, "ab"), part(5, &"x".repeat(20)), part(ALWAYS, "h")];
assert_eq!(text(&fit(parts, &sep, 10)), "ab h");
}
#[test]
fn ties_drop_the_rightmost_and_forms_shrink_before_going() {
let sep = Span::raw(" ");
let parts = vec![
part(1, "left"),
part(1, "right"),
Part::new(5, vec![Span::raw("long form")]).or(2, vec![Span::raw("lf")]),
];
assert_eq!(text(&fit(parts.clone(), &sep, 20)), "left right long form");
assert_eq!(text(&fit(parts.clone(), &sep, 15)), "left long form");
assert_eq!(text(&fit(parts.clone(), &sep, 9)), "long form");
assert_eq!(text(&fit(parts.clone(), &sep, 4)), "lf");
assert_eq!(text(&fit(parts, &sep, 1)), "");
}
}