mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-04-28 10:06:05 +02:00
437 lines
15 KiB
Rust
Executable File
437 lines
15 KiB
Rust
Executable File
use std::time::Instant;
|
|
|
|
use musicdb_lib::{
|
|
data::queue::{QueueContent, QueueFolder},
|
|
server::Command,
|
|
};
|
|
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::VirtualKeyCode, Graphics2D};
|
|
|
|
use crate::{
|
|
gui::{
|
|
DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemChildren, KeyAction, KeyBinding,
|
|
SpecificGuiElem,
|
|
},
|
|
gui_anim::AnimationController,
|
|
gui_base::{Button, Panel},
|
|
gui_edit_song::EditorForSongs,
|
|
gui_idle_display::IdleDisplay,
|
|
gui_library::LibraryBrowser,
|
|
gui_notif::NotifOverlay,
|
|
gui_queue::QueueViewer,
|
|
gui_settings::Settings,
|
|
gui_song_adder::SongAdder,
|
|
gui_statusbar::StatusBar,
|
|
gui_text::Label,
|
|
gui_wrappers::Hotkey,
|
|
};
|
|
|
|
/*
|
|
|
|
The root gui element.
|
|
Contains the Library, Queue, StatusBar, and sometimes Settings elements.
|
|
Resizes these elements to show/hide the settings menu and to smoothly switch to/from idle mode.
|
|
|
|
*/
|
|
|
|
/// calculates f(p), where f(x) = 3x^2 - 2x^3, because
|
|
/// f(0) = 0
|
|
/// f(0.5) = 0.5
|
|
/// f(1) = 1
|
|
/// f'(0) = f'(1) = 0
|
|
/// -> smooth animation, fast to calculate
|
|
pub fn transition(p: f32) -> f32 {
|
|
3.0 * p * p - 2.0 * p * p * p
|
|
}
|
|
|
|
pub struct GuiScreen {
|
|
config: GuiElemCfg,
|
|
pub c_notif_overlay: NotifOverlay,
|
|
pub c_idle_display: IdleDisplay,
|
|
pub c_editing_songs: Option<EditorForSongs>,
|
|
pub c_status_bar: StatusBar,
|
|
pub c_settings: Settings,
|
|
pub c_song_adder: Option<SongAdder>,
|
|
pub c_main_view: Panel<MainView>,
|
|
pub c_context_menu: Option<Box<dyn GuiElem>>,
|
|
pub idle: AnimationController<f32>,
|
|
// pub settings: (bool, Option<Instant>),
|
|
pub settings: (bool, Option<Instant>),
|
|
pub last_interaction: Instant,
|
|
idle_timeout: Option<f64>,
|
|
pub prev_mouse_pos: Vec2,
|
|
pub hotkey: Hotkey,
|
|
}
|
|
pub struct MainView {
|
|
pub button_clear_queue: Button<[Label; 1]>,
|
|
pub button_settings: Button<[Label; 1]>,
|
|
pub button_exit: Button<[Label; 1]>,
|
|
pub library_browser: LibraryBrowser,
|
|
pub queue_viewer: QueueViewer,
|
|
}
|
|
impl GuiElemChildren for MainView {
|
|
fn iter(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
|
|
Box::new(
|
|
[
|
|
self.button_clear_queue.elem_mut(),
|
|
self.button_settings.elem_mut(),
|
|
self.button_exit.elem_mut(),
|
|
self.library_browser.elem_mut(),
|
|
self.queue_viewer.elem_mut(),
|
|
]
|
|
.into_iter(),
|
|
)
|
|
}
|
|
fn len(&self) -> usize {
|
|
5
|
|
}
|
|
}
|
|
impl GuiScreen {
|
|
pub fn new(
|
|
config: GuiElemCfg,
|
|
c_notif_overlay: NotifOverlay,
|
|
no_animations: bool,
|
|
line_height: f32,
|
|
scroll_sensitivity_pixels: f64,
|
|
scroll_sensitivity_lines: f64,
|
|
scroll_sensitivity_pages: f64,
|
|
) -> Self {
|
|
Self {
|
|
config: config.w_keyboard_watch().w_mouse().w_keyboard_focus(),
|
|
c_notif_overlay,
|
|
c_status_bar: StatusBar::new(GuiElemCfg::at(Rectangle::from_tuples(
|
|
(0.0, 0.9),
|
|
(1.0, 1.0),
|
|
))),
|
|
c_editing_songs: None,
|
|
c_idle_display: IdleDisplay::new(GuiElemCfg::default().disabled()),
|
|
c_settings: Settings::new(
|
|
GuiElemCfg::default().disabled(),
|
|
no_animations,
|
|
line_height,
|
|
scroll_sensitivity_pixels,
|
|
scroll_sensitivity_lines,
|
|
scroll_sensitivity_pages,
|
|
),
|
|
c_song_adder: None,
|
|
c_main_view: Panel::new(
|
|
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.9))),
|
|
MainView {
|
|
button_clear_queue: Button::new(
|
|
GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.03))),
|
|
|_| {
|
|
vec![GuiAction::SendToServer(
|
|
musicdb_lib::server::Command::QueueUpdate(
|
|
vec![],
|
|
musicdb_lib::data::queue::QueueContent::Folder(
|
|
musicdb_lib::data::queue::QueueFolder::default(),
|
|
)
|
|
.into(),
|
|
),
|
|
)]
|
|
},
|
|
[Label::new(
|
|
GuiElemCfg::default(),
|
|
"Clear Queue".to_string(),
|
|
Color::WHITE,
|
|
None,
|
|
Vec2::new(0.5, 0.5),
|
|
)],
|
|
),
|
|
button_settings: Button::new(
|
|
GuiElemCfg::at(Rectangle::from_tuples((0.75, 0.0), (0.875, 0.03))),
|
|
|_| vec![GuiAction::OpenSettings(true)],
|
|
[Label::new(
|
|
GuiElemCfg::default(),
|
|
"Settings".to_string(),
|
|
Color::WHITE,
|
|
None,
|
|
Vec2::new(0.5, 0.5),
|
|
)],
|
|
),
|
|
button_exit: Button::new(
|
|
GuiElemCfg::at(Rectangle::from_tuples((0.875, 0.0), (1.0, 0.03))),
|
|
|_| vec![GuiAction::Exit],
|
|
[Label::new(
|
|
GuiElemCfg::default(),
|
|
"Exit".to_string(),
|
|
Color::WHITE,
|
|
None,
|
|
Vec2::new(0.5, 0.5),
|
|
)],
|
|
),
|
|
library_browser: LibraryBrowser::new(GuiElemCfg::at(Rectangle::from_tuples(
|
|
(0.0, 0.0),
|
|
(0.5, 1.0),
|
|
))),
|
|
queue_viewer: QueueViewer::new(GuiElemCfg::at(Rectangle::from_tuples(
|
|
(0.5, 0.03),
|
|
(1.0, 1.0),
|
|
))),
|
|
},
|
|
),
|
|
c_context_menu: None,
|
|
hotkey: Hotkey::new_noshift(VirtualKeyCode::Escape),
|
|
idle: AnimationController::new(0.0, 0.0, 0.01, 1.0, 0.8, 0.6, Instant::now()),
|
|
settings: (false, None),
|
|
last_interaction: Instant::now(),
|
|
idle_timeout: Some(60.0),
|
|
prev_mouse_pos: Vec2::ZERO,
|
|
}
|
|
}
|
|
fn get_prog(v: &mut (bool, Option<Instant>), seconds: f32) -> f32 {
|
|
if let Some(since) = &mut v.1 {
|
|
let prog = since.elapsed().as_secs_f32() / seconds;
|
|
if prog >= 1.0 {
|
|
v.1 = None;
|
|
if v.0 {
|
|
1.0
|
|
} else {
|
|
0.0
|
|
}
|
|
} else {
|
|
if v.0 {
|
|
prog
|
|
} else {
|
|
1.0 - prog
|
|
}
|
|
}
|
|
} else if v.0 {
|
|
1.0
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
pub fn force_idle(&mut self) {
|
|
self.idle.target = 1.0;
|
|
}
|
|
pub fn not_idle(&mut self) {
|
|
self.last_interaction = Instant::now();
|
|
if self.idle.target > 0.0 {
|
|
if self.idle.value < 1.0 {
|
|
self.idle.target = 0.0;
|
|
} else {
|
|
self.c_idle_display.c_idle_exit_hint.config_mut().enabled = true;
|
|
}
|
|
}
|
|
}
|
|
pub fn unidle(&mut self) {
|
|
self.not_idle();
|
|
self.c_idle_display.c_idle_exit_hint.config_mut().enabled = false;
|
|
self.idle.target = 0.0;
|
|
}
|
|
fn idle_check(&mut self) {
|
|
if self.idle.target == 0.0 {
|
|
if let Some(dur) = &self.idle_timeout {
|
|
if self.last_interaction.elapsed().as_secs_f64() > *dur {
|
|
self.idle.target = 1.0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn set_normal_ui_enabled(&mut self, enabled: bool) {
|
|
self.c_status_bar.config_mut().enabled = enabled;
|
|
// self.c_settings.config_mut().enabled = enabled;
|
|
self.c_main_view.config_mut().enabled = enabled;
|
|
}
|
|
}
|
|
impl GuiElem for GuiScreen {
|
|
fn config(&self) -> &GuiElemCfg {
|
|
&self.config
|
|
}
|
|
fn config_mut(&mut self) -> &mut GuiElemCfg {
|
|
&mut self.config
|
|
}
|
|
fn children(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
|
|
Box::new(
|
|
self.c_context_menu.iter_mut().map(|v| v.elem_mut()).chain(
|
|
[
|
|
self.c_notif_overlay.elem_mut(),
|
|
self.c_idle_display.elem_mut(),
|
|
]
|
|
.into_iter()
|
|
.chain(self.c_editing_songs.as_mut().map(|v| v.elem_mut()))
|
|
.chain(self.c_song_adder.as_mut().map(|v| v.elem_mut()).into_iter())
|
|
.chain([
|
|
self.c_status_bar.elem_mut(),
|
|
self.c_settings.elem_mut(),
|
|
self.c_main_view.elem_mut(),
|
|
]),
|
|
),
|
|
)
|
|
}
|
|
fn any(&self) -> &dyn std::any::Any {
|
|
self
|
|
}
|
|
fn any_mut(&mut self) -> &mut dyn std::any::Any {
|
|
self
|
|
}
|
|
fn elem(&self) -> &dyn GuiElem {
|
|
self
|
|
}
|
|
fn elem_mut(&mut self) -> &mut dyn GuiElem {
|
|
self
|
|
}
|
|
fn key_watch(
|
|
&mut self,
|
|
modifiers: speedy2d::window::ModifiersState,
|
|
down: bool,
|
|
key: Option<speedy2d::window::VirtualKeyCode>,
|
|
_scan: speedy2d::window::KeyScancode,
|
|
) -> Vec<GuiAction> {
|
|
if down {
|
|
self.not_idle();
|
|
}
|
|
if self.hotkey.triggered(modifiers, down, key) {
|
|
self.config.request_keyboard_focus = true;
|
|
vec![GuiAction::ResetKeyboardFocus]
|
|
} else {
|
|
vec![]
|
|
}
|
|
}
|
|
fn mouse_down(&mut self, _button: speedy2d::window::MouseButton) -> Vec<GuiAction> {
|
|
self.not_idle();
|
|
vec![]
|
|
}
|
|
fn draw(&mut self, info: &mut DrawInfo, _g: &mut Graphics2D) {
|
|
if self.config.init {
|
|
info.actions.extend([
|
|
GuiAction::AddKeybind(
|
|
Some(KeyBinding::ctrl(VirtualKeyCode::Q)),
|
|
KeyAction {
|
|
category: "General".to_owned(),
|
|
title: "Quit".to_owned(),
|
|
description: "Closes the application".to_owned(),
|
|
action: Box::new(|| vec![GuiAction::Exit]),
|
|
enabled: true,
|
|
},
|
|
Box::new(|_| {}),
|
|
),
|
|
GuiAction::AddKeybind(
|
|
Some(KeyBinding::ctrl(VirtualKeyCode::I)),
|
|
KeyAction {
|
|
category: "General".to_owned(),
|
|
title: "Idle".to_owned(),
|
|
description: "Opens the idle display".to_owned(),
|
|
action: Box::new(|| vec![GuiAction::ForceIdle]),
|
|
enabled: true,
|
|
},
|
|
Box::new(|_| {}),
|
|
),
|
|
GuiAction::AddKeybind(
|
|
Some(KeyBinding::ctrl(VirtualKeyCode::F)),
|
|
KeyAction {
|
|
category: "Library".to_owned(),
|
|
title: "Search songs".to_owned(),
|
|
description: "moves keyboard focus to the song search".to_owned(),
|
|
action: Box::new(|| {
|
|
vec![GuiAction::SetFocused(SpecificGuiElem::SearchSong)]
|
|
}),
|
|
enabled: true,
|
|
},
|
|
Box::new(|_| {}),
|
|
),
|
|
]);
|
|
}
|
|
// idle stuff
|
|
if self.prev_mouse_pos != info.mouse_pos {
|
|
self.prev_mouse_pos = info.mouse_pos;
|
|
self.not_idle();
|
|
} else if self.idle.target == 0.0 && self.config.pixel_pos.size() != info.pos.size() {
|
|
// resizing prevents idle, but doesn't un-idle
|
|
self.not_idle();
|
|
}
|
|
if !(!info.database.playing
|
|
|| matches!(info.database.queue.content(), QueueContent::Folder(QueueFolder { content: v, .. }) if v.is_empty()))
|
|
{
|
|
// skip idle_check if paused or queue is empty
|
|
self.idle_check();
|
|
}
|
|
// show/hide idle_exit_hint
|
|
let idle_exit_anim = if self.c_idle_display.c_idle_exit_hint.config().enabled {
|
|
let hide = info
|
|
.time
|
|
.duration_since(self.last_interaction)
|
|
.as_secs_f32()
|
|
/ 3.0;
|
|
let cv = if hide >= 1.0 {
|
|
self.c_idle_display.c_idle_exit_hint.config_mut().enabled = false;
|
|
false
|
|
} else {
|
|
let v = hide * hide;
|
|
let w = 0.15;
|
|
let h = 0.05;
|
|
let dx = w * v;
|
|
let dy = h * v;
|
|
self.c_idle_display.c_idle_exit_hint.config_mut().pos =
|
|
Rectangle::from_tuples((-dx, -dy), (w - dx, h - dy));
|
|
true
|
|
};
|
|
if let Some(h) = &info.helper {
|
|
h.set_cursor_visible(cv);
|
|
}
|
|
cv
|
|
} else {
|
|
false
|
|
};
|
|
// request_redraw for animations
|
|
let idle_changed = self.idle.update(info.time, info.high_performance);
|
|
if idle_changed || idle_exit_anim || self.settings.1.is_some() {
|
|
if let Some(h) = &info.helper {
|
|
h.request_redraw()
|
|
}
|
|
}
|
|
// animations: idle
|
|
if idle_changed {
|
|
let enable_normal_ui = self.idle.value < 1.0;
|
|
self.set_normal_ui_enabled(enable_normal_ui);
|
|
if let Some(h) = &info.helper {
|
|
h.set_cursor_visible(enable_normal_ui);
|
|
}
|
|
let idcfg = self.c_idle_display.config_mut();
|
|
let top = 1.0 - self.idle.value;
|
|
let bottom = top + 1.0;
|
|
idcfg.pos = Rectangle::from_tuples((0.0, top), (1.0, bottom));
|
|
idcfg.enabled = self.idle.value > 0.0;
|
|
self.c_status_bar.idle_mode = self.idle.value;
|
|
self.c_idle_display.idle_mode = self.idle.value;
|
|
}
|
|
// animations: settings
|
|
if self.settings.1.is_some() {
|
|
let p1 = Self::get_prog(&mut self.settings, 0.3);
|
|
let p = transition(p1);
|
|
let cfg = self.c_settings.config_mut();
|
|
cfg.enabled = p > 0.0;
|
|
cfg.pos = Rectangle::from_tuples((0.0, 0.9 - 0.9 * p), (1.0, 0.9));
|
|
}
|
|
// set idle timeout (only when settings are open)
|
|
if self.settings.0 || self.settings.1.is_some() {
|
|
self.idle_timeout = self.c_settings.get_timeout_val();
|
|
}
|
|
}
|
|
fn key_focus(
|
|
&mut self,
|
|
_modifiers: speedy2d::window::ModifiersState,
|
|
down: bool,
|
|
key: Option<speedy2d::window::VirtualKeyCode>,
|
|
_scan: speedy2d::window::KeyScancode,
|
|
) -> Vec<GuiAction> {
|
|
if down && matches!(key, Some(VirtualKeyCode::Space)) {
|
|
vec![GuiAction::Build(Box::new(|db| {
|
|
vec![GuiAction::SendToServer(if db.playing {
|
|
Command::Pause
|
|
} else {
|
|
Command::Resume
|
|
})]
|
|
}))]
|
|
} else if down && matches!(key, Some(VirtualKeyCode::F8)) {
|
|
vec![GuiAction::SendToServer(Command::ErrorInfo(
|
|
"".to_owned(),
|
|
"tEsT".to_owned(),
|
|
))]
|
|
} else {
|
|
vec![]
|
|
}
|
|
}
|
|
}
|