Krolyxon
Commit 1dcac38

rmptuisrcscreenslyrics.rs

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