From 922e4fcc00820d1fb4b3094fed2183cdc802d05f Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 13 Aug 2023 23:58:53 +0200 Subject: [PATCH] init --- .gitignore | 2 + README.md | 31 + musicdb-client/.gitignore | 1 + musicdb-client/Cargo.toml | 14 + musicdb-client/src/gui.rs | 880 ++++++++++++++++++ musicdb-client/src/gui_base.rs | 459 +++++++++ musicdb-client/src/gui_library.rs | 454 +++++++++ musicdb-client/src/gui_playback.rs | 238 +++++ musicdb-client/src/gui_queue.rs | 689 ++++++++++++++ musicdb-client/src/gui_screen.rs | 290 ++++++ musicdb-client/src/gui_settings.rs | 316 +++++++ musicdb-client/src/gui_text.rs | 226 +++++ musicdb-client/src/gui_wrappers.rs | 147 +++ musicdb-client/src/main.rs | 436 +++++++++ musicdb-lib/.gitignore | 2 + musicdb-lib/Cargo.toml | 12 + musicdb-lib/src/data/album.rs | 43 + musicdb-lib/src/data/artist.rs | 43 + musicdb-lib/src/data/database.rs | 377 ++++++++ musicdb-lib/src/data/mod.rs | 73 ++ musicdb-lib/src/data/queue.rs | 286 ++++++ musicdb-lib/src/data/song.rs | 166 ++++ musicdb-lib/src/lib.rs | 4 + musicdb-lib/src/load/mod.rs | 330 +++++++ musicdb-lib/src/player/mod.rs | 160 ++++ musicdb-lib/src/server/mod.rs | 265 ++++++ musicdb-lib/src/test.rs | 34 + musicdb-server/.gitignore | 1 + musicdb-server/Cargo.toml | 19 + musicdb-server/assets/album-view.html | 3 + musicdb-server/assets/albums_one.html | 1 + musicdb-server/assets/artist-view.html | 2 + musicdb-server/assets/artists.html | 2 + musicdb-server/assets/artists_one.html | 1 + musicdb-server/assets/queue.html | 3 + musicdb-server/assets/queue_folder.html | 10 + .../assets/queue_folder_current.html | 10 + musicdb-server/assets/queue_song.html | 5 + musicdb-server/assets/queue_song_current.html | 5 + musicdb-server/assets/root.html | 24 + musicdb-server/assets/songs_one.html | 1 + musicdb-server/src/main.rs | 184 ++++ musicdb-server/src/web.rs | 573 ++++++++++++ 43 files changed, 6822 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 musicdb-client/.gitignore create mode 100755 musicdb-client/Cargo.toml create mode 100755 musicdb-client/src/gui.rs create mode 100755 musicdb-client/src/gui_base.rs create mode 100755 musicdb-client/src/gui_library.rs create mode 100755 musicdb-client/src/gui_playback.rs create mode 100755 musicdb-client/src/gui_queue.rs create mode 100755 musicdb-client/src/gui_screen.rs create mode 100644 musicdb-client/src/gui_settings.rs create mode 100755 musicdb-client/src/gui_text.rs create mode 100755 musicdb-client/src/gui_wrappers.rs create mode 100755 musicdb-client/src/main.rs create mode 100755 musicdb-lib/.gitignore create mode 100755 musicdb-lib/Cargo.toml create mode 100755 musicdb-lib/src/data/album.rs create mode 100755 musicdb-lib/src/data/artist.rs create mode 100755 musicdb-lib/src/data/database.rs create mode 100755 musicdb-lib/src/data/mod.rs create mode 100755 musicdb-lib/src/data/queue.rs create mode 100755 musicdb-lib/src/data/song.rs create mode 100755 musicdb-lib/src/lib.rs create mode 100755 musicdb-lib/src/load/mod.rs create mode 100755 musicdb-lib/src/player/mod.rs create mode 100755 musicdb-lib/src/server/mod.rs create mode 100755 musicdb-lib/src/test.rs create mode 100755 musicdb-server/.gitignore create mode 100755 musicdb-server/Cargo.toml create mode 100644 musicdb-server/assets/album-view.html create mode 100644 musicdb-server/assets/albums_one.html create mode 100644 musicdb-server/assets/artist-view.html create mode 100644 musicdb-server/assets/artists.html create mode 100644 musicdb-server/assets/artists_one.html create mode 100644 musicdb-server/assets/queue.html create mode 100644 musicdb-server/assets/queue_folder.html create mode 100644 musicdb-server/assets/queue_folder_current.html create mode 100644 musicdb-server/assets/queue_song.html create mode 100644 musicdb-server/assets/queue_song_current.html create mode 100644 musicdb-server/assets/root.html create mode 100644 musicdb-server/assets/songs_one.html create mode 100755 musicdb-server/src/main.rs create mode 100644 musicdb-server/src/web.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..35fd6a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*/Cargo.lock +*/target diff --git a/README.md b/README.md new file mode 100644 index 0000000..746d4aa --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# musicdb + +custom music player running on my personal SBC which can be controlled from other WiFi devices (phone/pc) + +should perform pretty well (it runs well on my Pine A64 with 10k+ songs) + +## why??? + +#### server/client + +allows you to play music on any device you want while controlling playback from anywhere. +you can either run the client and server on the same machine or connect via tcp. + +if one client makes a change, all other clients will be notified of it and update almost instantly. + +it is also possible for a fake "client" to mirror the main server's playback, so you could sync up your entire house if you wanted to. + +#### complicated queue + +- allows more customization of playback (loops, custom shuffles, etc.) +- is more organized (adding an album doesn't add 10-20 songs, it creates a folder so you can (re)move the entire album in/from the queue) + +#### caching of songs + +for (almost) gapless playback, even when the data is stored on a NAS or cloud + +#### central database + +when storing data on a cloud, it would take forever to load all songs and scan them for metadata. +you would also run into issues with different file formats and where to store the cover images. +a custom database speeds up server startup and allows for more features. diff --git a/musicdb-client/.gitignore b/musicdb-client/.gitignore new file mode 100755 index 0000000..ea8c4bf --- /dev/null +++ b/musicdb-client/.gitignore @@ -0,0 +1 @@ +/target diff --git a/musicdb-client/Cargo.toml b/musicdb-client/Cargo.toml new file mode 100755 index 0000000..7d9d4a4 --- /dev/null +++ b/musicdb-client/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "musicdb-client" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" } +regex = "1.9.3" +speedy2d = { version = "1.12.0", optional = true } + +[features] +default = ["speedy2d"] diff --git a/musicdb-client/src/gui.rs b/musicdb-client/src/gui.rs new file mode 100755 index 0000000..d275768 --- /dev/null +++ b/musicdb-client/src/gui.rs @@ -0,0 +1,880 @@ +use std::{ + any::Any, + eprintln, + net::TcpStream, + sync::{Arc, Mutex}, + time::Instant, + usize, +}; + +use musicdb_lib::{ + data::{database::Database, queue::Queue, AlbumId, ArtistId, SongId}, + load::ToFromBytes, + server::Command, +}; +use speedy2d::{ + color::Color, + dimen::{UVec2, Vec2}, + font::Font, + shape::Rectangle, + window::{ + KeyScancode, ModifiersState, MouseButton, MouseScrollDistance, UserEventSender, + VirtualKeyCode, WindowCreationOptions, WindowHandler, WindowHelper, + }, + Graphics2D, +}; + +use crate::{gui_screen::GuiScreen, gui_wrappers::WithFocusHotkey}; + +pub enum GuiEvent { + Refresh, + UpdatedQueue, + UpdatedLibrary, + Exit, +} + +pub fn main( + database: Arc>, + connection: TcpStream, + event_sender_arc: Arc>>>, +) { + let window = speedy2d::Window::::new_with_user_events( + "MusicDB Client", + WindowCreationOptions::new_fullscreen_borderless(), + ) + .expect("couldn't open window"); + *event_sender_arc.lock().unwrap() = Some(window.create_user_event_sender()); + let sender = window.create_user_event_sender(); + window.run_loop(Gui::new(database, connection, event_sender_arc, sender)); +} + +pub struct Gui { + pub event_sender: UserEventSender, + pub database: Arc>, + pub connection: TcpStream, + pub gui: GuiElem, + pub size: UVec2, + pub mouse_pos: Vec2, + pub font: Font, + pub last_draw: Instant, + pub modifiers: ModifiersState, + pub dragging: Option<( + Dragging, + Option>, + )>, + pub line_height: f32, + pub last_height: f32, + pub scroll_pixels_multiplier: f64, + pub scroll_lines_multiplier: f64, + pub scroll_pages_multiplier: f64, +} +impl Gui { + fn new( + database: Arc>, + connection: TcpStream, + event_sender_arc: Arc>>>, + event_sender: UserEventSender, + ) -> Self { + database.lock().unwrap().update_endpoints.push( + musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd { + Command::Resume + | Command::Pause + | Command::Stop + | Command::Save + | Command::SetLibraryDirectory(..) => {} + Command::NextSong + | Command::QueueUpdate(..) + | Command::QueueAdd(..) + | Command::QueueInsert(..) + | Command::QueueRemove(..) + | Command::QueueGoto(..) => { + if let Some(s) = &*event_sender_arc.lock().unwrap() { + _ = s.send_event(GuiEvent::UpdatedQueue); + } + } + Command::SyncDatabase(..) + | Command::AddSong(_) + | Command::AddAlbum(_) + | Command::AddArtist(_) + | Command::ModifySong(_) + | Command::ModifyAlbum(_) + | Command::ModifyArtist(_) => { + if let Some(s) = &*event_sender_arc.lock().unwrap() { + _ = s.send_event(GuiEvent::UpdatedLibrary); + } + } + })), + ); + let line_height = 32.0; + let scroll_pixels_multiplier = 1.0; + let scroll_lines_multiplier = 3.0; + let scroll_pages_multiplier = 0.75; + Gui { + event_sender, + database, + connection, + gui: GuiElem::new(WithFocusHotkey::new_noshift( + VirtualKeyCode::Escape, + GuiScreen::new( + GuiElemCfg::default(), + line_height, + scroll_pixels_multiplier, + scroll_lines_multiplier, + scroll_pages_multiplier, + ), + )), + size: UVec2::ZERO, + mouse_pos: Vec2::ZERO, + font: Font::new(include_bytes!( + "/usr/share/fonts/mozilla-fira/FiraSans-Regular.otf" + )) + .unwrap(), + // font: Font::new(include_bytes!("/usr/share/fonts/TTF/FiraSans-Regular.ttf")).unwrap(), + last_draw: Instant::now(), + modifiers: ModifiersState::default(), + dragging: None, + line_height, + last_height: 720.0, + scroll_pixels_multiplier, + scroll_lines_multiplier, + scroll_pages_multiplier, + } + } +} + +/// the trait implemented by all Gui elements. +/// feel free to override the methods you wish to use. +#[allow(unused)] +pub trait GuiElemTrait { + fn config(&self) -> &GuiElemCfg; + fn config_mut(&mut self) -> &mut GuiElemCfg; + /// note: drawing happens from the last to the first element, while priority is from first to last. + /// if you wish to add a "high priority" child to a Vec using push, .rev() the iterator in this method. + fn children(&mut self) -> Box + '_>; + fn any(&self) -> &dyn Any; + fn any_mut(&mut self) -> &mut dyn Any; + fn clone_gui(&self) -> Box; + /// handles drawing. + fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) {} + /// an event that is invoked whenever a mouse button is pressed on the element. + fn mouse_down(&mut self, button: MouseButton) -> Vec { + Vec::with_capacity(0) + } + /// an event that is invoked whenever a mouse button that was pressed on the element is released anywhere. + fn mouse_up(&mut self, button: MouseButton) -> Vec { + Vec::with_capacity(0) + } + /// an event that is invoked after a mouse button was pressed and released on the same GUI element. + fn mouse_pressed(&mut self, button: MouseButton) -> Vec { + Vec::with_capacity(0) + } + fn mouse_wheel(&mut self, diff: f32) -> Vec { + Vec::with_capacity(0) + } + fn char_watch(&mut self, modifiers: ModifiersState, key: char) -> Vec { + Vec::with_capacity(0) + } + fn char_focus(&mut self, modifiers: ModifiersState, key: char) -> Vec { + Vec::with_capacity(0) + } + fn key_watch( + &mut self, + modifiers: ModifiersState, + down: bool, + key: Option, + scan: KeyScancode, + ) -> Vec { + Vec::with_capacity(0) + } + fn key_focus( + &mut self, + modifiers: ModifiersState, + down: bool, + key: Option, + scan: KeyScancode, + ) -> Vec { + Vec::with_capacity(0) + } + /// When something is dragged and released over this element + fn dragged(&mut self, dragged: Dragging) -> Vec { + Vec::with_capacity(0) + } + fn updated_library(&mut self) {} + fn updated_queue(&mut self) {} +} + +#[derive(Debug, Clone)] +pub struct GuiElemCfg { + pub enabled: bool, + /// if true, indicates that something (text size, screen size, ...) has changed + /// and you should probably relayout and redraw from scratch. + pub redraw: bool, + pub pos: Rectangle, + /// the pixel position after the last call to draw(). + /// in draw, use info.pos instead, as pixel_pos is only updated *after* draw(). + /// this can act like a "previous pos" field within draw. + pub pixel_pos: Rectangle, + pub mouse_down: (bool, bool, bool), + pub mouse_events: bool, + pub scroll_events: bool, + /// allows elements to watch all keyboard events, regardless of keyboard focus. + pub keyboard_events_watch: bool, + /// indicates that this element can have the keyboard focus + pub keyboard_events_focus: bool, + /// index of the child that has keyboard focus. if usize::MAX, `self` has focus. + /// will automatically be changed when Tab is pressed. [TODO] + pub keyboard_focus_index: usize, + pub request_keyboard_focus: bool, + pub drag_target: bool, +} +impl GuiElemCfg { + pub fn at(pos: Rectangle) -> Self { + Self { + pos, + ..Default::default() + } + } + pub fn w_mouse(mut self) -> Self { + self.mouse_events = true; + self + } + pub fn w_scroll(mut self) -> Self { + self.scroll_events = true; + self + } + pub fn w_keyboard_watch(mut self) -> Self { + self.keyboard_events_watch = true; + self + } + pub fn w_keyboard_focus(mut self) -> Self { + self.keyboard_events_focus = true; + self + } + pub fn w_drag_target(mut self) -> Self { + self.drag_target = true; + self + } + pub fn disabled(mut self) -> Self { + self.enabled = false; + self + } +} +impl Default for GuiElemCfg { + fn default() -> Self { + Self { + enabled: true, + redraw: false, + pos: Rectangle::new(Vec2::ZERO, Vec2::new(1.0, 1.0)), + pixel_pos: Rectangle::ZERO, + mouse_down: (false, false, false), + mouse_events: false, + scroll_events: false, + keyboard_events_watch: false, + keyboard_events_focus: false, + keyboard_focus_index: usize::MAX, + request_keyboard_focus: false, + drag_target: false, + } + } +} +pub enum GuiAction { + OpenMain, + SetIdle(bool), + OpenSettings(bool), + Build(Box Vec>), + SendToServer(Command), + /// unfocuses all gui elements, then assigns keyboard focus to one with config().request_keyboard_focus == true. + ResetKeyboardFocus, + SetDragging( + Option<( + Dragging, + Option>, + )>, + ), + SetLineHeight(f32), + Do(Box), + Exit, +} +pub enum Dragging { + Artist(ArtistId), + Album(AlbumId), + Song(SongId), + Queue(Queue), +} +pub struct DrawInfo<'a> { + pub actions: Vec, + pub pos: Rectangle, + pub database: &'a mut Database, + pub font: &'a Font, + /// absolute position of the mouse on the screen. + /// compare this to `pos` to find the mouse's relative position. + pub mouse_pos: Vec2, + pub helper: Option<&'a mut WindowHelper>, + pub has_keyboard_focus: bool, + pub child_has_keyboard_focus: bool, + /// the height of one line of text (in pixels) + pub line_height: f32, +} + +pub struct GuiElem { + pub inner: Box, +} +impl Clone for GuiElem { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone_gui(), + } + } +} +impl GuiElem { + pub fn new(inner: T) -> Self { + Self { + inner: Box::new(inner), + } + } + pub fn try_as(&self) -> Option<&T> { + self.inner.any().downcast_ref() + } + pub fn try_as_mut(&mut self) -> Option<&mut T> { + self.inner.any_mut().downcast_mut() + } + pub fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) { + if !self.inner.config_mut().enabled { + return; + } + // adjust info + let npos = adjust_area(&info.pos, &self.inner.config_mut().pos); + let ppos = std::mem::replace(&mut info.pos, npos); + if info.child_has_keyboard_focus { + if self.inner.config().keyboard_focus_index == usize::MAX { + info.has_keyboard_focus = true; + info.child_has_keyboard_focus = false; + } + } + // call trait's draw function + self.inner.draw(info, g); + // reset info + info.has_keyboard_focus = false; + let focus_path = info.child_has_keyboard_focus; + // children (in reverse order - first element has the highest priority) + let kbd_focus_index = self.inner.config().keyboard_focus_index; + for (i, c) in self + .inner + .children() + .collect::>() + .into_iter() + .enumerate() + .rev() + { + info.child_has_keyboard_focus = focus_path && i == kbd_focus_index; + c.draw(info, g); + } + // reset pt. 2 + info.child_has_keyboard_focus = focus_path; + self.inner.config_mut().pixel_pos = std::mem::replace(&mut info.pos, ppos); + } + /// recursively applies the function to all gui elements below and including this one + pub fn recursive_all(&mut self, f: &mut F) { + f(self); + for c in self.inner.children() { + c.recursive_all(f); + } + } + fn mouse_event Option>>( + &mut self, + condition: &mut F, + pos: Vec2, + ) -> Option> { + for c in &mut self.inner.children() { + if c.inner.config().enabled { + if c.inner.config().pixel_pos.contains(pos) { + if let Some(v) = c.mouse_event(condition, pos) { + return Some(v); + } + } + } + } + condition(self) + } + fn release_drag( + &mut self, + dragged: &mut Option, + pos: Vec2, + ) -> Option> { + self.mouse_event( + &mut |v| { + if v.inner.config().drag_target { + if let Some(d) = dragged.take() { + return Some(v.inner.dragged(d)); + } + } + None + }, + pos, + ) + } + fn mouse_button( + &mut self, + button: MouseButton, + down: bool, + pos: Vec2, + ) -> Option> { + if down { + self.mouse_event( + &mut |v: &mut GuiElem| { + if v.inner.config().mouse_events { + match button { + MouseButton::Left => v.inner.config_mut().mouse_down.0 = true, + MouseButton::Middle => v.inner.config_mut().mouse_down.1 = true, + MouseButton::Right => v.inner.config_mut().mouse_down.2 = true, + MouseButton::Other(_) => {} + } + Some(v.inner.mouse_down(button)) + } else { + None + } + }, + pos, + ) + } else { + let mut vec = vec![]; + if let Some(a) = self.mouse_event( + &mut |v: &mut GuiElem| { + let down = v.inner.config().mouse_down; + if v.inner.config().mouse_events + && ((button == MouseButton::Left && down.0) + || (button == MouseButton::Middle && down.1) + || (button == MouseButton::Right && down.2)) + { + Some(v.inner.mouse_pressed(button)) + } else { + None + } + }, + pos, + ) { + vec.extend(a); + }; + self.recursive_all(&mut |v| { + if v.inner.config().mouse_events { + match button { + MouseButton::Left => v.inner.config_mut().mouse_down.0 = false, + MouseButton::Middle => v.inner.config_mut().mouse_down.1 = false, + MouseButton::Right => v.inner.config_mut().mouse_down.2 = false, + MouseButton::Other(_) => {} + } + vec.extend(v.inner.mouse_up(button)); + } + }); + Some(vec) + } + } + fn mouse_wheel(&mut self, diff: f32, pos: Vec2) -> Option> { + self.mouse_event( + &mut |v| { + if v.inner.config().scroll_events { + Some(v.inner.mouse_wheel(diff)) + } else { + None + } + }, + pos, + ) + } + fn keyboard_event< + F: FnOnce(&mut Self, &mut Vec), + G: FnMut(&mut Self, &mut Vec), + >( + &mut self, + f_focus: F, + mut f_watch: G, + ) -> Vec { + let mut o = vec![]; + self.keyboard_event_inner(&mut Some(f_focus), &mut f_watch, &mut o, true); + o + } + fn keyboard_event_inner< + F: FnOnce(&mut Self, &mut Vec), + G: FnMut(&mut Self, &mut Vec), + >( + &mut self, + f_focus: &mut Option, + f_watch: &mut G, + events: &mut Vec, + focus: bool, + ) { + f_watch(self, events); + let focus_index = self.inner.config().keyboard_focus_index; + for (i, child) in self.inner.children().enumerate() { + child.keyboard_event_inner(f_focus, f_watch, events, focus && i == focus_index); + } + if focus { + // we have focus and no child has consumed f_focus + if let Some(f) = f_focus.take() { + f(self, events) + } + } + } + fn keyboard_move_focus(&mut self, decrement: bool, refocus: bool) -> bool { + let mut focus_index = if refocus { + usize::MAX + } else { + self.inner.config().keyboard_focus_index + }; + let allow_focus = self.inner.config().keyboard_events_focus; + let mut children = self.inner.children().collect::>(); + if focus_index == usize::MAX { + if decrement { + focus_index = children.len().saturating_sub(1); + } else { + focus_index = 0; + } + } + let mut changed = refocus; + let ok = loop { + if let Some(child) = children.get_mut(focus_index) { + if child.keyboard_move_focus(decrement, changed) { + break true; + } else { + changed = true; + if !decrement { + focus_index += 1; + } else { + focus_index = focus_index.wrapping_sub(1); + } + } + } else { + focus_index = usize::MAX; + break allow_focus && refocus; + } + }; + self.inner.config_mut().keyboard_focus_index = focus_index; + ok + } + fn keyboard_reset_focus(&mut self) -> bool { + let mut index = usize::MAX; + for (i, c) in self.inner.children().enumerate() { + if c.keyboard_reset_focus() { + index = i; + break; + } + } + let wants = std::mem::replace(&mut self.inner.config_mut().request_keyboard_focus, false); + self.inner.config_mut().keyboard_focus_index = index; + index != usize::MAX || wants + } +} + +pub fn adjust_area(outer: &Rectangle, rel_area: &Rectangle) -> Rectangle { + Rectangle::new( + adjust_pos(outer, rel_area.top_left()), + adjust_pos(outer, rel_area.bottom_right()), + ) +} +pub fn adjust_pos(outer: &Rectangle, rel_pos: &Vec2) -> Vec2 { + Vec2::new( + outer.top_left().x + outer.width() * rel_pos.x, + outer.top_left().y + outer.height() * rel_pos.y, + ) +} + +impl Gui { + fn exec_gui_action(&mut self, action: GuiAction) { + match action { + GuiAction::Build(f) => { + let actions = f(&mut *self.database.lock().unwrap()); + for action in actions { + self.exec_gui_action(action); + } + } + GuiAction::SendToServer(cmd) => { + if let Err(e) = cmd.to_bytes(&mut self.connection) { + eprintln!("Error sending command to server: {e}"); + } + } + GuiAction::ResetKeyboardFocus => _ = self.gui.keyboard_reset_focus(), + GuiAction::SetDragging(d) => self.dragging = d, + GuiAction::SetLineHeight(h) => { + self.line_height = h; + self.gui + .recursive_all(&mut |e| e.inner.config_mut().redraw = true); + } + GuiAction::Do(mut f) => f(self), + GuiAction::Exit => _ = self.event_sender.send_event(GuiEvent::Exit), + GuiAction::SetIdle(v) => { + if let Some(gui) = self + .gui + .inner + .any_mut() + .downcast_mut::>() + { + if gui.inner.idle.0 != v { + gui.inner.idle = (v, Some(Instant::now())); + } + } + } + GuiAction::OpenSettings(v) => { + if let Some(gui) = self + .gui + .inner + .any_mut() + .downcast_mut::>() + { + if gui.inner.idle.0 { + gui.inner.idle = (false, Some(Instant::now())); + } + if gui.inner.settings.0 != v { + gui.inner.settings = (v, Some(Instant::now())); + } + } + } + GuiAction::OpenMain => { + if let Some(gui) = self + .gui + .inner + .any_mut() + .downcast_mut::>() + { + if gui.inner.idle.0 { + gui.inner.idle = (false, Some(Instant::now())); + } + if gui.inner.settings.0 { + gui.inner.settings = (false, Some(Instant::now())); + } + } + } + } + } +} +impl WindowHandler for Gui { + fn on_draw(&mut self, helper: &mut WindowHelper, graphics: &mut Graphics2D) { + let start = Instant::now(); + graphics.draw_rectangle( + Rectangle::new(Vec2::ZERO, self.size.into_f32()), + Color::BLACK, + ); + let mut dblock = self.database.lock().unwrap(); + let mut info = DrawInfo { + actions: Vec::with_capacity(0), + pos: Rectangle::new(Vec2::ZERO, self.size.into_f32()), + database: &mut *dblock, + font: &self.font, + mouse_pos: self.mouse_pos, + helper: Some(helper), + has_keyboard_focus: false, + child_has_keyboard_focus: true, + line_height: self.line_height, + }; + self.gui.draw(&mut info, graphics); + let actions = std::mem::replace(&mut info.actions, Vec::with_capacity(0)); + if let Some((d, f)) = &mut self.dragging { + if let Some(f) = f { + f(&mut info, graphics); + } else { + match d { + Dragging::Artist(_) => graphics.draw_circle( + self.mouse_pos, + 25.0, + Color::from_int_rgba(0, 100, 255, 100), + ), + Dragging::Album(_) => graphics.draw_circle( + self.mouse_pos, + 25.0, + Color::from_int_rgba(0, 100, 255, 100), + ), + Dragging::Song(_) => graphics.draw_circle( + self.mouse_pos, + 25.0, + Color::from_int_rgba(0, 100, 255, 100), + ), + Dragging::Queue(_) => graphics.draw_circle( + self.mouse_pos, + 25.0, + Color::from_int_rgba(100, 0, 255, 100), + ), + } + } + } + drop(info); + drop(dblock); + for a in actions { + self.exec_gui_action(a); + } + // eprintln!( + // "fps <= {}", + // 1000 / self.last_draw.elapsed().as_millis().max(1) + // ); + self.last_draw = start; + } + fn on_mouse_button_down(&mut self, helper: &mut WindowHelper, button: MouseButton) { + if let Some(a) = self.gui.mouse_button(button, true, self.mouse_pos.clone()) { + for a in a { + self.exec_gui_action(a) + } + } + helper.request_redraw(); + } + fn on_mouse_button_up(&mut self, helper: &mut WindowHelper, button: MouseButton) { + if self.dragging.is_some() { + if let Some(a) = self.gui.release_drag( + &mut self.dragging.take().map(|v| v.0), + self.mouse_pos.clone(), + ) { + for a in a { + self.exec_gui_action(a) + } + } + } + if let Some(a) = self.gui.mouse_button(button, false, self.mouse_pos.clone()) { + for a in a { + self.exec_gui_action(a) + } + } + helper.request_redraw(); + } + fn on_mouse_wheel_scroll( + &mut self, + helper: &mut WindowHelper, + distance: speedy2d::window::MouseScrollDistance, + ) { + let dist = match distance { + MouseScrollDistance::Pixels { y, .. } => (self.scroll_pixels_multiplier * y) as f32, + MouseScrollDistance::Lines { y, .. } => { + (self.scroll_lines_multiplier * y) as f32 * self.line_height + } + MouseScrollDistance::Pages { y, .. } => { + (self.scroll_pages_multiplier * y) as f32 * self.last_height + } + }; + if let Some(a) = self.gui.mouse_wheel(dist, self.mouse_pos.clone()) { + for a in a { + self.exec_gui_action(a) + } + } + helper.request_redraw(); + } + fn on_keyboard_char(&mut self, helper: &mut WindowHelper, unicode_codepoint: char) { + helper.request_redraw(); + for a in self.gui.keyboard_event( + |e, a| { + if e.inner.config().keyboard_events_focus { + a.append( + &mut e + .inner + .char_focus(self.modifiers.clone(), unicode_codepoint), + ); + } + }, + |e, a| { + if e.inner.config().keyboard_events_watch { + a.append( + &mut e + .inner + .char_watch(self.modifiers.clone(), unicode_codepoint), + ); + } + }, + ) { + self.exec_gui_action(a); + } + } + fn on_key_down( + &mut self, + helper: &mut WindowHelper, + virtual_key_code: Option, + scancode: KeyScancode, + ) { + helper.request_redraw(); + if let Some(VirtualKeyCode::Tab) = virtual_key_code { + if !(self.modifiers.ctrl() || self.modifiers.alt() || self.modifiers.logo()) { + self.gui.keyboard_move_focus(self.modifiers.shift(), false); + } + } + for a in self.gui.keyboard_event( + |e, a| { + if e.inner.config().keyboard_events_focus { + a.append(&mut e.inner.key_focus( + self.modifiers.clone(), + true, + virtual_key_code, + scancode, + )); + } + }, + |e, a| { + if e.inner.config().keyboard_events_watch { + a.append(&mut e.inner.key_watch( + self.modifiers.clone(), + true, + virtual_key_code, + scancode, + )); + } + }, + ) { + self.exec_gui_action(a); + } + } + fn on_key_up( + &mut self, + helper: &mut WindowHelper, + virtual_key_code: Option, + scancode: KeyScancode, + ) { + helper.request_redraw(); + for a in self.gui.keyboard_event( + |e, a| { + if e.inner.config().keyboard_events_focus { + a.append(&mut e.inner.key_focus( + self.modifiers.clone(), + false, + virtual_key_code, + scancode, + )); + } + }, + |e, a| { + if e.inner.config().keyboard_events_watch { + a.append(&mut e.inner.key_watch( + self.modifiers.clone(), + false, + virtual_key_code, + scancode, + )); + } + }, + ) { + self.exec_gui_action(a); + } + } + fn on_keyboard_modifiers_changed( + &mut self, + _helper: &mut WindowHelper, + state: ModifiersState, + ) { + self.modifiers = state; + } + fn on_user_event(&mut self, helper: &mut WindowHelper, user_event: GuiEvent) { + match user_event { + GuiEvent::Refresh => helper.request_redraw(), + GuiEvent::UpdatedLibrary => { + self.gui.recursive_all(&mut |e| e.inner.updated_library()); + helper.request_redraw(); + } + GuiEvent::UpdatedQueue => { + self.gui.recursive_all(&mut |e| e.inner.updated_queue()); + helper.request_redraw(); + } + GuiEvent::Exit => helper.terminate_loop(), + } + } + fn on_mouse_move(&mut self, helper: &mut WindowHelper, position: Vec2) { + self.mouse_pos = position; + helper.request_redraw(); + } + fn on_resize(&mut self, _helper: &mut WindowHelper, size_pixels: UVec2) { + self.size = size_pixels; + self.gui + .recursive_all(&mut |e| e.inner.config_mut().redraw = true); + } +} diff --git a/musicdb-client/src/gui_base.rs b/musicdb-client/src/gui_base.rs new file mode 100755 index 0000000..7933c53 --- /dev/null +++ b/musicdb-client/src/gui_base.rs @@ -0,0 +1,459 @@ +use std::{sync::Arc, time::Instant}; + +use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::MouseButton}; + +use crate::{ + gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, + gui_text::Label, +}; + +/// A simple container for zero, one, or multiple child GuiElems. Can optionally fill the background with a color. +#[derive(Clone)] +pub struct Panel { + config: GuiElemCfg, + pub children: Vec, + pub background: Option, +} +impl Panel { + pub fn new(config: GuiElemCfg, children: Vec) -> Self { + Self { + config, + children, + background: None, + } + } + pub fn with_background(config: GuiElemCfg, children: Vec, background: Color) -> Self { + Self { + config, + children, + background: Some(background), + } + } +} +impl GuiElemTrait for Panel { + fn config(&self) -> &GuiElemCfg { + &self.config + } + fn config_mut(&mut self) -> &mut GuiElemCfg { + &mut self.config + } + fn children(&mut self) -> Box + '_> { + Box::new(self.children.iter_mut()) + } + fn any(&self) -> &dyn std::any::Any { + self + } + fn any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + fn clone_gui(&self) -> Box { + Box::new(self.clone()) + } + fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { + if let Some(c) = self.background { + g.draw_rectangle(info.pos.clone(), c); + } + } +} + +#[derive(Clone)] +pub struct ScrollBox { + config: GuiElemCfg, + pub children: Vec<(GuiElem, f32)>, + pub size_unit: ScrollBoxSizeUnit, + pub scroll_target: f32, + pub scroll_display: f32, + height_bottom: f32, + /// 0.max(height_bottom - 1) + max_scroll: f32, + last_height_px: f32, +} +#[derive(Clone)] +pub enum ScrollBoxSizeUnit { + Relative, + Pixels, +} +impl ScrollBox { + pub fn new( + mut config: GuiElemCfg, + size_unit: ScrollBoxSizeUnit, + children: Vec<(GuiElem, f32)>, + ) -> Self { + // config.redraw = true; + Self { + config: config.w_scroll(), + children, + size_unit, + scroll_target: 0.0, + scroll_display: 0.0, + /// the y-position of the bottom edge of the last element (i.e. the total height) + height_bottom: 0.0, + max_scroll: 0.0, + last_height_px: 0.0, + } + } +} +impl GuiElemTrait for ScrollBox { + fn config(&self) -> &GuiElemCfg { + &self.config + } + fn config_mut(&mut self) -> &mut GuiElemCfg { + &mut self.config + } + fn children(&mut self) -> Box + '_> { + Box::new( + self.children + .iter_mut() + .rev() + .map(|(v, _)| v) + .skip_while(|v| v.inner.config().pos.bottom_right().y < 0.0) + .take_while(|v| v.inner.config().pos.top_left().y < 1.0), + ) + } + fn any(&self) -> &dyn std::any::Any { + self + } + fn any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + fn clone_gui(&self) -> Box { + Box::new(self.clone()) + } + fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { + if self.config.pixel_pos.size() != info.pos.size() { + self.config.redraw = true; + } + // smooth scrolling animation + if self.scroll_target > self.max_scroll { + self.scroll_target = self.max_scroll; + } else if self.scroll_target < 0.0 { + self.scroll_target = 0.0; + } + self.scroll_display = 0.2 * self.scroll_target + 0.8 * self.scroll_display; + if self.scroll_display != self.scroll_target { + self.config.redraw = true; + if (self.scroll_display - self.scroll_target).abs() < 1.0 / info.pos.height() { + self.scroll_display = self.scroll_target; + } else if let Some(h) = &info.helper { + h.request_redraw(); + } + } + // recalculate positions + if self.config.redraw { + self.config.redraw = false; + let mut y_pos = -self.scroll_display; + for (e, h) in self.children.iter_mut() { + let h_rel = self.size_unit.to_rel(*h, info.pos.height()); + let y_rel = self.size_unit.to_rel(y_pos, info.pos.height()); + if y_rel + h_rel >= 0.0 && y_rel <= 1.0 { + let cfg = e.inner.config_mut(); + cfg.enabled = true; + cfg.pos = Rectangle::new( + Vec2::new(cfg.pos.top_left().x, 0.0f32.max(y_rel)), + Vec2::new(cfg.pos.bottom_right().x, 1.0f32.min(y_rel + h_rel)), + ); + } else { + e.inner.config_mut().enabled = false; + } + y_pos += *h; + } + self.height_bottom = y_pos + self.scroll_display; + self.max_scroll = + 0.0f32.max(self.height_bottom - self.size_unit.from_rel(0.75, info.pos.height())); + } + } + fn mouse_wheel(&mut self, diff: f32) -> Vec { + self.scroll_target = (self.scroll_target + - self.size_unit.from_abs(diff as f32, self.last_height_px)) + .max(0.0); + Vec::with_capacity(0) + } +} +impl ScrollBoxSizeUnit { + fn to_rel(&self, val: f32, draw_height: f32) -> f32 { + match self { + Self::Relative => val, + Self::Pixels => val / draw_height, + } + } + fn from_rel(&self, val: f32, draw_height: f32) -> f32 { + match self { + Self::Relative => val, + Self::Pixels => val * draw_height, + } + } + fn from_abs(&self, val: f32, draw_height: f32) -> f32 { + match self { + Self::Relative => val / draw_height, + Self::Pixels => val, + } + } +} + +#[derive(Clone)] +pub struct Button { + config: GuiElemCfg, + pub children: Vec, + action: Arc Vec + 'static>, +} +impl Button { + /// automatically adds w_mouse to config + pub fn new Vec + 'static>( + config: GuiElemCfg, + action: F, + children: Vec, + ) -> Self { + Self { + config: config.w_mouse(), + children, + action: Arc::new(action), + } + } +} +impl GuiElemTrait for Button { + fn config(&self) -> &GuiElemCfg { + &self.config + } + fn config_mut(&mut self) -> &mut GuiElemCfg { + &mut self.config + } + fn children(&mut self) -> Box + '_> { + Box::new(self.children.iter_mut()) + } + fn any(&self) -> &dyn std::any::Any { + self + } + fn any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + fn clone_gui(&self) -> Box { + Box::new(self.clone()) + } + fn mouse_pressed(&mut self, button: MouseButton) -> Vec { + if button == MouseButton::Left { + (self.action)(self) + } else { + vec![] + } + } + fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) { + let mouse_down = self.config.mouse_down.0; + let contains = info.pos.contains(info.mouse_pos); + g.draw_rectangle( + info.pos.clone(), + if mouse_down && contains { + Color::from_rgb(0.25, 0.25, 0.25) + } else if contains || mouse_down { + Color::from_rgb(0.15, 0.15, 0.15) + } else { + Color::from_rgb(0.1, 0.1, 0.1) + }, + ); + } +} + +#[derive(Clone)] +pub struct Slider { + pub config: GuiElemCfg, + pub children: Vec, + pub slider_pos: Rectangle, + pub min: f64, + pub max: f64, + pub val: f64, + val_changed: bool, + pub val_changed_subs: Vec, + /// if true, the display should be visible. + pub display: bool, + /// if Some, the display is in a transition period. + /// you can set this to None to indicate that the transition has finished, but this is not required. + pub display_since: Option, + pub on_update: Arc, +} +impl Slider { + /// returns true if the value of the slider has changed since the last time this function was called. + /// this is usually used by the closure responsible for directly handling updates. if you wish to check for changes + /// from outside, push a `false` to `val_changed_subs` and remember your index. + /// when the value changes, this will be set to `true`. don't forget to reset it to `false` again if you find it set to `true`, + /// or your code will run every time. + pub fn val_changed(&mut self) -> bool { + if self.val_changed { + self.val_changed = false; + true + } else { + false + } + } + pub fn val_changed_peek(&self) -> bool { + self.val_changed + } + pub fn new( + config: GuiElemCfg, + slider_pos: Rectangle, + min: f64, + max: f64, + val: f64, + children: Vec, + on_update: F, + ) -> Self { + Self { + config: config.w_mouse().w_scroll(), + children, + slider_pos, + min, + max, + val, + val_changed: true, + val_changed_subs: vec![], + display: false, + display_since: None, + on_update: Arc::new(on_update), + } + } + pub fn new_labeled( + config: GuiElemCfg, + min: f64, + max: f64, + val: f64, + mktext: F, + ) -> Self { + Self::new( + config, + Rectangle::new(Vec2::ZERO, Vec2::new(1.0, 1.0)), + min, + max, + val, + vec![GuiElem::new(Label::new( + GuiElemCfg::default(), + String::new(), + Color::WHITE, + // Some(Color::from_int_rgba(0, 0, 0, 150)), + None, + Vec2::new(0.5, 1.0), + ))], + move |s, i| { + if s.display || s.display_since.is_some() { + let mut label = s.children.pop().unwrap(); + if let Some(l) = label.inner.any_mut().downcast_mut::