tlc: panel visual polish — rounded borders, scrollbars, alt rows, 3D buttons

Rounded panel borders (BorderType::Rounded), native ratatui scrollbars
on file lists, alternating row colors (even rows lightened), file-size
inline bar in panel footer (█ proportional to largest file), 3D button
effect (▔ top + ▁ bottom rows when focused), and panel switch border
flash (cyan info color for 4 frames on Tab).
This commit is contained in:
2026-06-20 00:07:05 +03:00
parent d79dc12af3
commit 445edc39db
2 changed files with 85 additions and 8 deletions
@@ -8,7 +8,7 @@
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
use ratatui::widgets::{Block, BorderType, Borders, List, ListItem, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState};
use ratatui::Frame;
use crate::filemanager::panel::Panel;
@@ -38,6 +38,12 @@ impl FileManager {
return;
}
self.status.set_spinner_char(if self.spinner.is_active() {
self.spinner.current_char().to_string()
} else {
String::new()
});
if !self.panels_visible {
let chunks = Layout::default()
.direction(Direction::Vertical)
@@ -240,8 +246,13 @@ impl FileManager {
let title = format!(" {} ", format_utils::path_short(panel.path()));
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(if active {
Style::default().fg(header_fg).bg(panel_bg)
if self.panel_switch_anim < 100 {
Style::default().fg(self.theme.info).bg(panel_bg).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(header_fg).bg(panel_bg)
}
} else {
Style::default().fg(self.theme.border).bg(panel_bg)
})
@@ -303,8 +314,11 @@ impl FileManager {
.marked_names()
.iter()
.any(|n| n == &entry.name);
let style =
let mut style =
entry_style(entry, active, idx == cursor, is_marked, &self.theme);
if row % 2 == 0 && idx != cursor && !is_marked {
style = style.bg(self.theme.alt_row_bg());
}
let prefix = if is_marked { "*" } else { " " };
let display =
format!("{prefix}{}", format_line_mode(entry, mode, line_width));
@@ -317,16 +331,63 @@ impl FileManager {
let list = List::new(items)
.style(Style::default().fg(panel_fg).bg(panel_bg));
frame.render_widget(list, list_area);
let total = panel.entry_count().max(1);
let mut sb_state = ScrollbarState::new(total)
.viewport_content_length(body_height as usize)
.position(top);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.thumb_style(Style::default().fg(self.theme.border)),
list_area,
&mut sb_state,
);
}
}
if show_footer {
let footer_y = inner.y + inner.height.saturating_sub(1);
let footer_text = format_utils::panel_footer_text(panel);
let mut footer_spans: Vec<Span> = vec![Span::styled(
footer_text,
Style::default().fg(disabled_fg).bg(panel_bg),
)];
if let Some(entry) = panel.entries().get(cursor) {
if !entry.is_dir() && entry.name != ".." {
let max_size = panel
.entries()
.iter()
.filter(|e| !e.is_dir())
.map(|e| e.stat.size)
.max()
.unwrap_or(1)
.max(1);
let ratio = (entry.stat.size.min(max_size) * 10 / max_size).min(10) as usize;
let bar: String = "\u{2588}".repeat(ratio);
let pad = 10usize.saturating_sub(ratio);
footer_spans.push(Span::raw(" "));
footer_spans.push(Span::styled(
bar,
Style::default().fg(self.theme.info).bg(panel_bg),
));
if pad > 0 {
footer_spans.push(Span::styled(
"\u{2591}".repeat(pad),
Style::default().fg(self.theme.hidden).bg(panel_bg),
));
}
}
}
let total_w = footer_spans.iter().map(|s| s.width() as u16).sum::<u16>();
if total_w < inner.width {
let pad = inner.width - total_w;
footer_spans.insert(0, Span::styled(
" ".repeat(pad as usize),
Style::default().bg(panel_bg),
));
}
frame.render_widget(
Paragraph::new(Span::styled(
format_utils::panel_footer_text(panel),
Style::default().fg(disabled_fg).bg(panel_bg),
)),
Paragraph::new(Line::from(footer_spans)),
Rect::new(inner.x, footer_y, inner.width, 1),
);
}
@@ -154,7 +154,23 @@ impl Button {
Style::default().fg(pair.fg).bg(pair.bg),
));
let p = Paragraph::new(Line::from(spans)).style(Style::default().bg(pair.bg).fg(pair.fg));
frame.render_widget(p, area);
if self.focused && !self.disabled && area.height >= 3 {
let top_row = "\u{2594}".repeat(area.width as usize);
frame.render_widget(
Paragraph::new(top_row)
.style(Style::default().fg(theme.button_highlight()).bg(pair.bg)),
Rect::new(area.x, area.y, area.width, 1),
);
frame.render_widget(p, Rect::new(area.x, area.y + 1, area.width, 1));
let bot_row = "\u{2581}".repeat(area.width as usize);
frame.render_widget(
Paragraph::new(bot_row)
.style(Style::default().fg(theme.button_shadow()).bg(pair.bg)),
Rect::new(area.x, area.y + 2, area.width, 1),
);
} else {
frame.render_widget(p, area);
}
}
}