feat(tui): give the panes the colour the demo promised
The cursor row set a foreground, and ratatui only skips a cell's colour when that field is None — so selecting a row repainted its gold sizes, blue usernames and status glyphs into one flat cream. Table cursors now tint the background only, over a warm oxide wash instead of neutral. Browse files, their sizes and the settings popup rendered in the raw terminal foreground; they take the palette now, and free slots read green when a peer has one.
This commit is contained in:
@@ -200,46 +200,54 @@ impl MainTui {
|
||||
let area = centered_rect(70, 60, frame.area());
|
||||
frame.render_widget(ratatui::widgets::Clear, area);
|
||||
|
||||
let mut lines: Vec<ratatui::text::Line> = Vec::new();
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
let marker = |selected: bool| if selected { "> " } else { " " };
|
||||
|
||||
let download_line = if settings.mode == SettingsMode::EditingDownloadDir
|
||||
{
|
||||
format!("> Download folder: {}▏", settings.input)
|
||||
} else {
|
||||
format!(
|
||||
"{}Download folder: {}",
|
||||
marker(settings.selected == 0),
|
||||
settings.download_dir
|
||||
)
|
||||
let entry = |selected: bool, label: &str, value: String| {
|
||||
Line::from(vec![
|
||||
Span::styled(marker(selected).to_string(), accent_style()),
|
||||
Span::styled(label.to_string(), dimmed_style()),
|
||||
Span::styled(value, primary_style()),
|
||||
])
|
||||
};
|
||||
lines.push(ratatui::text::Line::from(download_line));
|
||||
lines.push(ratatui::text::Line::from(""));
|
||||
lines.push(ratatui::text::Line::from(format!(
|
||||
"Shared folders ({}):",
|
||||
settings.share_dirs.len()
|
||||
)));
|
||||
|
||||
lines.push(if settings.mode == SettingsMode::EditingDownloadDir {
|
||||
Line::from(vec![
|
||||
Span::styled("> ", accent_style()),
|
||||
Span::styled("Download folder: ", dimmed_style()),
|
||||
Span::styled(settings.input.clone(), primary_style()),
|
||||
Span::styled("▏", accent_style()),
|
||||
])
|
||||
} else {
|
||||
entry(
|
||||
settings.selected == 0,
|
||||
"Download folder: ",
|
||||
settings.download_dir.clone(),
|
||||
)
|
||||
});
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::styled(
|
||||
format!("Shared folders ({}):", settings.share_dirs.len()),
|
||||
dimmed_style(),
|
||||
));
|
||||
for (i, dir) in settings.share_dirs.iter().enumerate() {
|
||||
lines.push(ratatui::text::Line::from(format!(
|
||||
"{}{}",
|
||||
marker(settings.selected == i + 1),
|
||||
dir
|
||||
)));
|
||||
lines.push(entry(settings.selected == i + 1, "", dir.clone()));
|
||||
}
|
||||
if settings.share_dirs.is_empty() {
|
||||
lines.push(ratatui::text::Line::from(
|
||||
lines.push(Line::styled(
|
||||
" (nothing shared — press 'a' to add a folder)",
|
||||
dimmed_style(),
|
||||
));
|
||||
}
|
||||
if settings.mode == SettingsMode::AddingShare {
|
||||
lines.push(ratatui::text::Line::from(format!(
|
||||
" Add share: {}▏",
|
||||
settings.input
|
||||
)));
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" Add share: ", dimmed_style()),
|
||||
Span::styled(settings.input.clone(), primary_style()),
|
||||
Span::styled("▏", accent_style()),
|
||||
]));
|
||||
}
|
||||
if let Some(status) = &settings.status {
|
||||
lines.push(ratatui::text::Line::from(""));
|
||||
lines.push(ratatui::text::Line::from(status.clone()));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::styled(status.clone(), warning_style()));
|
||||
}
|
||||
|
||||
let block = pane_block(true).title(plain_title("Settings", true));
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::models::{BrowseState, BrowseStatus, BrowseTabs};
|
||||
use crate::ui::{
|
||||
HIGHLIGHT_SYMBOL, dimmed_style, error_style, format_bytes,
|
||||
HIGHLIGHT_SYMBOL, body_style, dimmed_style, error_style, format_bytes,
|
||||
get_spinner_char, highlight_style, pane_block, primary_style,
|
||||
row_highlight_style, warning_style,
|
||||
};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
@@ -76,11 +77,17 @@ fn render_browse_one(
|
||||
get_spinner_char(spinner_state),
|
||||
browse.username
|
||||
);
|
||||
frame.render_widget(Paragraph::new(text).block(block), area);
|
||||
frame.render_widget(
|
||||
Paragraph::new(text).style(dimmed_style()).block(block),
|
||||
area,
|
||||
);
|
||||
}
|
||||
BrowseStatus::Empty => {
|
||||
let text = format!("{} is not sharing any files.", browse.username);
|
||||
frame.render_widget(Paragraph::new(text).block(block), area);
|
||||
frame.render_widget(
|
||||
Paragraph::new(text).style(dimmed_style()).block(block),
|
||||
area,
|
||||
);
|
||||
}
|
||||
BrowseStatus::TimedOut => {
|
||||
let text = vec![
|
||||
@@ -89,11 +96,12 @@ fn render_browse_one(
|
||||
error_style(),
|
||||
),
|
||||
Line::raw(""),
|
||||
Line::raw(
|
||||
Line::styled(
|
||||
"They may be offline, or their connection can't be \
|
||||
reached (both of you may be behind a router/firewall).",
|
||||
body_style(),
|
||||
),
|
||||
Line::raw("Press r to try again."),
|
||||
Line::styled("Press r to try again.", dimmed_style()),
|
||||
];
|
||||
frame.render_widget(Paragraph::new(text).block(block), area);
|
||||
}
|
||||
@@ -107,15 +115,20 @@ fn render_browse_one(
|
||||
let glyph = if row.expanded { "▾" } else { "▸" };
|
||||
(
|
||||
Cell::from(format!("{indent}{glyph} {}", row.name))
|
||||
.style(primary_style()),
|
||||
.style(
|
||||
primary_style()
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Cell::from(String::new()),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Cell::from(format!("{indent} {}", row.name)),
|
||||
Cell::from(format!("{indent} {}", row.name))
|
||||
.style(body_style()),
|
||||
Cell::from(
|
||||
row.size.map(format_bytes).unwrap_or_default(),
|
||||
),
|
||||
)
|
||||
.style(warning_style()),
|
||||
)
|
||||
};
|
||||
Row::new(vec![label, size])
|
||||
@@ -124,7 +137,7 @@ fn render_browse_one(
|
||||
|
||||
let table =
|
||||
Table::new(rows, [Constraint::Fill(1), Constraint::Length(12)])
|
||||
.row_highlight_style(highlight_style())
|
||||
.row_highlight_style(row_highlight_style())
|
||||
.highlight_symbol(HIGHLIGHT_SYMBOL)
|
||||
.highlight_spacing(HighlightSpacing::Always)
|
||||
.block(block);
|
||||
|
||||
@@ -199,12 +199,16 @@ fn build_info_lines(
|
||||
lines.push(label_value("Queue position", &position_text));
|
||||
|
||||
if let Some(slots) = download.metadata.peer_free_slots {
|
||||
let slot_text = if slots > 0 {
|
||||
format!("{slots} available")
|
||||
let (slot_text, slot_style) = if slots > 0 {
|
||||
(format!("{slots} available"), success_style())
|
||||
} else {
|
||||
"all busy".to_string()
|
||||
("all busy".to_string(), inactive_style())
|
||||
};
|
||||
lines.push(label_value("Free slots", &slot_text));
|
||||
lines.push(label_value_styled(
|
||||
"Free slots",
|
||||
slot_text,
|
||||
slot_style,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(speed) = download.metadata.peer_upload_speed {
|
||||
|
||||
@@ -2,8 +2,9 @@ use crate::models::DownloadEntry;
|
||||
use crate::ui::{
|
||||
GLYPH_ACTIVE, GLYPH_DONE, GLYPH_FAILED, HIGHLIGHT_SYMBOL, accent_style,
|
||||
body_style, dimmed_style, download_status_glyph, error_style, format_bytes,
|
||||
format_speed, header_style, highlight_style, inactive_style, info_style,
|
||||
pane_block, pane_title, primary_style, success_style, warning_style,
|
||||
format_speed, header_style, inactive_style, info_style, pane_block,
|
||||
pane_title, primary_style, row_highlight_style, success_style,
|
||||
warning_style,
|
||||
};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
@@ -173,7 +174,7 @@ pub fn render_downloads_pane(
|
||||
|
||||
let table = Table::new(rows, widths)
|
||||
.header(header)
|
||||
.row_highlight_style(highlight_style())
|
||||
.row_highlight_style(row_highlight_style())
|
||||
.highlight_symbol(HIGHLIGHT_SYMBOL)
|
||||
.highlight_spacing(HighlightSpacing::Always)
|
||||
.block(pane_block(focused).title(pane_title(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::models::FileDisplayData;
|
||||
use crate::ui::{
|
||||
BYTES_PER_MB, HIGHLIGHT_SYMBOL, body_style, dimmed_style, format_bytes,
|
||||
header_style, highlight_style, info_style, pane_block, pane_title,
|
||||
header_style, info_style, pane_block, pane_title, row_highlight_style,
|
||||
success_style, warning_style,
|
||||
};
|
||||
use ratatui::{
|
||||
@@ -167,7 +167,7 @@ pub fn render_results_pane(
|
||||
|
||||
let table = Table::new(rows, widths)
|
||||
.header(header)
|
||||
.row_highlight_style(highlight_style())
|
||||
.row_highlight_style(row_highlight_style())
|
||||
.highlight_symbol(HIGHLIGHT_SYMBOL)
|
||||
.highlight_spacing(HighlightSpacing::Always)
|
||||
.block(pane_block(focused).title(pane_title("2", &title, focused)));
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::models::{RoomsState, RoomsView};
|
||||
use crate::ui::{
|
||||
HIGHLIGHT_SYMBOL, PANE_PADDING, accent_style, dimmed_style,
|
||||
highlight_style, info_style, pane_block, plain_title, primary_style,
|
||||
row_highlight_style,
|
||||
};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
@@ -84,7 +85,7 @@ fn render_list(
|
||||
let table =
|
||||
Table::new(table_rows, [Constraint::Fill(1), Constraint::Length(8)])
|
||||
.header(header)
|
||||
.row_highlight_style(highlight_style())
|
||||
.row_highlight_style(row_highlight_style())
|
||||
.highlight_symbol(HIGHLIGHT_SYMBOL)
|
||||
.highlight_spacing(HighlightSpacing::Always)
|
||||
.block(block);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::models::{SearchEntry, SearchStatus};
|
||||
use crate::ui::{
|
||||
HIGHLIGHT_SYMBOL, body_style, dimmed_style, header_style, highlight_style,
|
||||
pane_block, pane_title, success_style, warning_style,
|
||||
HIGHLIGHT_SYMBOL, body_style, dimmed_style, header_style, pane_block,
|
||||
pane_title, row_highlight_style, success_style, warning_style,
|
||||
};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
@@ -60,7 +60,7 @@ pub fn render_searches_pane(
|
||||
|
||||
let table = Table::new(rows, widths)
|
||||
.header(header)
|
||||
.row_highlight_style(highlight_style())
|
||||
.row_highlight_style(row_highlight_style())
|
||||
.highlight_symbol(HIGHLIGHT_SYMBOL)
|
||||
.highlight_spacing(HighlightSpacing::Always)
|
||||
.block(pane_block(focused).title(pane_title("1", "Searches", focused)));
|
||||
|
||||
@@ -11,6 +11,9 @@ use soulseek_rs::DownloadStatus;
|
||||
pub const PANE_PADDING: Padding = Padding::horizontal(1);
|
||||
|
||||
pub const VINYL_2: Color = Color::Rgb(0x26, 0x21, 0x1D);
|
||||
/// A warm oxide wash for the cursor row — dark enough that every column keeps
|
||||
/// its own colour on top of it.
|
||||
pub const SELECTION: Color = Color::Rgb(0x30, 0x1B, 0x14);
|
||||
pub const DUST: Color = Color::Rgb(0x8A, 0x81, 0x78);
|
||||
pub const PAPER: Color = Color::Rgb(0xE8, 0xE1, 0xD6);
|
||||
pub const PAPER_DIM: Color = Color::Rgb(0xC9, 0xC1, 0xB4);
|
||||
@@ -45,11 +48,17 @@ pub fn header_style() -> Style {
|
||||
|
||||
pub fn highlight_style() -> Style {
|
||||
Style::default()
|
||||
.bg(VINYL_2)
|
||||
.bg(SELECTION)
|
||||
.fg(PAPER)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
/// The cursor row in a table. No foreground: a table row already colours its
|
||||
/// own cells, and setting one here would flatten them all to a single tone.
|
||||
pub fn row_highlight_style() -> Style {
|
||||
Style::default().bg(SELECTION).add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
pub fn success_style() -> Style {
|
||||
Style::default().fg(COLOR_SUCCESS)
|
||||
}
|
||||
@@ -252,6 +261,13 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_cursor_row_tints_the_background_without_recolouring_cells() {
|
||||
let cursor = row_highlight_style();
|
||||
assert_eq!(cursor.fg, None);
|
||||
assert_eq!(cursor.bg, Some(SELECTION));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn progress_bar_splits_fill_and_track() {
|
||||
let bar = format_progress_bar(0.25, 20, 25);
|
||||
|
||||
Reference in New Issue
Block a user