Files
ferrosonic/src/ui/layout.rs
Jamie Hewitt 12cc70e6ec Initial commit — ferrosonic terminal Subsonic client
Terminal-based Subsonic music client in Rust featuring bit-perfect audio
playback via PipeWire sample rate switching, gapless playback, MPRIS2
desktop integration, cava audio visualizer with theme-matched gradients,
13 built-in color themes with custom TOML theme support, mouse controls,
artist/album browser, playlist support, and play queue management.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 21:43:26 +00:00

113 lines
3.5 KiB
Rust

//! Main layout and rendering
use ratatui::{
layout::{Constraint, Layout},
Frame,
};
use crate::app::state::{AppState, LayoutAreas, Page};
use super::footer::Footer;
use super::header::Header;
use super::pages;
use super::widgets::{CavaWidget, NowPlayingWidget};
/// Draw the entire UI
pub fn draw(frame: &mut Frame, state: &mut AppState) {
let area = frame.area();
let cava_active = state.settings_state.cava_enabled && !state.cava_screen.is_empty();
// Main layout:
// [Header] - 1 line
// [Cava] - ~40% (optional, only when cava is active)
// [Page Content] - flexible
// [Now Playing] - 7 lines
// [Footer] - 1 line
let (header_area, cava_area, content_area, now_playing_area, footer_area) = if cava_active {
let chunks = Layout::vertical([
Constraint::Length(1), // Header
Constraint::Percentage(40), // Cava visualizer — top half-ish
Constraint::Min(10), // Page content
Constraint::Length(7), // Now playing
Constraint::Length(1), // Footer
])
.split(area);
(chunks[0], Some(chunks[1]), chunks[2], chunks[3], chunks[4])
} else {
let chunks = Layout::vertical([
Constraint::Length(1), // Header
Constraint::Min(10), // Page content
Constraint::Length(7), // Now playing
Constraint::Length(1), // Footer
])
.split(area);
(chunks[0], None, chunks[1], chunks[2], chunks[3])
};
// Compute dual-pane splits for pages that use them
let (content_left, content_right) = match state.page {
Page::Artists | Page::Playlists => {
let panes = Layout::horizontal([
Constraint::Percentage(40),
Constraint::Percentage(60),
])
.split(content_area);
(Some(panes[0]), Some(panes[1]))
}
_ => (None, None),
};
// Store layout areas for mouse hit-testing
state.layout = LayoutAreas {
header: header_area,
cava: cava_area,
content: content_area,
now_playing: now_playing_area,
footer: footer_area,
content_left,
content_right,
};
// Render header
let colors = *state.settings_state.theme_colors();
let header = Header::new(state.page, state.now_playing.state, colors);
frame.render_widget(header, header_area);
// Render cava visualizer if active
if let Some(cava_rect) = cava_area {
let cava_widget = CavaWidget::new(&state.cava_screen);
frame.render_widget(cava_widget, cava_rect);
}
// Render current page
match state.page {
Page::Artists => {
pages::artists::render(frame, content_area, state);
}
Page::Queue => {
pages::queue::render(frame, content_area, state);
}
Page::Playlists => {
pages::playlists::render(frame, content_area, state);
}
Page::Server => {
pages::server::render(frame, content_area, state);
}
Page::Settings => {
pages::settings::render(frame, content_area, state);
}
}
// Render now playing
let now_playing = NowPlayingWidget::new(&state.now_playing, colors);
frame.render_widget(now_playing, now_playing_area);
// Render footer
let footer = Footer::new(state.page, colors)
.sample_rate(state.now_playing.sample_rate)
.notification(state.notification.as_ref());
frame.render_widget(footer, footer_area);
}