use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Modifier, Style},
widgets::{Block, Widget},
};
use super::super::{
nu_common::{NuStyle, string_width},
views::util::{nu_style_to_tui, set_span},
};
pub struct TitleBar {
title: String,
title_style: Style,
info_left: String,
info_right: String,
info_style: Style,
background_style: Style,
}
impl TitleBar {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
title_style: Style::default().add_modifier(Modifier::BOLD),
info_left: String::new(),
info_right: String::new(),
info_style: Style::default().add_modifier(Modifier::DIM),
background_style: Style::default(),
}
}
pub fn with_info_left(mut self, info: impl Into<String>) -> Self {
self.info_left = info.into();
self
}
pub fn with_info_right(mut self, info: impl Into<String>) -> Self {
self.info_right = info.into();
self
}
pub fn set_background_style(&mut self, style: NuStyle) {
self.background_style = nu_style_to_tui(style);
}
pub fn set_title_style(&mut self, style: NuStyle) {
self.title_style = nu_style_to_tui(style).add_modifier(Modifier::BOLD);
}
pub fn set_info_style(&mut self, style: NuStyle) {
self.info_style = nu_style_to_tui(style).add_modifier(Modifier::DIM);
}
}
impl Widget for TitleBar {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.height == 0 || area.width < 10 {
return;
}
let block = Block::default().style(self.background_style);
block.render(area, buf);
const PADDING: u16 = 1;
const SEPARATOR_WIDTH: u16 = 3;
let mut left_offset = PADDING;
let mut right_offset = PADDING;
if !self.info_left.is_empty() {
let info_width = string_width(&self.info_left) as u16;
let max_left_width = area.width / 3;
if left_offset + info_width < max_left_width {
set_span(
buf,
(area.x + left_offset, area.y),
&self.info_left,
self.info_style,
info_width,
);
left_offset += info_width + SEPARATOR_WIDTH;
}
}
if !self.info_right.is_empty() {
let info_width = string_width(&self.info_right) as u16;
let max_right_width = area.width / 3;
if right_offset + info_width < max_right_width {
let x = area.right().saturating_sub(right_offset + info_width);
set_span(
buf,
(x, area.y),
&self.info_right,
self.info_style,
info_width,
);
right_offset += info_width + SEPARATOR_WIDTH;
}
}
let title_width = string_width(&self.title) as u16;
let available_center = area.width.saturating_sub(left_offset + right_offset);
if title_width > 0 && available_center > 3 {
let ideal_x = area.x + (area.width.saturating_sub(title_width)) / 2;
let min_x = area.x + left_offset;
let max_x = area.right().saturating_sub(right_offset + title_width);
let title_x = ideal_x.clamp(min_x, max_x.max(min_x));
let actual_width = (area.right().saturating_sub(right_offset))
.saturating_sub(title_x)
.min(title_width);
if actual_width > 0 {
let title_to_render = if title_width > actual_width {
let mut truncated = self.title.clone();
while string_width(&truncated) as u16 > actual_width.saturating_sub(1)
&& !truncated.is_empty()
{
truncated.pop();
}
if !truncated.is_empty() {
truncated.push('…');
}
truncated
} else {
self.title
};
set_span(
buf,
(title_x, area.y),
&title_to_render,
self.title_style,
actual_width,
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn buffer_to_string(buf: &Buffer) -> String {
let mut result = String::new();
for y in 0..buf.area.height {
for x in 0..buf.area.width {
if let Some(cell) = buf.cell((x, y)) {
result.push_str(cell.symbol());
}
}
if y < buf.area.height - 1 {
result.push('\n');
}
}
result
}
fn render_title_bar(title_bar: TitleBar, width: u16, height: u16) -> Buffer {
let area = Rect::new(0, 0, width, height);
let mut buf = Buffer::empty(area);
title_bar.render(area, &mut buf);
buf
}
#[test]
fn test_title_bar_basic_render() {
let title_bar = TitleBar::new("Test Title");
let buf = render_title_bar(title_bar, 40, 1);
let content = buffer_to_string(&buf);
assert!(content.contains("Test Title"));
}
#[test]
fn test_title_bar_with_left_info() {
let title_bar = TitleBar::new("Title").with_info_left("Left Info");
let buf = render_title_bar(title_bar, 50, 1);
let content = buffer_to_string(&buf);
assert!(content.contains("Left Info"));
assert!(content.contains("Title"));
}
#[test]
fn test_title_bar_with_right_info() {
let title_bar = TitleBar::new("Title").with_info_right("Right Info");
let buf = render_title_bar(title_bar, 50, 1);
let content = buffer_to_string(&buf);
assert!(content.contains("Right Info"));
assert!(content.contains("Title"));
}
#[test]
fn test_title_bar_with_both_infos() {
let title_bar = TitleBar::new("Center")
.with_info_left("Left")
.with_info_right("Right");
let buf = render_title_bar(title_bar, 60, 1);
let content = buffer_to_string(&buf);
assert!(content.contains("Left"));
assert!(content.contains("Center"));
assert!(content.contains("Right"));
let left_pos = content.find("Left").unwrap();
let center_pos = content.find("Center").unwrap();
let right_pos = content.find("Right").unwrap();
assert!(left_pos < center_pos);
assert!(center_pos < right_pos);
}
#[test]
fn test_title_bar_narrow_width_no_render() {
let title_bar = TitleBar::new("Title");
let buf = render_title_bar(title_bar, 9, 1);
let content = buffer_to_string(&buf);
assert!(!content.contains("Title"));
}
#[test]
fn test_title_bar_zero_height_no_render() {
let area = Rect::new(0, 0, 40, 0);
assert_eq!(area.height, 0);
}
#[test]
fn test_title_bar_title_centered() {
let title_bar = TitleBar::new("XX");
let buf = render_title_bar(title_bar, 20, 1);
let content = buffer_to_string(&buf);
let title_pos = content.find("XX").unwrap();
assert!((8..=10).contains(&title_pos));
}
#[test]
fn test_title_bar_unicode_width() {
let title_bar = TitleBar::new("日本語"); let buf = render_title_bar(title_bar, 40, 1);
assert_eq!(buf.area.width, 40);
}
#[test]
fn test_title_bar_empty_title() {
let title_bar = TitleBar::new("");
let buf = render_title_bar(title_bar, 40, 1);
assert_eq!(buf.area.width, 40);
}
#[test]
fn test_title_bar_builder_pattern() {
let title_bar = TitleBar::new("Title")
.with_info_left("L")
.with_info_right("R");
assert_eq!(title_bar.title, "Title");
assert_eq!(title_bar.info_left, "L");
assert_eq!(title_bar.info_right, "R");
}
#[test]
fn test_title_bar_very_long_title_truncation() {
let long_title = "A".repeat(100);
let title_bar = TitleBar::new(long_title);
let buf = render_title_bar(title_bar, 30, 1);
let content = buffer_to_string(&buf);
assert!(content.contains('…') || content.len() <= 30);
}
#[test]
fn test_title_bar_info_respects_width_limit() {
let title_bar = TitleBar::new("T")
.with_info_left("A".repeat(50).as_str())
.with_info_right("B".repeat(50).as_str());
let buf = render_title_bar(title_bar, 30, 1);
assert_eq!(buf.area.width, 30);
}
}