diff --git a/soulseek-rs/src/ui/main_tui/render.rs b/soulseek-rs/src/ui/main_tui/render.rs index a2ff1fb..dc3c632 100644 --- a/soulseek-rs/src/ui/main_tui/render.rs +++ b/soulseek-rs/src/ui/main_tui/render.rs @@ -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 = Vec::new(); + let mut lines: Vec = 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)); diff --git a/soulseek-rs/src/ui/panes/browse_pane.rs b/soulseek-rs/src/ui/panes/browse_pane.rs index 9ea104c..c5348d4 100644 --- a/soulseek-rs/src/ui/panes/browse_pane.rs +++ b/soulseek-rs/src/ui/panes/browse_pane.rs @@ -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); diff --git a/soulseek-rs/src/ui/panes/download_info_pane.rs b/soulseek-rs/src/ui/panes/download_info_pane.rs index 1255585..e88da88 100644 --- a/soulseek-rs/src/ui/panes/download_info_pane.rs +++ b/soulseek-rs/src/ui/panes/download_info_pane.rs @@ -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 { diff --git a/soulseek-rs/src/ui/panes/downloads_pane.rs b/soulseek-rs/src/ui/panes/downloads_pane.rs index aa0cfa9..92394cc 100644 --- a/soulseek-rs/src/ui/panes/downloads_pane.rs +++ b/soulseek-rs/src/ui/panes/downloads_pane.rs @@ -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( diff --git a/soulseek-rs/src/ui/panes/results_pane.rs b/soulseek-rs/src/ui/panes/results_pane.rs index f29af08..fc6540a 100644 --- a/soulseek-rs/src/ui/panes/results_pane.rs +++ b/soulseek-rs/src/ui/panes/results_pane.rs @@ -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))); diff --git a/soulseek-rs/src/ui/panes/rooms_pane.rs b/soulseek-rs/src/ui/panes/rooms_pane.rs index 95c9cce..fa5fe49 100644 --- a/soulseek-rs/src/ui/panes/rooms_pane.rs +++ b/soulseek-rs/src/ui/panes/rooms_pane.rs @@ -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); diff --git a/soulseek-rs/src/ui/panes/searches_pane.rs b/soulseek-rs/src/ui/panes/searches_pane.rs index 835dc85..22e525b 100644 --- a/soulseek-rs/src/ui/panes/searches_pane.rs +++ b/soulseek-rs/src/ui/panes/searches_pane.rs @@ -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))); diff --git a/soulseek-rs/src/ui/styles.rs b/soulseek-rs/src/ui/styles.rs index 9d24559..d6ca757 100644 --- a/soulseek-rs/src/ui/styles.rs +++ b/soulseek-rs/src/ui/styles.rs @@ -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);