Krolyxon
Commit 96dd350

rmptuisrcscreenslyrics.rs

132 lines · 4.6 KB · Rust

1//! Screen 7: lyrics of the current song. Also drawn as a pane on the queue.
2
3use crossterm::event::{MouseEvent, MouseEventKind};
4use ratatui::{
5 Frame,
6 layout::{Alignment, Rect},
7 style::Modifier,
8 text::{Line, Span},
9 widgets::Paragraph,
10};
11
12use super::{Ctx, Effect, Screen};
13use crate::{
14 action::Action,
15 lyrics::{Lyrics, LyricsStatus},
16 ui,
17};
18
19#[derive(Debug, Default)]
20pub struct LyricsScreen {
21 /// Scroll offset for plain (unsynced) lyrics.
22 scroll: usize,
23 height: usize,
24 file: Option<String>,
25}
26
27/// Draws the lyrics pane without manual scrolling.
28pub fn draw_lyrics(frame: &mut Frame, area: Rect, ctx: &Ctx, focused: bool) {
29 render(frame, area, ctx, focused, 0);
30}
31
32fn render(frame: &mut Frame, area: Rect, ctx: &Ctx, focused: bool, scroll: usize) -> usize {
33 let theme = ctx.theme();
34 let title = match &ctx.mpd.current {
35 Some(song) => format!(" Lyrics · {} ", ui::song_label(song)),
36 None => " Lyrics ".to_owned(),
37 };
38 let block = ui::pane(title, focused, theme);
39 let inner = block.inner(area);
40 frame.render_widget(block, area);
41 let height = usize::from(inner.height);
42
43 let lyrics = match (&ctx.mpd.current, &ctx.lyrics.status) {
44 (None, _) => {
45 ui::draw_placeholder(frame, inner, &["Nothing is playing"], theme);
46 return height;
47 }
48 (Some(_), LyricsStatus::Found(lyrics)) => lyrics,
49 (Some(_), LyricsStatus::Idle | LyricsStatus::Loading) => {
50 ui::draw_placeholder(frame, inner, &["Looking for lyrics…"], theme);
51 return height;
52 }
53 (Some(_), LyricsStatus::NotFound) => {
54 ui::draw_placeholder(frame, inner, &["No lyrics found"], theme);
55 return height;
56 }
57 (Some(_), LyricsStatus::Failed(error)) => {
58 ui::draw_placeholder(frame, inner, &["Could not fetch lyrics", error], theme);
59 return height;
60 }
61 };
62
63 let current = lyrics.current_line(ctx.mpd.elapsed());
64 let top = match (lyrics, current) {
65 // Keep the current line in the middle.
66 (Lyrics::Synced(_), Some(line)) => line.saturating_sub(height / 2),
67 (Lyrics::Synced(_), None) => 0,
68 (Lyrics::Plain(_), _) => scroll.min(lyrics.len().saturating_sub(1)),
69 };
70 let lines: Vec<Line> = (top..lyrics.len().min(top + height))
71 .map(|i| {
72 let text = lyrics.line(i);
73 let style = match (lyrics, current) {
74 (Lyrics::Synced(_), Some(c)) if i == c => {
75 theme.playing().add_modifier(Modifier::BOLD)
76 }
77 (Lyrics::Synced(_), Some(c)) if i < c => theme.dim(),
78 (Lyrics::Synced(_), None) => theme.dim(),
79 _ => theme.text(),
80 };
81 Line::from(Span::styled(text.to_owned(), style))
82 })
83 .collect();
84 frame.render_widget(Paragraph::new(lines).alignment(Alignment::Center), inner);
85 height
86}
87
88impl Screen for LyricsScreen {
89 fn draw(&mut self, frame: &mut Frame, area: Rect, ctx: &Ctx) {
90 // Plain lyrics start from the top for every song.
91 let file = ctx.mpd.current.as_ref().map(|s| s.file.clone());
92 if file != self.file {
93 self.file = file;
94 self.scroll = 0;
95 }
96 self.height = render(frame, area, ctx, true, self.scroll);
97 }
98
99 fn handle_action(&mut self, action: Action, ctx: &mut Ctx) -> bool {
100 let len = ctx.lyrics.lyrics().map_or(0, Lyrics::len);
101 let page = self.height.max(1);
102 let max = len.saturating_sub(1);
103 self.scroll = match action {
104 Action::Up => self.scroll.saturating_sub(1),
105 Action::Down => (self.scroll + 1).min(max),
106 Action::PageUp => self.scroll.saturating_sub(page),
107 Action::PageDown => (self.scroll + page).min(max),
108 Action::HalfPageUp => self.scroll.saturating_sub(page / 2),
109 Action::HalfPageDown => (self.scroll + page / 2).min(max),
110 Action::Top => 0,
111 Action::Bottom => max,
112 Action::ReloadLyrics => {
113 ctx.effects.push(Effect::ReloadLyrics);
114 ctx.notify("Fetching lyrics again…");
115 return true;
116 }
117 _ => return false,
118 };
119 true
120 }
121
122 fn on_mouse(&mut self, event: MouseEvent, ctx: &mut Ctx) {
123 let action = match event.kind {
124 MouseEventKind::ScrollDown => Action::Down,
125 MouseEventKind::ScrollUp => Action::Up,
126 _ => return,
127 };
128 for _ in 0..3 {
129 self.handle_action(action, ctx);
130 }
131 }
132}