use crate::DisplayContext;
use crate::SkimItem;
use crate::SkimOptions;
use crate::theme::ColorTheme;
use crate::tui::BorderType;
use crate::tui::options::TuiLayout;
use crate::tui::util::char_display_width;
use crate::tui::util::style_line;
use crate::tui::util::style_text;
use crate::tui::widget::{SkimRender, SkimWidget};
use ansi_to_tui::IntoText;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::text::Text;
use ratatui::widgets::Widget;
use ratatui::widgets::{Block, Borders, Paragraph};
use std::cmp::max;
use std::sync::Arc;
#[derive(Clone, Default)]
pub struct Header {
pub header: String,
pub header_lines: Vec<Arc<dyn SkimItem>>,
header_lines_count: u16,
indent_size: u16,
theme: Arc<ColorTheme>,
pub border: Option<BorderType>,
reverse_lines: bool,
reverse: bool,
}
impl Header {
pub fn theme(mut self, theme: Arc<ColorTheme>) -> Self {
self.theme = theme;
self
}
pub fn height(&self) -> u16 {
let static_lines = if self.header.is_empty() {
0
} else {
self.header.lines().count() as u16
};
static_lines + self.header_lines_count
}
pub fn set_header_lines(&mut self, items: Vec<Arc<dyn SkimItem>>) {
self.header_lines = items;
if self.reverse_lines {
self.header_lines.reverse();
}
}
fn header_text<'a>(&self) -> Text<'a> {
let mut res = self.header.into_text().unwrap(); style_text(&mut res, self.theme.header);
res
}
}
fn apply_tabstop(text: &str, tabstop: usize) -> String {
let mut result = String::new();
let mut current_width = 0;
for ch in text.chars() {
if ch == '\t' {
let tab_width = tabstop - (current_width % tabstop);
result.push_str(&" ".repeat(tab_width));
current_width += tab_width;
} else {
result.push(ch);
current_width += char_display_width(ch);
}
}
result
}
impl SkimWidget for Header {
fn from_options(options: &SkimOptions, theme: Arc<ColorTheme>) -> Self {
let tabstop = max(1, options.tabstop);
let header = options.header.clone().unwrap_or_default();
let expanded_header = apply_tabstop(&header, tabstop);
let reverse_lines = options.layout == TuiLayout::Default;
Self {
header: expanded_header,
header_lines: Vec::new(),
header_lines_count: options
.header_lines
.try_into()
.expect("header_lines count overflows u16"),
indent_size: (options.selector_icon.chars().count() + options.multi_select_icon.chars().count())
.try_into()
.expect("Failed to fit selector lens into an u16"),
theme,
border: options.border,
reverse_lines,
reverse: options.layout == TuiLayout::Reverse,
}
}
fn render(&mut self, area: Rect, buf: &mut Buffer) -> SkimRender {
let block = if let Some(border_type) = self.border {
Block::default()
.borders(Borders::ALL)
.border_type(border_type.into())
.border_style(self.theme.border)
} else {
Block::default()
}
.padding(ratatui::widgets::Padding::left(self.indent_size));
let content_height = if self.header.is_empty() {
0
} else {
self.header_text().lines.len()
} + self.header_lines.len();
let container_height = if self.border.is_some() {
area.height - 2
} else {
area.height
};
let mut combined_header = if self.reverse && !self.header.is_empty() {
self.header_text()
} else {
let mut r = Text::default();
for _ in 0..(container_height.saturating_sub(content_height.try_into().unwrap())) {
r.push_line("");
}
r
};
let display_context = DisplayContext {
base_style: self.theme.header,
..Default::default()
};
for item in self.header_lines.iter() {
let mut line = item.display(display_context.clone());
style_line(&mut line, self.theme.header);
combined_header.push_line(line);
}
if !self.reverse && !self.header.is_empty() {
combined_header += self.header_text();
}
Paragraph::new(combined_header)
.style(self.theme.header)
.block(block)
.render(area, buf);
SkimRender::default()
}
}