From 445edc39dbc894f04147ac432b5fcebb712a2e50 Mon Sep 17 00:00:00 2001 From: vasilito Date: Sat, 20 Jun 2026 00:07:05 +0300 Subject: [PATCH] =?UTF-8?q?tlc:=20panel=20visual=20polish=20=E2=80=94=20ro?= =?UTF-8?q?unded=20borders,=20scrollbars,=20alt=20rows,=203D=20buttons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../tui/tlc/source/src/filemanager/render.rs | 75 +++++++++++++++++-- .../tui/tlc/source/src/widget/button.rs | 18 ++++- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/local/recipes/tui/tlc/source/src/filemanager/render.rs b/local/recipes/tui/tlc/source/src/filemanager/render.rs index 563f2e8728..dd6e9119c1 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/render.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/render.rs @@ -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 = 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::(); + 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), ); } diff --git a/local/recipes/tui/tlc/source/src/widget/button.rs b/local/recipes/tui/tlc/source/src/widget/button.rs index 88e76bca5a..a0fb02b159 100644 --- a/local/recipes/tui/tlc/source/src/widget/button.rs +++ b/local/recipes/tui/tlc/source/src/widget/button.rs @@ -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); + } } }