canary-rs/scripts/sao-ui/src/music_player.rs

431 lines
13 KiB
Rust

// Copyright (c) 2022 Marceline Crmaer
// SPDX-License-Identifier: AGPL-3.0-or-later
use api::*;
use canary_script::*;
use canary_music_player::{AlbumInfo, OutMsg, PlaybackStatus, ProgressChanged, TrackInfo};
use crate::widgets::prelude::*;
use button::{RoundButton, RoundButtonStyle};
use dialog::{DialogBodyStyle, DialogFooterStyle};
use shell::Offset;
use slider::Slider;
use text::{HorizontalAlignment, Label, LabelText};
pub struct MusicPlayerPanel {
panel: Panel,
widget: Option<MusicPlayerWidget>,
disconnected: Offset<Label>,
}
impl PanelImpl for MusicPlayerPanel {
fn update(&mut self, dt: f32) {
if let Some(widget) = self.widget.as_mut() {
Widget::update(widget, dt);
}
}
fn draw(&mut self) {
let ctx = DrawContext::new(self.panel);
if let Some(widget) = self.widget.as_mut() {
Widget::draw(widget, &ctx);
} else {
self.disconnected.draw(&ctx);
}
}
fn on_resize(&mut self, new_size: Vec2) {
self.disconnected.set_offset(new_size / 2.0);
if let Some(widget) = self.widget.as_mut() {
widget.resize(new_size);
}
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
if let Some(widget) = self.widget.as_mut() {
Widget::on_cursor_event(widget, kind, at);
}
}
fn on_message(&mut self, msg: Message) {
use canary_music_player::{serde_json::from_slice, InMsg};
let msg = msg.to_vec();
let msg: InMsg = match from_slice(&msg) {
Ok(msg) => msg,
Err(_) => return,
};
use InMsg::*;
match (self.widget.as_mut(), msg) {
(Some(_), Disconnected) => self.widget = None,
(None, Connected) => self.widget = Some(MusicPlayerWidget::new(self.panel)),
(Some(widget), AlbumChanged(info)) => widget.update_album(info),
(Some(widget), TrackChanged(info)) => widget.update_track(info),
(Some(widget), PlaybackStatusChanged(status)) => widget.update_playback_status(status),
(Some(widget), ProgressChanged(progress)) => widget.update_progress(progress),
_ => {}
}
}
}
impl MusicPlayerPanel {
pub fn bind(panel: Panel, _msg: Message) -> Box<dyn PanelImpl> {
let dc_text = LabelText {
font: Font::new(crate::DISPLAY_FONT),
text: "Disconnected".to_string(),
};
let disconnected = Label::new_centered(dc_text, 10.0, Color::WHITE);
let disconnected = Offset::new(disconnected, Vec2::ZERO);
Box::new(Self {
panel,
widget: None,
disconnected,
})
}
}
pub struct MusicPlayerStyle {
pub body: DialogBodyStyle,
pub footer: DialogFooterStyle,
pub rounding: f32,
pub art_margin: f32,
pub button_spacing: f32,
pub slider_height: f32,
}
impl Default for MusicPlayerStyle {
fn default() -> Self {
Self {
body: Default::default(),
footer: Default::default(),
rounding: 5.0,
art_margin: 5.0,
button_spacing: 15.0,
slider_height: 7.5,
}
}
}
pub struct MusicPlayerWidget {
panel: Panel,
artist: Offset<Label>,
album: Offset<Label>,
track: Offset<Label>,
previous: Offset<RoundButton>,
play: Offset<RoundButton>,
next: Offset<RoundButton>,
position: Offset<Label>,
duration: Offset<Label>,
slider: Slider,
style: MusicPlayerStyle,
art_rect: Rect,
body_rect: Rect,
footer_rect: Rect,
position_secs: f32,
duration_secs: f32,
position_dirty: bool,
position_updating: bool,
status: PlaybackStatus,
}
impl Container for MusicPlayerWidget {
fn with_children(&mut self, mut f: impl FnMut(&mut dyn Widget)) {
f(&mut self.artist);
f(&mut self.album);
f(&mut self.track);
f(&mut self.previous);
f(&mut self.play);
f(&mut self.next);
f(&mut self.position);
f(&mut self.duration);
f(&mut self.slider);
}
fn update(&mut self, dt: f32) {
let position_display = if let Some(position) = self.slider.has_update() {
self.position_updating = true;
Some(position * self.duration_secs)
} else if self.position_updating {
let position = self.slider.get_position() * self.duration_secs;
let offset = position - self.position_secs;
let msg = OutMsg::Seek { offset };
self.send_message(&msg);
self.position_secs = position;
self.position_updating = false;
Some(position)
} else if let PlaybackStatus::Playing = self.status {
self.position_secs += dt;
Some(self.position_secs)
} else if self.position_dirty {
self.position_dirty = false;
Some(self.position_secs)
} else {
None
};
if let Some(position) = position_display {
self.position_dirty = false;
self.position.set_text(&Self::format_time(position));
self.slider
.set_position(self.position_secs / self.duration_secs);
}
if self.previous.was_clicked() {
self.send_message(&OutMsg::Previous);
}
if self.play.was_clicked() {
self.send_message(&OutMsg::PlayPause);
}
if self.next.was_clicked() {
self.send_message(&OutMsg::Next);
}
}
fn draw(&mut self, ctx: &DrawContext) {
ctx.draw_partially_rounded_rect(
CornerFlags::TOP,
self.body_rect,
self.style.rounding,
self.style.body.color,
);
let placeholder_art_color = THEME.palette.overlay;
ctx.draw_rounded_rect(self.art_rect, self.style.rounding, placeholder_art_color);
ctx.draw_partially_rounded_rect(
CornerFlags::BOTTOM,
self.footer_rect,
self.style.rounding,
self.style.footer.color,
);
}
}
impl MusicPlayerWidget {
pub fn new(panel: Panel) -> Self {
let style = MusicPlayerStyle::default();
let display_font = Font::new(crate::DISPLAY_FONT);
let content_font = Font::new(crate::CONTENT_FONT);
let make_body_label = |content: &str| {
let text = LabelText {
font: display_font,
text: content.to_string(),
};
let color = style.body.text_color;
let label = Label::new_centered(text, 10.0, color);
Offset::new(label, Vec2::ZERO)
};
let make_footer_label = |content: &str| {
let text = LabelText {
font: content_font,
text: content.to_string(),
};
let size = style.footer.height;
let scale = size * 0.4;
let baseline = size * 0.125;
let label = Label::new(
text,
HorizontalAlignment::Center,
scale,
THEME.palette.text,
0.0,
0.0,
baseline,
);
Offset::new(label, Vec2::ZERO)
};
let icon_font = Font::new(crate::ICON_FONT);
let prev_text = LabelText {
font: icon_font,
text: "".to_string(),
};
let play_text = LabelText {
font: icon_font,
text: "".to_string(),
};
let next_text = LabelText {
font: icon_font,
text: "".to_string(),
};
let primary_button = RoundButtonStyle {
radius: style.footer.height * 0.3,
spacing: style.footer.height * 0.1,
thickness: style.footer.height * 0.025,
body_color: THEME.palette.yellow,
ring_color: THEME.palette.yellow,
icon_color: THEME.palette.black,
};
let secondary_button = RoundButtonStyle {
radius: style.footer.height * 0.25,
spacing: style.footer.height * 0.05,
thickness: style.footer.height * 0.025,
body_color: style.footer.color,
ring_color: THEME.palette.black,
icon_color: THEME.palette.black,
};
let prev = RoundButton::new(secondary_button.clone(), Some(prev_text));
let play = RoundButton::new(primary_button, Some(play_text));
let next = RoundButton::new(secondary_button, Some(next_text));
let slider = Slider::new(
Default::default(),
Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
);
Self {
panel,
artist: make_body_label("Artist"),
album: make_body_label("Album"),
track: make_body_label("Track"),
previous: Offset::new(prev, Vec2::ZERO),
play: Offset::new(play, Vec2::ZERO),
next: Offset::new(next, Vec2::ZERO),
position: make_footer_label("--:--"),
duration: make_footer_label("--:--"),
slider,
style,
art_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
body_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
footer_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
position_secs: 0.0,
duration_secs: 0.0,
position_dirty: false,
position_updating: false,
status: PlaybackStatus::Paused,
}
}
pub fn send_message(&self, msg: &OutMsg) {
let msg = serde_json::to_vec(msg).unwrap();
self.panel.send_message(&msg);
}
pub fn format_time(secs: f32) -> String {
let duration = secs.floor() as usize;
let seconds = duration % 60;
let minutes = (duration / 60) % 60;
let hours = (duration / 60) / 60;
if hours > 0 {
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
} else {
format!("{:02}:{:02}", minutes, seconds)
}
}
pub fn resize(&mut self, new_size: Vec2) {
let style = &self.style;
let width = new_size.x;
let body_height = new_size.y - style.footer.height;
let body_height = body_height.max(0.0);
let body_size = Vec2::new(width, body_height);
let footer_size = Vec2::new(width, style.footer.height);
let art_size = body_height - style.art_margin * 2.0;
self.art_rect = Rect::from_xy_size(Vec2::splat(style.art_margin), Vec2::splat(art_size));
let label_x = (width + self.art_rect.br.x) / 2.0;
let artist_baseline = body_height * 0.25;
let album_baseline = body_height * 0.55;
let track_baseline = body_height * 0.85;
let button_y = body_height + style.footer.height / 2.0;
let previous_x = style.button_spacing * 0.5;
let play_x = style.button_spacing * 1.5;
let next_x = style.button_spacing * 2.5;
let position_x = style.button_spacing * 3.5;
let slider_left = style.button_spacing * 4.25;
let slider_right = width - style.button_spacing * 1.5;
let slider_top = button_y - style.slider_height / 2.0;
let slider_bottom = button_y + style.slider_height / 2.0;
let duration_x = width - style.button_spacing * 0.75;
let slider_rect = Rect {
tl: Vec2::new(slider_left, slider_top),
br: Vec2::new(slider_right, slider_bottom),
};
self.artist.set_offset(Vec2::new(label_x, artist_baseline));
self.album.set_offset(Vec2::new(label_x, album_baseline));
self.track.set_offset(Vec2::new(label_x, track_baseline));
self.position.set_offset(Vec2::new(position_x, button_y));
self.duration.set_offset(Vec2::new(duration_x, button_y));
self.body_rect = Rect::from_xy_size(Vec2::ZERO, body_size);
self.footer_rect = Rect::from_xy_size(self.body_rect.bl(), footer_size);
self.previous.set_offset(Vec2::new(previous_x, button_y));
self.play.set_offset(Vec2::new(play_x, button_y));
self.next.set_offset(Vec2::new(next_x, button_y));
self.slider.set_rect(slider_rect);
}
pub fn update_album(&mut self, info: AlbumInfo) {
self.album.set_text(
info.title
.as_ref()
.map(|s| s.as_str())
.unwrap_or("<album here>"),
);
}
pub fn update_track(&mut self, info: TrackInfo) {
self.artist.set_text(
info.artists
.first()
.map(|s| s.as_str())
.unwrap_or("<artist here>"),
);
self.track.set_text(
info.title
.as_ref()
.map(|s| s.as_str())
.unwrap_or("<album here>"),
);
if let Some(length) = info.length {
self.duration.set_text(&Self::format_time(length));
self.duration_secs = length;
} else {
self.duration.set_text("--:--");
}
}
pub fn update_progress(&mut self, progress: ProgressChanged) {
self.position_secs = progress.position;
self.position_dirty = true;
}
pub fn update_playback_status(&mut self, status: PlaybackStatus) {
self.status = status;
let icon = match status {
PlaybackStatus::Playing => "",
PlaybackStatus::Paused => "",
PlaybackStatus::Stopped => "",
};
self.play.set_text(icon);
}
}