use crate::app::{App, ViewMode};
use crate::storage::VisualStyle;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
Frame,
};
pub fn render(f: &mut Frame, app: &App) {
let size = f.area();
if size.width < 30 || size.height < 8 {
return;
}
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Length(1), Constraint::Fill(1), Constraint::Length(1), Constraint::Length(2), Constraint::Length(1), ])
.split(size);
render_top_bar(f, app, chunks[0]);
match app.view_mode {
ViewMode::Search => render_search_view(f, app, chunks[2]),
ViewMode::Queue => render_queue_view(f, app, chunks[2]),
ViewMode::Playlists => render_playlists_view(f, app, chunks[2]),
ViewMode::PlaylistDetail => render_playlist_detail_view(f, app, chunks[2]),
ViewMode::Favorites => render_favorites_view(f, app, chunks[2]),
ViewMode::History => render_history_view(f, app, chunks[2]),
ViewMode::UserProfile => render_user_profile_view(f, app, chunks[2]),
ViewMode::Lyrics => render_lyrics_view(f, app, chunks[2]),
ViewMode::Settings => render_settings_view(f, app, chunks[2]),
}
render_player_tape(f, app, chunks[4]);
render_bottom_bar(f, app, chunks[5]);
if let Some(chars) = &app.modal_input {
render_input_modal(f, app, chars, "New Playlist Name:");
} else if let Some(target_track) = &app.modal_target_track {
render_add_to_playlist_modal(f, app, target_track);
}
}
fn format_duration(seconds: f64) -> String {
let s = seconds.max(0.0) as u64;
let m = s / 60;
let rem_s = s % 60;
format!("{}:{:02}", m, rem_s)
}
fn render_top_bar(f: &mut Frame, app: &App, area: Rect) {
let accent = app.theme_accent();
let current_view = app.view_mode.name().to_lowercase();
let mut spans = vec![
Span::styled(format!(" {} ", current_view), Style::default().fg(accent).add_modifier(Modifier::BOLD)),
Span::styled("· ", Style::default().fg(Color::Rgb(50, 50, 50))),
Span::styled(format!("vol {}% ", app.volume), Style::default().fg(Color::Rgb(110, 110, 110))),
];
if app.shuffle {
spans.push(Span::styled("· ", Style::default().fg(Color::Rgb(50, 50, 50))));
spans.push(Span::styled("shuffle ", Style::default().fg(accent)));
}
if app.repeat != crate::app::RepeatMode::Off {
spans.push(Span::styled("· ", Style::default().fg(Color::Rgb(50, 50, 50))));
spans.push(Span::styled(
format!("rep:{} ", app.repeat.name().to_lowercase()),
Style::default().fg(accent),
));
}
if let Some((msg, _)) = &app.status_message {
spans.push(Span::styled("· ", Style::default().fg(Color::Rgb(50, 50, 50))));
spans.push(Span::styled(
msg.as_str(),
Style::default().fg(Color::Rgb(120, 120, 120)),
));
}
let p = Paragraph::new(Line::from(spans)).alignment(Alignment::Left);
f.render_widget(p, area);
}
fn render_search_view(f: &mut Frame, app: &App, area: Rect) {
let accent = app.theme_accent();
let search_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Length(1), Constraint::Fill(1), ])
.split(area);
let prompt_icon = if app.is_editing_search { "⌕ " } else { "· " };
let prompt_color = if app.is_editing_search { accent } else { Color::Rgb(90, 90, 90) };
let mut input_spans = vec![
Span::styled(format!(" {} ", prompt_icon), Style::default().fg(prompt_color)),
];
if app.query_chars.is_empty() && !app.is_editing_search {
input_spans.push(Span::styled(
"press 'i' or '/' to search SoundCloud...",
Style::default().fg(Color::Rgb(70, 70, 70)),
));
} else {
let cursor_pos = app.input_cursor;
for (idx, &ch) in app.query_chars.iter().enumerate() {
if app.is_editing_search && idx == cursor_pos {
input_spans.push(Span::styled(
ch.to_string(),
Style::default().fg(Color::Black).bg(accent).add_modifier(Modifier::BOLD),
));
} else {
input_spans.push(Span::styled(ch.to_string(), Style::default().fg(Color::White)));
}
}
if app.is_editing_search && cursor_pos == app.query_chars.len() {
input_spans.push(Span::styled(
" ",
Style::default().bg(accent),
));
}
}
let input_p = Paragraph::new(Line::from(input_spans));
f.render_widget(input_p, search_layout[0]);
if app.is_searching {
let loading = Paragraph::new(Line::from(vec![
Span::styled("⚡ ", Style::default().fg(accent)),
Span::styled("searching tracks...", Style::default().fg(Color::Rgb(120, 120, 120))),
]))
.alignment(Alignment::Center);
f.render_widget(loading, search_layout[2]);
return;
}
if app.search_results.is_empty() {
let empty_msg = Paragraph::new(vec![
Line::from(""),
Line::from(Span::styled(
"type any song or artist and hit enter",
Style::default().fg(Color::Rgb(65, 65, 65)),
)),
])
.alignment(Alignment::Center);
f.render_widget(empty_msg, search_layout[2]);
return;
}
render_tracks_list(f, &app.search_results, app.search_cursor, accent, search_layout[2], app);
}
fn render_tracks_list(
f: &mut Frame,
tracks: &[crate::storage::TrackItem],
cursor: usize,
accent: Color,
area: Rect,
app: &App,
) {
let max_lines = area.height as usize;
if max_lines == 0 {
return;
}
let scroll_offset = if cursor >= max_lines {
cursor - max_lines + 1
} else {
0
};
let mut lines = Vec::new();
for (idx, track) in tracks.iter().enumerate().skip(scroll_offset).take(max_lines) {
let is_selected = idx == cursor;
let is_playing = app
.current_track()
.map(|t| t.url == track.url)
.unwrap_or(false);
let is_liked = app.store.is_liked(&track.url);
let cursor_glyph = if is_selected {
"› "
} else if is_playing {
"▶ "
} else {
" "
};
let like_icon = if is_liked { "♥ " } else { " " };
let title_style = if is_selected {
Style::default().fg(Color::White).add_modifier(Modifier::BOLD)
} else if is_playing {
Style::default().fg(accent).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Rgb(140, 140, 140))
};
let artist_style = if is_selected {
Style::default().fg(accent)
} else {
Style::default().fg(Color::Rgb(70, 70, 70))
};
let dur = if track.duration > 0.0 {
format_duration(track.duration)
} else {
"".to_string()
};
lines.push(Line::from(vec![
Span::styled(format!(" {}", cursor_glyph), Style::default().fg(if is_selected { accent } else { Color::Rgb(80, 80, 80) })),
Span::styled(like_icon, Style::default().fg(if is_liked { Color::Rgb(220, 70, 70) } else { Color::Rgb(40, 40, 40) })),
Span::styled(format!("{:<42}", truncate_str(&track.title, 40)), title_style),
Span::styled(format!(" {:<24}", truncate_str(&track.artist, 22)), artist_style),
Span::styled(format!("{:>7}", dur), Style::default().fg(Color::Rgb(65, 65, 65))),
]));
}
let p = Paragraph::new(lines);
f.render_widget(p, area);
}
fn truncate_str(s: &str, max_len: usize) -> String {
if s.chars().count() > max_len {
let truncated: String = s.chars().take(max_len.saturating_sub(1)).collect();
format!("{}…", truncated)
} else {
s.to_string()
}
}
fn render_queue_view(f: &mut Frame, app: &App, area: Rect) {
let accent = app.theme_accent();
if app.queue.is_empty() {
let empty = Paragraph::new(vec![
Line::from(""),
Line::from(Span::styled("queue is empty", Style::default().fg(Color::Rgb(80, 80, 80)))),
Line::from(Span::styled("press 'a' to enqueue tracks", Style::default().fg(Color::Rgb(50, 50, 50)))),
])
.alignment(Alignment::Center);
f.render_widget(empty, area);
return;
}
render_tracks_list(f, &app.queue, app.queue_cursor, accent, area, app);
}
fn render_playlists_view(f: &mut Frame, app: &App, area: Rect) {
let accent = app.theme_accent();
if app.store.playlists.is_empty() {
let empty = Paragraph::new(vec![
Line::from(""),
Line::from(Span::styled("no saved playlists", Style::default().fg(Color::Rgb(80, 80, 80)))),
Line::from(Span::styled("paste any soundcloud playlist link in search to load", Style::default().fg(Color::Rgb(50, 50, 50)))),
])
.alignment(Alignment::Center);
f.render_widget(empty, area);
return;
}
let max_lines = area.height as usize;
if max_lines == 0 {
return;
}
let cursor = app.playlists_cursor;
let scroll_offset = if cursor >= max_lines {
cursor - max_lines + 1
} else {
0
};
let mut lines = Vec::new();
for (idx, pl) in app.store.playlists.iter().enumerate().skip(scroll_offset).take(max_lines) {
let is_selected = idx == cursor;
let prefix = if is_selected { " → " } else { " " };
let title_style = if is_selected {
Style::default().fg(Color::White).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Rgb(150, 150, 150))
};
lines.push(Line::from(vec![
Span::styled(prefix, Style::default().fg(if is_selected { accent } else { Color::Rgb(60, 60, 60) })),
Span::styled("≡ ", Style::default().fg(accent)),
Span::styled(format!("{:<38}", truncate_str(&pl.title, 36)), title_style),
Span::styled(format!(" {:<20}", truncate_str(&pl.author, 18)), Style::default().fg(Color::Rgb(80, 80, 80))),
Span::styled(format!("{:>6} tracks", pl.track_count), Style::default().fg(Color::Rgb(65, 65, 65))),
]));
}
let p = Paragraph::new(lines);
f.render_widget(p, area);
}
fn render_playlist_detail_view(f: &mut Frame, app: &App, area: Rect) {
let accent = app.theme_accent();
let playlist = match app.selected_playlist_idx.and_then(|i| app.store.playlists.get(i)) {
Some(p) => p,
None => {
let empty = Paragraph::new("Playlist not found").alignment(Alignment::Center);
f.render_widget(empty, area);
return;
}
};
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Length(1), Constraint::Fill(1), ])
.split(area);
let header = Paragraph::new(Line::from(vec![
Span::styled("≡ ", Style::default().fg(accent)),
Span::styled(&playlist.title, Style::default().fg(Color::White).add_modifier(Modifier::BOLD)),
Span::styled(format!(" by {} • {} tracks", playlist.author, playlist.track_count), Style::default().fg(Color::Rgb(90, 90, 90))),
Span::styled(" (esc to back)", Style::default().fg(Color::Rgb(60, 60, 60))),
]));
f.render_widget(header, chunks[0]);
render_tracks_list(f, &playlist.tracks, app.playlist_detail_cursor, accent, chunks[2], app);
}
fn render_favorites_view(f: &mut Frame, app: &App, area: Rect) {
let accent = app.theme_accent();
if app.store.liked.is_empty() {
let empty = Paragraph::new(vec![
Line::from(""),
Line::from(Span::styled("no favorites", Style::default().fg(Color::Rgb(80, 80, 80)))),
Line::from(Span::styled("press 'l' while playing to add", Style::default().fg(Color::Rgb(50, 50, 50)))),
])
.alignment(Alignment::Center);
f.render_widget(empty, area);
return;
}
render_tracks_list(f, &app.store.liked, app.favorites_cursor, accent, area, app);
}
fn render_history_view(f: &mut Frame, app: &App, area: Rect) {
let accent = app.theme_accent();
if app.store.history.is_empty() {
let empty = Paragraph::new(vec![
Line::from(""),
Line::from(Span::styled("no history", Style::default().fg(Color::Rgb(80, 80, 80)))),
])
.alignment(Alignment::Center);
f.render_widget(empty, area);
return;
}
let tracks: Vec<_> = app.store.history.iter().map(|h| h.track.clone()).collect();
render_tracks_list(f, &tracks, app.history_cursor, accent, area, app);
}
fn render_settings_view(f: &mut Frame, app: &App, size: Rect) {
let v = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Fill(1), Constraint::Length(8), Constraint::Fill(1)])
.split(size);
let accent = app.theme_accent();
let mut lines = Vec::new();
lines.push(Line::from(Span::styled("settings", Style::default().fg(accent).add_modifier(Modifier::BOLD))));
lines.push(Line::from(Span::styled("────────────", Style::default().fg(Color::Rgb(40, 40, 40)))));
let items = [
("theme", app.store.config.theme.name().to_string()),
("style", app.store.config.visual.name().to_string()),
("volume", format!("{}%", app.store.config.default_volume)),
("discord rpc", if app.store.config.discord_rpc { "on".to_string() } else { "off".to_string() }),
];
for (idx, (label, val)) in items.iter().enumerate() {
let is_selected = idx == app.settings_cursor;
let prefix = if is_selected { " → " } else { " " };
let style_label = if is_selected {
Style::default().fg(Color::White).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Rgb(100, 100, 100))
};
let style_val = if is_selected {
Style::default().fg(accent).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Rgb(120, 120, 120))
};
lines.push(Line::from(vec![
Span::styled(prefix, Style::default().fg(if is_selected { accent } else { Color::Rgb(50, 50, 50) })),
Span::styled(format!("{:<16}", label), style_label),
Span::styled(val.as_str(), style_val),
]));
}
let p = Paragraph::new(lines).alignment(Alignment::Center);
f.render_widget(p, v[1]);
}
fn render_player_tape(f: &mut Frame, app: &App, area: Rect) {
let accent = app.theme_accent();
let width = area.width as usize;
if width < 20 {
return;
}
let track_title = app
.current_track()
.map(|t| format!("{} · {}", t.title, t.artist))
.unwrap_or_else(|| "idle".to_string());
let state_icon = if app.current_track().is_none() {
"◼"
} else if app.is_paused {
"⏸"
} else {
"▶"
};
let (cur_str, tot_str, progress_ratio) = if app.total_duration > 0.0 {
let ratio = (app.current_pos / app.total_duration).clamp(0.0, 1.0);
(
format_duration(app.current_pos),
format_duration(app.total_duration),
ratio,
)
} else if app.current_pos > 0.0 {
(format_duration(app.current_pos), "--:--".to_string(), 0.0)
} else {
("0:00".to_string(), "0:00".to_string(), 0.0)
};
let bar_width = width.saturating_sub(24).clamp(12, 70);
let filled_len = (bar_width as f64 * progress_ratio) as usize;
let waveform_blocks = [' ', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
let mut tape_spans = Vec::new();
tape_spans.push(Span::styled(format!("{:>5} ", cur_str), Style::default().fg(Color::Rgb(90, 90, 90))));
if !app.current_waveform.is_empty() {
let total_samples = app.current_waveform.len();
for i in 0..bar_width {
let sample_idx = (i * total_samples) / bar_width;
let val = app.current_waveform.get(sample_idx).copied().unwrap_or(0) as usize;
let glyph_idx = (val * (waveform_blocks.len() - 1)) / 100;
let glyph = waveform_blocks[glyph_idx.min(waveform_blocks.len() - 1)];
let style = if i < filled_len {
Style::default().fg(accent).add_modifier(Modifier::BOLD)
} else if i == filled_len {
Style::default().fg(Color::White).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Rgb(55, 55, 55))
};
tape_spans.push(Span::styled(glyph.to_string(), style));
}
} else {
let bar_str: String = match app.store.config.visual {
VisualStyle::Tape => {
let mut s = String::new();
for i in 0..bar_width {
if i < filled_len {
s.push('━');
} else if i == filled_len {
s.push('●');
} else {
s.push('─');
}
}
s
}
VisualStyle::Blocky => {
let mut s = String::new();
for i in 0..bar_width {
if i < filled_len {
s.push('█');
} else if i == filled_len {
s.push('▓');
} else {
s.push('░');
}
}
s
}
VisualStyle::Minimal => {
let mut s = String::new();
for i in 0..bar_width {
if i < filled_len {
s.push('·');
} else if i == filled_len {
s.push('◆');
} else {
s.push(' ');
}
}
s
}
};
tape_spans.push(Span::styled(bar_str, Style::default().fg(accent)));
}
tape_spans.push(Span::styled(format!(" {:<5}", tot_str), Style::default().fg(Color::Rgb(90, 90, 90))));
let line1 = Line::from(vec![
Span::styled(format!("{} ", state_icon), Style::default().fg(accent)),
Span::styled(
truncate_str(&track_title, width.saturating_sub(10)),
Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
),
]);
let line2 = Line::from(tape_spans);
let p = Paragraph::new(vec![line1, line2]).alignment(Alignment::Center);
f.render_widget(p, area);
}
fn render_user_profile_view(f: &mut Frame, app: &App, area: Rect) {
let accent = app.theme_accent();
let profile = match &app.current_user_profile {
Some(p) => p,
None => {
let empty = Paragraph::new("No profile loaded. Press 'u' on any track to view author").alignment(Alignment::Center);
f.render_widget(empty, area);
return;
}
};
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Length(1), Constraint::Fill(1), ])
.split(area);
let desc = profile.description.as_deref().unwrap_or("SoundCloud Artist");
let header = Paragraph::new(Line::from(vec![
Span::styled("@", Style::default().fg(accent).add_modifier(Modifier::BOLD)),
Span::styled(&profile.username, Style::default().fg(Color::White).add_modifier(Modifier::BOLD)),
Span::styled(format!(" • {} followers • {} tracks", profile.followers_count, profile.track_count), Style::default().fg(Color::Rgb(100, 100, 100))),
Span::styled(format!(" • {}", truncate_str(desc, 30)), Style::default().fg(Color::Rgb(70, 70, 70))),
Span::styled(" (esc to back)", Style::default().fg(Color::Rgb(50, 50, 50))),
]));
f.render_widget(header, chunks[0]);
render_tracks_list(f, &profile.tracks, app.user_profile_cursor, accent, chunks[2], app);
}
fn render_lyrics_view(f: &mut Frame, app: &App, area: Rect) {
let accent = app.theme_accent();
let current_track = match app.current_track() {
Some(t) => t,
None => {
let empty = Paragraph::new("No track playing").alignment(Alignment::Center);
f.render_widget(empty, area);
return;
}
};
let lyrics = match &app.current_lyrics {
Some(l) if l.track_id == current_track.id => l,
_ => {
let loading = Paragraph::new(vec![
Line::from(""),
Line::from(Span::styled("♪ searching lyrics on LRCLIB...", Style::default().fg(Color::Rgb(100, 100, 100)))),
Line::from(""),
Line::from(Span::styled(format!("{} - {}", current_track.artist, current_track.title), Style::default().fg(Color::Rgb(60, 60, 60)))),
]).alignment(Alignment::Center);
f.render_widget(loading, area);
return;
}
};
let cur_time = app.current_pos;
let max_lines = area.height as usize;
if max_lines == 0 {
return;
}
if !lyrics.lines.is_empty() {
let current_line_idx = lyrics
.lines
.iter()
.rposition(|line| line.time_secs <= cur_time)
.unwrap_or(0);
let half_screen = max_lines / 2;
let start_idx = current_line_idx.saturating_sub(half_screen);
let mut lines = Vec::new();
for (idx, line) in lyrics.lines.iter().enumerate().skip(start_idx).take(max_lines) {
let is_current = idx == current_line_idx;
if is_current {
lines.push(Line::from(Span::styled(
format!("▶ {}", line.text),
Style::default().fg(accent).add_modifier(Modifier::BOLD),
)));
} else {
let diff = (idx as isize - current_line_idx as isize).abs();
let dim_val = match diff {
1 => 130,
2 => 100,
3 => 70,
_ => 45,
};
lines.push(Line::from(Span::styled(
format!(" {}", line.text),
Style::default().fg(Color::Rgb(dim_val, dim_val, dim_val)),
)));
}
}
let p = Paragraph::new(lines).alignment(Alignment::Center);
f.render_widget(p, area);
} else if let Some(plain) = &lyrics.plain_lyrics {
let lines: Vec<Line> = plain.lines().take(max_lines).map(|l| Line::from(Span::styled(l, Style::default().fg(Color::Rgb(140, 140, 140))))).collect();
let p = Paragraph::new(lines).alignment(Alignment::Center);
f.render_widget(p, area);
} else {
let empty = Paragraph::new(vec![
Line::from(""),
Line::from(Span::styled("No lyrics found for this track", Style::default().fg(Color::Rgb(80, 80, 80)))),
]).alignment(Alignment::Center);
f.render_widget(empty, area);
}
}
fn render_input_modal(f: &mut Frame, app: &App, input_chars: &[char], prompt: &str) {
let size = f.area();
let accent = app.theme_accent();
let text: String = input_chars.iter().collect();
let area = centered_rect(50, 20, size);
f.render_widget(ratatui::widgets::Clear, area);
let block = ratatui::widgets::Block::default()
.borders(ratatui::widgets::Borders::ALL)
.border_style(Style::default().fg(accent))
.title(Span::styled(" Create Playlist ", Style::default().fg(accent).add_modifier(Modifier::BOLD)));
f.render_widget(block, area);
let inner = area.inner(ratatui::layout::Margin { horizontal: 2, vertical: 1 });
let p = Paragraph::new(vec![
Line::from(Span::styled(prompt, Style::default().fg(Color::Rgb(140, 140, 140)))),
Line::from(""),
Line::from(vec![
Span::styled("▸ ", Style::default().fg(accent)),
Span::styled(&text, Style::default().fg(Color::White).add_modifier(Modifier::BOLD)),
Span::styled("▍", Style::default().fg(accent)),
]),
Line::from(""),
Line::from(Span::styled("Enter: Confirm • Esc: Cancel", Style::default().fg(Color::Rgb(80, 80, 80)))),
]);
f.render_widget(p, inner);
}
fn render_add_to_playlist_modal(f: &mut Frame, app: &App, target_track: &crate::storage::TrackItem) {
let size = f.area();
let accent = app.theme_accent();
let area = centered_rect(55, 40, size);
f.render_widget(ratatui::widgets::Clear, area);
let title_str = format!(" Add to Playlist: {} ", truncate_str(&target_track.title, 25));
let block = ratatui::widgets::Block::default()
.borders(ratatui::widgets::Borders::ALL)
.border_style(Style::default().fg(accent))
.title(Span::styled(title_str, Style::default().fg(accent).add_modifier(Modifier::BOLD)));
f.render_widget(block, area);
let inner = area.inner(ratatui::layout::Margin { horizontal: 2, vertical: 1 });
let mut lines = Vec::new();
lines.push(Line::from(Span::styled("Select target playlist:", Style::default().fg(Color::Rgb(120, 120, 120)))));
lines.push(Line::from(""));
for (idx, pl) in app.store.playlists.iter().enumerate() {
let is_sel = idx == app.modal_playlist_cursor;
let prefix = if is_sel { " ▸ " } else { " " };
let style = if is_sel {
Style::default().fg(Color::White).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Rgb(130, 130, 130))
};
lines.push(Line::from(vec![
Span::styled(prefix, Style::default().fg(if is_sel { accent } else { Color::Rgb(50, 50, 50) })),
Span::styled(format!("{:<28}", truncate_str(&pl.title, 26)), style),
Span::styled(format!("({} tracks)", pl.track_count), Style::default().fg(Color::Rgb(70, 70, 70))),
]));
}
lines.push(Line::from(""));
lines.push(Line::from(Span::styled("Enter: Add • Esc: Cancel", Style::default().fg(Color::Rgb(80, 80, 80)))));
let p = Paragraph::new(lines);
f.render_widget(p, inner);
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}
fn render_bottom_bar(f: &mut Frame, _app: &App, area: Rect) {
let hints = vec![
Span::styled(" space ", Style::default().fg(Color::Rgb(120, 120, 120))),
Span::styled("play/pause ", Style::default().fg(Color::Rgb(70, 70, 70))),
Span::styled("· ", Style::default().fg(Color::Rgb(40, 40, 40))),
Span::styled("enter ", Style::default().fg(Color::Rgb(120, 120, 120))),
Span::styled("play ", Style::default().fg(Color::Rgb(70, 70, 70))),
Span::styled("· ", Style::default().fg(Color::Rgb(40, 40, 40))),
Span::styled("tab ", Style::default().fg(Color::Rgb(120, 120, 120))),
Span::styled("views ", Style::default().fg(Color::Rgb(70, 70, 70))),
Span::styled("· ", Style::default().fg(Color::Rgb(40, 40, 40))),
Span::styled("a ", Style::default().fg(Color::Rgb(120, 120, 120))),
Span::styled("queue ", Style::default().fg(Color::Rgb(70, 70, 70))),
Span::styled("· ", Style::default().fg(Color::Rgb(40, 40, 40))),
Span::styled("s ", Style::default().fg(Color::Rgb(120, 120, 120))),
Span::styled("shuf ", Style::default().fg(Color::Rgb(70, 70, 70))),
Span::styled("· ", Style::default().fg(Color::Rgb(40, 40, 40))),
Span::styled("r ", Style::default().fg(Color::Rgb(120, 120, 120))),
Span::styled("repeat ", Style::default().fg(Color::Rgb(70, 70, 70))),
Span::styled("· ", Style::default().fg(Color::Rgb(40, 40, 40))),
Span::styled("y ", Style::default().fg(Color::Rgb(120, 120, 120))),
Span::styled("lyrics ", Style::default().fg(Color::Rgb(70, 70, 70))),
Span::styled("· ", Style::default().fg(Color::Rgb(40, 40, 40))),
Span::styled("q ", Style::default().fg(Color::Rgb(120, 120, 120))),
Span::styled("quit", Style::default().fg(Color::Rgb(70, 70, 70))),
];
let p = Paragraph::new(Line::from(hints)).alignment(Alignment::Left);
f.render_widget(p, area);
}