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};
|
||||
|
||||
/// Vertical block glyphs by eighths, index 0 = empty. Maps a bar level
|
||||
/// in `[0, 1]` to a height.
|
||||
/// Vertical block glyphs by eighths, index 0 = empty, 8 = full cell.
|
||||
const BLOCKS: [char; 9] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
||||
|
||||
fn bar_glyph(level: f32) -> char {
|
||||
let idx = (level.clamp(0.0, 1.0) * 8.0).round() as usize;
|
||||
BLOCKS[idx.min(8)]
|
||||
/// Fixed height of the now-playing info block (4 text lines + border)
|
||||
/// when the spectrum is shown; the spectrum then fills the rest.
|
||||
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 {
|
||||
|
|
@ -100,17 +125,22 @@ impl NowPlaying {
|
|||
}
|
||||
|
||||
pub fn render(&self, f: &mut Frame, area: Rect) {
|
||||
// Info block fills, then a fixed progress row and (when enabled)
|
||||
// a fixed spectrum row. Length(1) is hard-fixed so the rows are
|
||||
// never squeezed out by the block above them.
|
||||
let spectrum_row = if self.spectrum_enabled { 1 } else { 0 };
|
||||
// With the spectrum on, the info block takes a fixed height, the
|
||||
// progress a fixed row, and the spectrum fills whatever is left
|
||||
// (`Min(0)`) so the bars are as tall as the pane allows. With it
|
||||
// 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()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Min(3),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(spectrum_row),
|
||||
])
|
||||
.constraints(constraints)
|
||||
.split(area);
|
||||
|
||||
let media_info_text = if let Some(track) = &self.track {
|
||||
|
|
@ -221,23 +251,16 @@ impl NowPlaying {
|
|||
f.render_widget(time_p, elapsed_layout[1]);
|
||||
}
|
||||
|
||||
// The spectrum row: one block glyph per bar, in the accent color.
|
||||
// Stretched to the pane width so it fills the row regardless of
|
||||
// the server's bin count.
|
||||
// The spectrum: full-height accent bars filling the region left
|
||||
// below the progress. Columns stretch across the pane width
|
||||
// regardless of the server's bin count.
|
||||
if self.spectrum_enabled && !self.spectrum.is_empty() {
|
||||
let width = now_playing_layout[2].width as usize;
|
||||
let bars: String = (0..width)
|
||||
.map(|col| {
|
||||
// Nearest-neighbor map from column to bin.
|
||||
let bin = col * self.spectrum.len() / width.max(1);
|
||||
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]);
|
||||
let area = now_playing_layout[2];
|
||||
let (width, height) = (area.width as usize, area.height as usize);
|
||||
if width > 0 && height > 0 {
|
||||
let lines = spectrum_lines(&self.spectrum, width, height);
|
||||
f.render_widget(Paragraph::new(lines), area);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -292,24 +315,41 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn bar_glyph_maps_level_to_height() {
|
||||
assert_eq!(bar_glyph(0.0), ' ');
|
||||
assert_eq!(bar_glyph(1.0), '█');
|
||||
assert_eq!(bar_glyph(-5.0), ' ', "clamped");
|
||||
assert_eq!(bar_glyph(2.0), '█', "clamped");
|
||||
assert!(BLOCKS.contains(&bar_glyph(0.5)));
|
||||
fn spectrum_lines_fill_full_height_columns() {
|
||||
// A full-level bin fills every row of its column with the full
|
||||
// block; a zero bin leaves every row blank.
|
||||
let lines = spectrum_lines(&[1.0, 0.0], 2, 4);
|
||||
assert_eq!(lines.len(), 4, "one line per row");
|
||||
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]
|
||||
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);
|
||||
pane.update_spectrum(vec![1.0; 24]);
|
||||
let rows = rendered_rows(&pane);
|
||||
// A full-height frame draws the tallest block somewhere.
|
||||
assert!(
|
||||
rows.iter().any(|r| r.contains('█')),
|
||||
"expected spectrum bars, got: {rows:?}"
|
||||
);
|
||||
// Full-level bars fill several rows with the full block.
|
||||
let full_rows = rows.iter().filter(|r| r.contains('█')).count();
|
||||
assert!(full_rows >= 2, "expected tall spectrum bars, got: {rows:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in New Issue