use ratatui::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Paragraph},
Frame,
};
use tui_dispatch_core::Component;
use crate::style::{BaseStyle, ComponentStyle, Padding};
#[derive(Debug, Clone)]
pub struct StatusBarStyle {
pub base: BaseStyle,
pub text: Style,
pub hint_key: Style,
pub hint_label: Style,
pub separator: Style,
}
impl Default for StatusBarStyle {
fn default() -> Self {
Self {
base: BaseStyle {
border: None,
fg: None,
..Default::default()
},
text: Style::default(),
hint_key: Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
hint_label: Style::default(),
separator: Style::default().fg(Color::DarkGray),
}
}
}
impl StatusBarStyle {
pub fn borderless() -> Self {
let mut style = Self::default();
style.base.border = None;
style
}
pub fn minimal() -> Self {
let mut style = Self::default();
style.base.border = None;
style.base.padding = Padding::default();
style
}
}
impl ComponentStyle for StatusBarStyle {
fn base(&self) -> &BaseStyle {
&self.base
}
}
#[derive(Debug, Clone, Copy)]
pub struct StatusBarHint<'a> {
pub key: &'a str,
pub label: &'a str,
}
impl<'a> StatusBarHint<'a> {
pub fn new(key: &'a str, label: &'a str) -> Self {
Self { key, label }
}
}
#[derive(Debug, Clone)]
pub enum StatusBarItem<'a> {
Text(&'a str),
Span(Span<'a>),
}
impl<'a> StatusBarItem<'a> {
pub fn text(text: &'a str) -> Self {
Self::Text(text)
}
pub fn span(span: Span<'a>) -> Self {
Self::Span(span)
}
}
#[derive(Debug, Clone)]
pub enum StatusBarContent<'a> {
Empty,
Items(&'a [StatusBarItem<'a>]),
Hints(&'a [StatusBarHint<'a>]),
}
#[derive(Debug, Clone)]
pub struct StatusBarSection<'a> {
pub content: StatusBarContent<'a>,
pub separator: &'a str,
}
impl<'a> Default for StatusBarSection<'a> {
fn default() -> Self {
Self::empty()
}
}
impl<'a> StatusBarSection<'a> {
pub fn empty() -> Self {
Self {
content: StatusBarContent::Empty,
separator: " ",
}
}
pub fn items(items: &'a [StatusBarItem<'a>]) -> Self {
Self {
content: StatusBarContent::Items(items),
separator: " ",
}
}
pub fn hints(hints: &'a [StatusBarHint<'a>]) -> Self {
Self {
content: StatusBarContent::Hints(hints),
separator: "",
}
}
pub fn with_separator(mut self, separator: &'a str) -> Self {
self.separator = separator;
self
}
}
pub struct StatusBarProps<'a> {
pub left: StatusBarSection<'a>,
pub center: StatusBarSection<'a>,
pub right: StatusBarSection<'a>,
pub style: StatusBarStyle,
pub is_focused: bool,
}
impl<'a> StatusBarProps<'a> {
pub fn new(
left: StatusBarSection<'a>,
center: StatusBarSection<'a>,
right: StatusBarSection<'a>,
) -> Self {
Self {
left,
center,
right,
style: StatusBarStyle::default(),
is_focused: false,
}
}
}
#[derive(Default)]
pub struct StatusBar;
impl StatusBar {
pub fn new() -> Self {
Self
}
}
impl<A> Component<A> for StatusBar {
type Props<'a> = StatusBarProps<'a>;
fn handle_event(
&mut self,
_event: &tui_dispatch_core::EventKind,
_props: Self::Props<'_>,
) -> impl IntoIterator<Item = A> {
None
}
fn render(&mut self, frame: &mut Frame, area: Rect, props: Self::Props<'_>) {
let style = &props.style;
let mut background_style = Style::default();
if let Some(bg) = style.base.bg {
background_style = background_style.bg(bg);
}
for y in area.y..area.y.saturating_add(area.height) {
for x in area.x..area.x.saturating_add(area.width) {
if let Some(cell) = frame.buffer_mut().cell_mut((x, y)) {
cell.set_symbol(" ");
cell.set_style(background_style);
}
}
}
let content_area = Rect {
x: area.x + style.base.padding.left,
y: area.y + style.base.padding.top,
width: area.width.saturating_sub(style.base.padding.horizontal()),
height: area.height.saturating_sub(style.base.padding.vertical()),
};
let mut inner_area = content_area;
if let Some(border) = &style.base.border {
let block = Block::default()
.borders(border.borders)
.border_style(border.style_for_focus(props.is_focused));
inner_area = block.inner(content_area);
frame.render_widget(block, content_area);
}
if inner_area.width == 0 || inner_area.height == 0 {
return;
}
let row_area = Rect {
y: inner_area.y,
height: 1,
..inner_area
};
let left_line = section_line(&props.left, style);
let center_line = section_line(&props.center, style);
let right_line = section_line(&props.right, style);
let content_width = row_area.width as usize;
let right_width = right_line.width().min(content_width);
let left_width = left_line
.width()
.min(content_width.saturating_sub(right_width));
let gap_width = content_width.saturating_sub(left_width + right_width);
let center_width = center_line.width().min(gap_width);
if left_width > 0 {
let left_area = Rect {
width: left_width as u16,
..row_area
};
frame.render_widget(Paragraph::new(left_line).style(style.text), left_area);
}
if center_width > 0 {
let center_x = row_area.x
+ left_width as u16
+ ((gap_width.saturating_sub(center_width)) / 2) as u16;
let center_area = Rect {
x: center_x,
width: center_width as u16,
..row_area
};
frame.render_widget(Paragraph::new(center_line).style(style.text), center_area);
}
if right_width > 0 {
let right_area = Rect {
x: row_area.x + row_area.width.saturating_sub(right_width as u16),
width: right_width as u16,
..row_area
};
frame.render_widget(Paragraph::new(right_line).style(style.text), right_area);
}
}
}
fn section_line<'a>(section: &StatusBarSection<'a>, style: &StatusBarStyle) -> Line<'a> {
match section.content {
StatusBarContent::Empty => Line::raw(""),
StatusBarContent::Items(items) => items_line(items, section.separator, style),
StatusBarContent::Hints(hints) => hints_line(hints, section.separator, style),
}
}
fn items_line<'a>(
items: &'a [StatusBarItem<'a>],
separator: &'a str,
style: &StatusBarStyle,
) -> Line<'a> {
let mut spans = Vec::new();
for (idx, item) in items.iter().enumerate() {
if idx > 0 && !separator.is_empty() {
spans.push(Span::styled(separator, style.separator));
}
match item {
StatusBarItem::Text(text) => spans.push(Span::styled(*text, style.text)),
StatusBarItem::Span(span) => spans.push(span.clone()),
}
}
Line::from(spans)
}
fn hints_line<'a>(
hints: &'a [StatusBarHint<'a>],
separator: &'a str,
style: &StatusBarStyle,
) -> Line<'a> {
let mut spans = Vec::new();
for (idx, hint) in hints.iter().enumerate() {
if idx > 0 && !separator.is_empty() {
spans.push(Span::styled(separator, style.separator));
}
spans.push(Span::styled(format!(" {} ", hint.key), style.hint_key));
spans.push(Span::styled(format!(" {} ", hint.label), style.hint_label));
}
Line::from(spans)
}
#[cfg(test)]
mod tests {
use super::*;
use tui_dispatch_core::testing::RenderHarness;
#[test]
fn test_status_bar_renders_sections() {
let mut harness = RenderHarness::new(60, 1);
let mut status_bar = StatusBar::new();
let left_items = [StatusBarItem::text("Left")];
let center_items = [StatusBarItem::text("Center")];
let right_hints = [StatusBarHint::new("F1", "Help")];
let output = harness.render_to_string_plain(|frame| {
<StatusBar as Component<()>>::render(
&mut status_bar,
frame,
frame.area(),
StatusBarProps {
left: StatusBarSection::items(&left_items),
center: StatusBarSection::items(¢er_items),
right: StatusBarSection::hints(&right_hints),
style: StatusBarStyle::default(),
is_focused: false,
},
);
});
assert!(output.contains("Left"));
assert!(output.contains("Center"));
assert!(output.contains("Help"));
}
}