use crate::render::{Cell, Modifier};
use crate::style::Color;
use crate::widget::theme::{DARK_BG, SECONDARY_TEXT};
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum StatusBarPosition {
Top,
#[default]
Bottom,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SectionAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Clone)]
pub struct StatusSection {
pub content: String,
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: bool,
pub min_width: u16,
pub priority: u8,
}
impl StatusSection {
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
fg: None,
bg: None,
bold: false,
min_width: 0,
priority: 0,
}
}
pub fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
pub fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
pub fn min_width(mut self, width: u16) -> Self {
self.min_width = width;
self
}
pub fn priority(mut self, priority: u8) -> Self {
self.priority = priority;
self
}
pub fn width(&self) -> u16 {
self.content.chars().count().max(self.min_width as usize) as u16
}
}
#[derive(Clone)]
pub struct KeyHint {
pub key: String,
pub description: String,
}
impl KeyHint {
pub fn new(key: impl Into<String>, description: impl Into<String>) -> Self {
Self {
key: key.into(),
description: description.into(),
}
}
}
pub struct StatusBar {
left: Vec<StatusSection>,
center: Vec<StatusSection>,
right: Vec<StatusSection>,
position: StatusBarPosition,
bg: Color,
fg: Color,
key_hints: Vec<KeyHint>,
key_fg: Color,
key_bg: Color,
separator: Option<char>,
height: u16,
props: WidgetProps,
}
impl StatusBar {
pub fn new() -> Self {
Self {
left: Vec::new(),
center: Vec::new(),
right: Vec::new(),
position: StatusBarPosition::Bottom,
bg: DARK_BG,
fg: Color::WHITE,
key_hints: Vec::new(),
key_fg: Color::BLACK,
key_bg: SECONDARY_TEXT,
separator: None,
height: 1,
props: WidgetProps::new(),
}
}
pub fn position(mut self, position: StatusBarPosition) -> Self {
self.position = position;
self
}
pub fn header(mut self) -> Self {
self.position = StatusBarPosition::Top;
self
}
pub fn footer(mut self) -> Self {
self.position = StatusBarPosition::Bottom;
self
}
pub fn left(mut self, section: StatusSection) -> Self {
self.left.push(section);
self
}
pub fn center(mut self, section: StatusSection) -> Self {
self.center.push(section);
self
}
pub fn right(mut self, section: StatusSection) -> Self {
self.right.push(section);
self
}
pub fn left_text(self, text: impl Into<String>) -> Self {
self.left(StatusSection::new(text))
}
pub fn center_text(self, text: impl Into<String>) -> Self {
self.center(StatusSection::new(text))
}
pub fn right_text(self, text: impl Into<String>) -> Self {
self.right(StatusSection::new(text))
}
pub fn bg(mut self, color: Color) -> Self {
self.bg = color;
self
}
pub fn fg(mut self, color: Color) -> Self {
self.fg = color;
self
}
pub fn key(mut self, key: impl Into<String>, description: impl Into<String>) -> Self {
self.key_hints.push(KeyHint::new(key, description));
self
}
pub fn keys(mut self, hints: Vec<KeyHint>) -> Self {
self.key_hints.extend(hints);
self
}
pub fn separator(mut self, sep: char) -> Self {
self.separator = Some(sep);
self
}
pub fn height(mut self, height: u16) -> Self {
self.height = height.max(1);
self
}
pub fn update_left(&mut self, index: usize, content: impl Into<String>) {
if let Some(section) = self.left.get_mut(index) {
section.content = content.into();
}
}
pub fn update_center(&mut self, index: usize, content: impl Into<String>) {
if let Some(section) = self.center.get_mut(index) {
section.content = content.into();
}
}
pub fn update_right(&mut self, index: usize, content: impl Into<String>) {
if let Some(section) = self.right.get_mut(index) {
section.content = content.into();
}
}
pub fn clear(&mut self) {
self.left.clear();
self.center.clear();
self.right.clear();
self.key_hints.clear();
}
fn render_y(&self, area_height: u16) -> u16 {
match self.position {
StatusBarPosition::Top => 0,
StatusBarPosition::Bottom => area_height.saturating_sub(self.height),
}
}
#[doc(hidden)]
pub fn get_left(&self) -> &[StatusSection] {
&self.left
}
#[doc(hidden)]
pub fn get_center(&self) -> &[StatusSection] {
&self.center
}
#[doc(hidden)]
pub fn get_right(&self) -> &[StatusSection] {
&self.right
}
#[doc(hidden)]
pub fn get_position(&self) -> StatusBarPosition {
self.position
}
#[doc(hidden)]
pub fn get_bg(&self) -> Color {
self.bg
}
#[doc(hidden)]
pub fn get_fg(&self) -> Color {
self.fg
}
#[doc(hidden)]
pub fn get_key_hints(&self) -> &[KeyHint] {
&self.key_hints
}
#[doc(hidden)]
pub fn get_separator(&self) -> Option<char> {
self.separator
}
#[doc(hidden)]
pub fn get_height(&self) -> u16 {
self.height
}
#[doc(hidden)]
pub fn get_render_y(&self, area_height: u16) -> u16 {
self.render_y(area_height)
}
}
impl Default for StatusBar {
fn default() -> Self {
Self::new()
}
}
impl View for StatusBar {
crate::impl_view_meta!("StatusBar");
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let y = self.render_y(area.height);
if y >= area.height {
return;
}
for row in 0..self.height {
if y + row >= area.height {
break;
}
for x in 0..area.width {
let mut cell = Cell::new(' ');
cell.bg = Some(self.bg);
ctx.set(x, y + row, cell);
}
}
let left_width: u16 = self.left.iter().map(|s| s.width() + 1).sum();
let center_width: u16 = self.center.iter().map(|s| s.width() + 1).sum();
let right_width: u16 = self.right.iter().map(|s| s.width() + 1).sum();
let mut x: u16 = 0;
for section in &self.left {
x = self.render_section(ctx, section, x, y);
if self.separator.is_some() && x < area.width {
x += 1;
}
}
let center_start = (area.width.saturating_sub(center_width)) / 2;
let mut x = center_start.max(x + 1);
for section in &self.center {
x = self.render_section(ctx, section, x, y);
if self.separator.is_some() && x < area.width {
x += 1;
}
}
let mut x = area.width - right_width;
for section in &self.right {
x = self.render_section(ctx, section, x, y);
if self.separator.is_some() && x < area.width {
x += 1;
}
}
if self.height > 1 && !self.key_hints.is_empty() {
self.render_key_hints(ctx, 0, y + 1, area.width);
} else if self.height == 1 && !self.key_hints.is_empty() {
let hints_start = left_width + 2;
let hints_end = area.width - right_width - 2;
if hints_start < hints_end {
self.render_key_hints_inline(ctx, hints_start, y, hints_end - hints_start);
}
}
}
}
impl_styled_view!(StatusBar);
impl_props_builders!(StatusBar);
impl StatusBar {
fn render_section(
&self,
ctx: &mut RenderContext,
section: &StatusSection,
x: u16,
y: u16,
) -> u16 {
let fg = section.fg.unwrap_or(self.fg);
let bg = section.bg.unwrap_or(self.bg);
let mut current_x = x;
for ch in section.content.chars() {
if current_x >= ctx.area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
cell.bg = Some(bg);
if section.bold {
cell.modifier |= Modifier::BOLD;
}
ctx.set(current_x, y, cell);
current_x += 1;
}
while current_x < x + section.min_width && current_x < ctx.area.width {
let mut cell = Cell::new(' ');
cell.bg = Some(bg);
ctx.set(current_x, y, cell);
current_x += 1;
}
current_x
}
fn render_key_hints(&self, ctx: &mut RenderContext, x: u16, y: u16, width: u16) {
let mut current_x = x;
for hint in &self.key_hints {
if current_x >= x + width {
break;
}
for ch in hint.key.chars() {
if current_x >= x + width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(self.key_fg);
cell.bg = Some(self.key_bg);
cell.modifier |= Modifier::BOLD;
ctx.set(current_x, y, cell);
current_x += 1;
}
let desc = format!(" {} ", hint.description);
for ch in desc.chars() {
if current_x >= x + width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(self.fg);
cell.bg = Some(self.bg);
ctx.set(current_x, y, cell);
current_x += 1;
}
}
}
fn render_key_hints_inline(&self, ctx: &mut RenderContext, x: u16, y: u16, width: u16) {
let mut current_x = x;
for hint in &self.key_hints {
let hint_width = hint.key.len() + hint.description.len() + 3;
if current_x + hint_width as u16 > x + width {
break;
}
for ch in hint.key.chars() {
let mut cell = Cell::new(ch);
cell.fg = Some(self.key_fg);
cell.bg = Some(self.key_bg);
ctx.set(current_x, y, cell);
current_x += 1;
}
current_x += 1;
for ch in hint.description.chars() {
let mut cell = Cell::new(ch);
cell.fg = Some(self.fg);
cell.bg = Some(self.bg);
ctx.set(current_x, y, cell);
current_x += 1;
}
current_x += 2;
}
}
}
pub fn statusbar() -> StatusBar {
StatusBar::new()
}
pub fn header() -> StatusBar {
StatusBar::new().header()
}
pub fn footer() -> StatusBar {
StatusBar::new().footer()
}
pub fn section(content: impl Into<String>) -> StatusSection {
StatusSection::new(content)
}
pub fn key_hint(key: impl Into<String>, description: impl Into<String>) -> KeyHint {
KeyHint::new(key, description)
}