Make the TUI spectrum bars fill the height below the progress
The bars were a single glyph row because the info block grew to fill the now-playing pane. The info block now takes a fixed height and the spectrum fills the remaining rows, drawn as full-height columns (full blocks stacked from the bottom, a partial block for the fractional top cell) instead of one row of sub-cell glyphs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0b9970b550
commit
b3fec6d3c0
|
|
@ -14,13 +14,38 @@ use ratatui::{
|
||||||
|
|
||||||
use super::{COLOR_PRIMARY, COLOR_SECONDARY};
|
use super::{COLOR_PRIMARY, COLOR_SECONDARY};
|
||||||
|
|
||||||
/// Vertical block glyphs by eighths, index 0 = empty. Maps a bar level
|
/// Vertical block glyphs by eighths, index 0 = empty, 8 = full cell.
|
||||||
/// in `[0, 1]` to a height.
|
|
||||||
const BLOCKS: [char; 9] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
const BLOCKS: [char; 9] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
||||||
|
|
||||||
fn bar_glyph(level: f32) -> char {
|
/// Fixed height of the now-playing info block (4 text lines + border)
|
||||||
let idx = (level.clamp(0.0, 1.0) * 8.0).round() as usize;
|
/// when the spectrum is shown; the spectrum then fills the rest.
|
||||||
BLOCKS[idx.min(8)]
|
const INFO_HEIGHT: u16 = 6;
|
||||||
|
|
||||||
|
/// Renders `bins` as full-height vertical bars filling a `width`×`height`
|
||||||
|
/// area: one `Line` per row, top row first. Each column maps to a bin;
|
||||||
|
/// its level in `[0, 1]` fills from the bottom, using partial block
|
||||||
|
/// glyphs for the topmost fractional cell. Pure, so it is unit-tested.
|
||||||
|
fn spectrum_lines(bins: &[f32], width: usize, height: usize) -> Vec<Line<'static>> {
|
||||||
|
(0..height)
|
||||||
|
.map(|row| {
|
||||||
|
// Row 0 is the top; count cells up from the bottom.
|
||||||
|
let from_bottom = height - 1 - row;
|
||||||
|
let cells: String = (0..width)
|
||||||
|
.map(|col| {
|
||||||
|
let bin = if bins.is_empty() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(col * bins.len() / width.max(1)).min(bins.len() - 1)
|
||||||
|
};
|
||||||
|
let level = bins.get(bin).copied().unwrap_or(0.0).clamp(0.0, 1.0);
|
||||||
|
let total_eighths = (level * height as f32 * 8.0).round() as usize;
|
||||||
|
let cell = total_eighths.saturating_sub(from_bottom * 8).min(8);
|
||||||
|
BLOCKS[cell]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Line::from(Span::styled(cells, Style::default().fg(COLOR_PRIMARY)))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NowPlaying {
|
pub struct NowPlaying {
|
||||||
|
|
@ -100,17 +125,22 @@ impl NowPlaying {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&self, f: &mut Frame, area: Rect) {
|
pub fn render(&self, f: &mut Frame, area: Rect) {
|
||||||
// Info block fills, then a fixed progress row and (when enabled)
|
// With the spectrum on, the info block takes a fixed height, the
|
||||||
// a fixed spectrum row. Length(1) is hard-fixed so the rows are
|
// progress a fixed row, and the spectrum fills whatever is left
|
||||||
// never squeezed out by the block above them.
|
// (`Min(0)`) so the bars are as tall as the pane allows. With it
|
||||||
let spectrum_row = if self.spectrum_enabled { 1 } else { 0 };
|
// off, the info block fills as before.
|
||||||
|
let constraints = if self.spectrum_enabled {
|
||||||
|
vec![
|
||||||
|
Constraint::Length(INFO_HEIGHT),
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Min(0),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![Constraint::Min(3), Constraint::Length(1)]
|
||||||
|
};
|
||||||
let now_playing_layout = Layout::default()
|
let now_playing_layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints(constraints)
|
||||||
Constraint::Min(3),
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(spectrum_row),
|
|
||||||
])
|
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
let media_info_text = if let Some(track) = &self.track {
|
let media_info_text = if let Some(track) = &self.track {
|
||||||
|
|
@ -221,23 +251,16 @@ impl NowPlaying {
|
||||||
f.render_widget(time_p, elapsed_layout[1]);
|
f.render_widget(time_p, elapsed_layout[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The spectrum row: one block glyph per bar, in the accent color.
|
// The spectrum: full-height accent bars filling the region left
|
||||||
// Stretched to the pane width so it fills the row regardless of
|
// below the progress. Columns stretch across the pane width
|
||||||
// the server's bin count.
|
// regardless of the server's bin count.
|
||||||
if self.spectrum_enabled && !self.spectrum.is_empty() {
|
if self.spectrum_enabled && !self.spectrum.is_empty() {
|
||||||
let width = now_playing_layout[2].width as usize;
|
let area = now_playing_layout[2];
|
||||||
let bars: String = (0..width)
|
let (width, height) = (area.width as usize, area.height as usize);
|
||||||
.map(|col| {
|
if width > 0 && height > 0 {
|
||||||
// Nearest-neighbor map from column to bin.
|
let lines = spectrum_lines(&self.spectrum, width, height);
|
||||||
let bin = col * self.spectrum.len() / width.max(1);
|
f.render_widget(Paragraph::new(lines), area);
|
||||||
bar_glyph(self.spectrum[bin.min(self.spectrum.len() - 1)])
|
}
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let spectrum_p = Paragraph::new(Line::from(Span::styled(
|
|
||||||
bars,
|
|
||||||
Style::default().fg(COLOR_PRIMARY),
|
|
||||||
)));
|
|
||||||
f.render_widget(spectrum_p, now_playing_layout[2]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -292,24 +315,41 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bar_glyph_maps_level_to_height() {
|
fn spectrum_lines_fill_full_height_columns() {
|
||||||
assert_eq!(bar_glyph(0.0), ' ');
|
// A full-level bin fills every row of its column with the full
|
||||||
assert_eq!(bar_glyph(1.0), '█');
|
// block; a zero bin leaves every row blank.
|
||||||
assert_eq!(bar_glyph(-5.0), ' ', "clamped");
|
let lines = spectrum_lines(&[1.0, 0.0], 2, 4);
|
||||||
assert_eq!(bar_glyph(2.0), '█', "clamped");
|
assert_eq!(lines.len(), 4, "one line per row");
|
||||||
assert!(BLOCKS.contains(&bar_glyph(0.5)));
|
let text: Vec<String> = lines
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.spans.iter().map(|s| s.content.as_ref()).collect())
|
||||||
|
.collect();
|
||||||
|
// Column 0 (level 1.0) is full in every row; column 1 (0.0) empty.
|
||||||
|
assert!(text.iter().all(|row| row.starts_with('█')), "{text:?}");
|
||||||
|
assert!(text.iter().all(|row| row.ends_with(' ')), "{text:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_spectrum_row_renders_block_glyphs_when_enabled() {
|
fn spectrum_lines_grow_from_the_bottom() {
|
||||||
|
// Half level over 4 rows ≈ 16 eighths → the bottom two rows fill.
|
||||||
|
let lines = spectrum_lines(&[0.5], 1, 4);
|
||||||
|
let col: Vec<char> = lines
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.spans[0].content.chars().next().unwrap())
|
||||||
|
.collect();
|
||||||
|
// Top rows empty, bottom rows full — bars rise from the floor.
|
||||||
|
assert_eq!(col[0], ' ', "top empty: {col:?}");
|
||||||
|
assert_eq!(col[3], '█', "bottom full: {col:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_spectrum_renders_block_glyphs_when_enabled() {
|
||||||
let mut pane = now_playing(10_000, 60_000);
|
let mut pane = now_playing(10_000, 60_000);
|
||||||
pane.update_spectrum(vec![1.0; 24]);
|
pane.update_spectrum(vec![1.0; 24]);
|
||||||
let rows = rendered_rows(&pane);
|
let rows = rendered_rows(&pane);
|
||||||
// A full-height frame draws the tallest block somewhere.
|
// Full-level bars fill several rows with the full block.
|
||||||
assert!(
|
let full_rows = rows.iter().filter(|r| r.contains('█')).count();
|
||||||
rows.iter().any(|r| r.contains('█')),
|
assert!(full_rows >= 2, "expected tall spectrum bars, got: {rows:?}");
|
||||||
"expected spectrum bars, got: {rows:?}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue