From 55190d194351304a51f8e319408cc41604db5aa9 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 12 Nov 2023 20:52:24 +0100 Subject: [PATCH] artist image support --- musicdb-client/src/gui.rs | 436 +++++++--- musicdb-client/src/gui_anim.rs | 130 +++ musicdb-client/src/gui_base.rs | 128 +-- musicdb-client/src/gui_edit.rs | 637 -------------- musicdb-client/src/gui_idle_display.rs | 271 ++++++ musicdb-client/src/gui_library.rs | 1103 ++++++++++++------------ musicdb-client/src/gui_notif.rs | 18 +- musicdb-client/src/gui_playback.rs | 544 ++++-------- musicdb-client/src/gui_queue.rs | 410 ++++----- musicdb-client/src/gui_screen.rs | 430 ++++----- musicdb-client/src/gui_settings.rs | 499 +++++------ musicdb-client/src/gui_statusbar.rs | 123 +++ musicdb-client/src/gui_text.rs | 107 +-- musicdb-client/src/gui_wrappers.rs | 173 +--- musicdb-client/src/main.rs | 6 +- musicdb-client/src/textcfg.rs | 3 +- musicdb-filldb/src/main.rs | 100 ++- musicdb-lib/src/data/song.rs | 2 +- musicdb-lib/src/server/get.rs | 34 +- 19 files changed, 2387 insertions(+), 2767 deletions(-) create mode 100644 musicdb-client/src/gui_anim.rs delete mode 100755 musicdb-client/src/gui_edit.rs create mode 100644 musicdb-client/src/gui_idle_display.rs create mode 100644 musicdb-client/src/gui_statusbar.rs diff --git a/musicdb-client/src/gui.rs b/musicdb-client/src/gui.rs index 26c393f..f5ecbe7 100755 --- a/musicdb-client/src/gui.rs +++ b/musicdb-client/src/gui.rs @@ -1,13 +1,11 @@ use std::{ any::Any, collections::HashMap, - eprintln, io::Cursor, net::TcpStream, - sync::{Arc, Mutex}, + sync::{mpsc::Sender, Arc, Mutex}, thread::JoinHandle, time::{Duration, Instant}, - usize, }; use musicdb_lib::{ @@ -33,7 +31,6 @@ use crate::{ gui_notif::{NotifInfo, NotifOverlay}, gui_screen::GuiScreen, gui_text::Label, - gui_wrappers::WithFocusHotkey, textcfg, }; @@ -87,6 +84,9 @@ pub fn main( let mut scroll_lines_multiplier = 3.0; let mut scroll_pages_multiplier = 0.75; let status_bar_text; + let idle_top_text; + let idle_side1_text; + let idle_side2_text; match std::fs::read_to_string(&config_file) { Ok(cfg) => { if let Ok(table) = cfg.parse::() { @@ -135,6 +135,42 @@ pub fn main( eprintln!("[toml] missing the required `text.status_bar` string value."); std::process::exit(30); } + if let Some(v) = t.get("idle_top").and_then(|v| v.as_str()) { + match v.parse() { + Ok(v) => idle_top_text = v, + Err(e) => { + eprintln!("[toml] `text.idle_top couldn't be parsed: {e}`"); + std::process::exit(30); + } + } + } else { + eprintln!("[toml] missing the required `text.idle_top` string value."); + std::process::exit(30); + } + if let Some(v) = t.get("idle_side1").and_then(|v| v.as_str()) { + match v.parse() { + Ok(v) => idle_side1_text = v, + Err(e) => { + eprintln!("[toml] `text.idle_side1 couldn't be parsed: {e}`"); + std::process::exit(30); + } + } + } else { + eprintln!("[toml] missing the required `text.idle_side1` string value."); + std::process::exit(30); + } + if let Some(v) = t.get("idle_side2").and_then(|v| v.as_str()) { + match v.parse() { + Ok(v) => idle_side2_text = v, + Err(e) => { + eprintln!("[toml] `text.idle_side2 couldn't be parsed: {e}`"); + std::process::exit(30); + } + } + } else { + eprintln!("[toml] missing the required `text.idle_side2` string value."); + std::process::exit(30); + } } else { eprintln!("[toml] missing the required `[text]` section!"); std::process::exit(30); @@ -183,12 +219,53 @@ pub fn main( scroll_pixels_multiplier, scroll_lines_multiplier, scroll_pages_multiplier, - GuiConfig { status_bar_text }, + GuiConfig { + status_bar_text, + idle_top_text, + idle_side1_text, + idle_side2_text, + filter_presets_song: vec![ + ( + "Fav".to_owned(), + crate::gui_library::FilterType::TagEq("Fav".to_owned()), + ), + ( + "Year".to_owned(), + crate::gui_library::FilterType::TagWithValueInt("Year".to_owned(), 1990, 2000), + ), + ], + filter_presets_album: vec![ + ( + "Fav".to_owned(), + crate::gui_library::FilterType::TagEq("Fav".to_owned()), + ), + ( + "Year".to_owned(), + crate::gui_library::FilterType::TagWithValueInt("Year".to_owned(), 1990, 2000), + ), + ], + filter_presets_artist: vec![ + ( + "Fav".to_owned(), + crate::gui_library::FilterType::TagEq("Fav".to_owned()), + ), + ( + "Year".to_owned(), + crate::gui_library::FilterType::TagWithValueInt("Year".to_owned(), 1990, 2000), + ), + ], + }, )); } pub struct GuiConfig { pub status_bar_text: textcfg::TextBuilder, + pub idle_top_text: textcfg::TextBuilder, + pub idle_side1_text: textcfg::TextBuilder, + pub idle_side2_text: textcfg::TextBuilder, + pub filter_presets_song: Vec<(String, crate::gui_library::FilterType)>, + pub filter_presets_album: Vec<(String, crate::gui_library::FilterType)>, + pub filter_presets_artist: Vec<(String, crate::gui_library::FilterType)>, } pub struct Gui { @@ -196,17 +273,21 @@ pub struct Gui { pub database: Arc>, pub connection: TcpStream, pub get_con: Arc>>, - pub gui: WithFocusHotkey, + pub gui: GuiScreen, + pub notif_sender: + Sender (Box, NotifInfo) + Send>>, pub size: UVec2, pub mouse_pos: Vec2, pub font: Font, - pub covers: Option>, + pub covers: Option>, + pub custom_images: Option>, pub last_draw: Instant, pub modifiers: ModifiersState, pub dragging: Option<( Dragging, Option>, )>, + pub no_animations: bool, pub line_height: f32, pub last_height: f32, pub scroll_pixels_multiplier: f64, @@ -229,6 +310,7 @@ impl Gui { gui_config: GuiConfig, ) -> Self { let (notif_overlay, notif_sender) = NotifOverlay::new(); + let notif_sender_two = notif_sender.clone(); database.lock().unwrap().update_endpoints.push( musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd { Command::Resume @@ -265,12 +347,12 @@ impl Gui { } Command::ErrorInfo(t, d) => { let (t, d) = (t.clone(), d.clone()); - notif_sender + notif_sender_two .send(Box::new(move |_| { ( Box::new(Panel::with_background( GuiElemCfg::default(), - vec![Box::new(Label::new( + [Label::new( GuiElemCfg::default(), if t.is_empty() { format!("Server message\n{d}") @@ -280,7 +362,7 @@ impl Gui { Color::WHITE, None, Vec2::new(0.5, 0.5), - ))], + )], Color::from_rgba(0.0, 0.0, 0.0, 0.8), )), if t.is_empty() { @@ -295,30 +377,32 @@ impl Gui { } })), ); + let no_animations = false; Gui { event_sender, database, connection, get_con, - gui: WithFocusHotkey::new_noshift( - VirtualKeyCode::Escape, - GuiScreen::new( - GuiElemCfg::default(), - notif_overlay, - line_height, - scroll_pixels_multiplier, - scroll_lines_multiplier, - scroll_pages_multiplier, - ), + gui: GuiScreen::new( + GuiElemCfg::default(), + notif_overlay, + no_animations, + line_height, + scroll_pixels_multiplier, + scroll_lines_multiplier, + scroll_pages_multiplier, ), + notif_sender, size: UVec2::ZERO, mouse_pos: Vec2::ZERO, font, covers: Some(HashMap::new()), + custom_images: Some(HashMap::new()), // font: Font::new(include_bytes!("/usr/share/fonts/TTF/FiraSans-Regular.ttf")).unwrap(), last_draw: Instant::now(), modifiers: ModifiersState::default(), dragging: None, + no_animations, line_height, last_height: 720.0, scroll_pixels_multiplier, @@ -332,20 +416,20 @@ impl Gui { /// the trait implemented by all Gui elements. /// feel free to override the methods you wish to use. #[allow(unused)] -pub trait GuiElemTrait: 'static { +pub trait GuiElem { 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 and change draw_rev to false. - fn children(&mut self) -> Box + '_>; + fn children(&mut self) -> Box + '_>; /// defaults to true. fn draw_rev(&self) -> bool { true } fn any(&self) -> &dyn Any; fn any_mut(&mut self) -> &mut dyn Any; - fn elem(&self) -> &dyn GuiElemTrait; - fn elem_mut(&mut self) -> &mut dyn GuiElemTrait; + fn elem(&self) -> &dyn GuiElem; + fn elem_mut(&mut self) -> &mut dyn GuiElem; /// 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. @@ -393,7 +477,9 @@ pub trait GuiElemTrait: 'static { } fn updated_library(&mut self) {} fn updated_queue(&mut self) {} - +} +impl GuiElemInternal for T {} +pub(crate) trait GuiElemInternal: GuiElem { fn _draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) { if !self.config_mut().enabled { return; @@ -436,7 +522,7 @@ pub trait GuiElemTrait: 'static { self.config_mut().pixel_pos = std::mem::replace(&mut info.pos, ppos); } /// recursively applies the function to all gui elements below and including this one - fn _recursive_all(&mut self, f: &mut dyn FnMut(&mut dyn GuiElemTrait)) { + fn _recursive_all(&mut self, f: &mut dyn FnMut(&mut dyn GuiElem)) { f(self.elem_mut()); for c in self.children() { c._recursive_all(f); @@ -444,7 +530,7 @@ pub trait GuiElemTrait: 'static { } fn _mouse_event( &mut self, - condition: &mut dyn FnMut(&mut dyn GuiElemTrait) -> Option>, + condition: &mut dyn FnMut(&mut dyn GuiElem) -> Option>, pos: Vec2, ) -> Option> { for c in &mut self.children() { @@ -483,7 +569,7 @@ pub trait GuiElemTrait: 'static { ) -> Option> { if down { self._mouse_event( - &mut |v: &mut dyn GuiElemTrait| { + &mut |v: &mut dyn GuiElem| { if v.config().mouse_events { match button { MouseButton::Left => v.config_mut().mouse_down.0 = true, @@ -501,7 +587,7 @@ pub trait GuiElemTrait: 'static { } else { let mut vec = vec![]; if let Some(a) = self._mouse_event( - &mut |v: &mut dyn GuiElemTrait| { + &mut |v: &mut dyn GuiElem| { let down = v.config().mouse_down; if v.config().mouse_events && ((button == MouseButton::Left && down.0) @@ -545,8 +631,8 @@ pub trait GuiElemTrait: 'static { } fn _keyboard_event( &mut self, - f_focus: &mut dyn FnMut(&mut dyn GuiElemTrait, &mut Vec), - f_watch: &mut dyn FnMut(&mut dyn GuiElemTrait, &mut Vec), + f_focus: &mut dyn FnMut(&mut dyn GuiElem, &mut Vec), + f_watch: &mut dyn FnMut(&mut dyn GuiElem, &mut Vec), ) -> Vec { let mut o = vec![]; self._keyboard_event_inner(&mut Some(f_focus), f_watch, &mut o, true); @@ -554,8 +640,8 @@ pub trait GuiElemTrait: 'static { } fn _keyboard_event_inner( &mut self, - f_focus: &mut Option<&mut dyn FnMut(&mut dyn GuiElemTrait, &mut Vec)>, - f_watch: &mut dyn FnMut(&mut dyn GuiElemTrait, &mut Vec), + f_focus: &mut Option<&mut dyn FnMut(&mut dyn GuiElem, &mut Vec)>, + f_watch: &mut dyn FnMut(&mut dyn GuiElem, &mut Vec), events: &mut Vec, focus: bool, ) { @@ -620,28 +706,149 @@ pub trait GuiElemTrait: 'static { index != usize::MAX || wants } } -pub trait GuiElemChildren { - fn iter(&mut self) -> Box + '_>; + +pub trait GuiElemWrapper { + fn as_elem(&self) -> &dyn GuiElem; + fn as_elem_mut(&mut self) -> &mut dyn GuiElem; } -impl GuiElemChildren for T { - fn iter(&mut self) -> Box + '_> { - Box::new([self.elem_mut()].into_iter()) +impl GuiElemWrapper for Box { + fn as_elem(&self) -> &dyn GuiElem { + self.as_ref() + } + fn as_elem_mut(&mut self) -> &mut dyn GuiElem { + self.as_mut() } } -impl GuiElemChildren for (A, B) { - fn iter(&mut self) -> Box + '_> { +impl GuiElemWrapper for Box { + fn as_elem(&self) -> &dyn GuiElem { + self.as_ref() + } + fn as_elem_mut(&mut self) -> &mut dyn GuiElem { + self.as_mut() + } +} + +impl GuiElem for T { + fn config(&self) -> &GuiElemCfg { + self.as_elem().config() + } + fn config_mut(&mut self) -> &mut GuiElemCfg { + self.as_elem_mut().config_mut() + } + fn children(&mut self) -> Box + '_> { + self.as_elem_mut().children() + } + fn draw_rev(&self) -> bool { + self.as_elem().draw_rev() + } + fn any(&self) -> &dyn Any { + self.as_elem().any() + } + fn any_mut(&mut self) -> &mut dyn Any { + self.as_elem_mut().any_mut() + } + fn elem(&self) -> &dyn GuiElem { + self.as_elem().elem() + } + fn elem_mut(&mut self) -> &mut dyn GuiElem { + self.as_elem_mut().elem_mut() + } + fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) { + self.as_elem_mut().draw(info, g) + } + fn mouse_down(&mut self, button: MouseButton) -> Vec { + self.as_elem_mut().mouse_down(button) + } + fn mouse_up(&mut self, button: MouseButton) -> Vec { + self.as_elem_mut().mouse_up(button) + } + fn mouse_pressed(&mut self, button: MouseButton) -> Vec { + self.as_elem_mut().mouse_pressed(button) + } + fn mouse_wheel(&mut self, diff: f32) -> Vec { + self.as_elem_mut().mouse_wheel(diff) + } + fn char_watch(&mut self, modifiers: ModifiersState, key: char) -> Vec { + self.as_elem_mut().char_watch(modifiers, key) + } + fn char_focus(&mut self, modifiers: ModifiersState, key: char) -> Vec { + self.as_elem_mut().char_focus(modifiers, key) + } + fn key_watch( + &mut self, + modifiers: ModifiersState, + down: bool, + key: Option, + scan: KeyScancode, + ) -> Vec { + self.as_elem_mut().key_watch(modifiers, down, key, scan) + } + fn key_focus( + &mut self, + modifiers: ModifiersState, + down: bool, + key: Option, + scan: KeyScancode, + ) -> Vec { + self.as_elem_mut().key_focus(modifiers, down, key, scan) + } + fn dragged(&mut self, dragged: Dragging) -> Vec { + self.as_elem_mut().dragged(dragged) + } + fn updated_library(&mut self) { + self.as_elem_mut().updated_library() + } + fn updated_queue(&mut self) { + self.as_elem_mut().updated_queue() + } +} + +pub trait GuiElemChildren { + fn iter(&mut self) -> Box + '_>; + fn len(&self) -> usize; +} +impl GuiElemChildren for () { + fn iter(&mut self) -> Box + '_> { + Box::new([].into_iter()) + } + fn len(&self) -> usize { + 0 + } +} +impl GuiElemChildren for [T; N] { + fn iter(&mut self) -> Box + '_> { + Box::new(self.iter_mut().map(|v| v.elem_mut())) + } + fn len(&self) -> usize { + N + } +} +impl GuiElemChildren for Vec { + fn iter(&mut self) -> Box + '_> { + Box::new(self.iter_mut().map(|v| v.elem_mut())) + } + fn len(&self) -> usize { + self.len() + } +} +impl GuiElemChildren for (A, B) { + fn iter(&mut self) -> Box + '_> { Box::new([self.0.elem_mut(), self.1.elem_mut()].into_iter()) } -} -impl GuiElemChildren for (A, B, C) { - fn iter(&mut self) -> Box + '_> { - Box::new([self.0.elem_mut(), self.1.elem_mut(), self.2.elem_mut()].into_iter()) + fn len(&self) -> usize { + 2 } } -impl GuiElemChildren - for (A, B, C, D) -{ - fn iter(&mut self) -> Box + '_> { +impl GuiElemChildren for (A, B, C) { + fn iter(&mut self) -> Box + '_> { + Box::new([self.0.elem_mut(), self.1.elem_mut(), self.2.elem_mut()].into_iter()) + } + fn len(&self) -> usize { + 3 + } +} +impl GuiElemChildren for (A, B, C, D) { + fn iter(&mut self) -> Box + '_> { Box::new( [ self.0.elem_mut(), @@ -652,11 +859,14 @@ impl GuiElem .into_iter(), ) } + fn len(&self) -> usize { + 4 + } } -impl - GuiElemChildren for (A, B, C, D, E) +impl GuiElemChildren + for (A, B, C, D, E) { - fn iter(&mut self) -> Box + '_> { + fn iter(&mut self) -> Box + '_> { Box::new( [ self.0.elem_mut(), @@ -668,6 +878,9 @@ impl usize { + 5 + } } #[derive(Debug, Clone)] @@ -703,6 +916,7 @@ pub struct GuiElemCfg { /// if this is true, things can be dragged into this element via drag-n-drop pub drag_target: bool, } +#[allow(unused)] impl GuiElemCfg { pub fn at(pos: Rectangle) -> Self { Self { @@ -757,15 +971,17 @@ impl Default for GuiElemCfg { } } } +#[allow(unused)] pub enum GuiAction { OpenMain, SetIdle(bool), + SetAnimationsDisabled(bool), OpenSettings(bool), - OpenEditPanel(Box), - CloseEditPanel, + ShowNotification(Box (Box, NotifInfo) + Send>), /// Build the GuiAction(s) later, when we have access to the Database (can turn an AlbumId into a QueueContent::Folder, etc) Build(Box Vec>), SendToServer(Command), + ContextMenu(Option>), /// unfocuses all gui elements, then assigns keyboard focus to one with config().request_keyboard_focus == true if there is one. ResetKeyboardFocus, SetDragging( @@ -791,6 +1007,7 @@ pub enum Dragging { /// GuiElems have access to this within draw. /// Except for `actions`, they should not change any of these values - GuiElem::draw will handle everything automatically. pub struct DrawInfo<'a> { + pub time: Instant, pub actions: Vec, pub pos: Rectangle, pub database: &'a mut Database, @@ -800,7 +1017,8 @@ pub struct DrawInfo<'a> { pub mouse_pos: Vec2, pub helper: Option<&'a mut WindowHelper>, pub get_con: Arc>>, - pub covers: &'a mut HashMap, + pub covers: &'a mut HashMap, + pub custom_images: &'a mut HashMap, pub has_keyboard_focus: bool, pub child_has_keyboard_focus: bool, /// the height of one line of text (in pixels) @@ -809,7 +1027,9 @@ pub struct DrawInfo<'a> { Dragging, Option>, )>, - pub gui_config: &'a GuiConfig, + pub context_menu: Option>, + pub gui_config: &'a mut GuiConfig, + pub no_animations: bool, } pub fn adjust_area(outer: &Rectangle, rel_area: &Rectangle) -> Rectangle { @@ -839,8 +1059,11 @@ impl Gui { eprintln!("Error sending command to server: {e}"); } } + GuiAction::ShowNotification(func) => _ = self.notif_sender.send(func), GuiAction::ResetKeyboardFocus => _ = self.gui._keyboard_reset_focus(), GuiAction::SetDragging(d) => self.dragging = d, + GuiAction::SetAnimationsDisabled(d) => self.no_animations = d, + GuiAction::ContextMenu(m) => self.gui.c_context_menu = m, GuiAction::SetLineHeight(h) => { self.line_height = h; self.gui @@ -850,77 +1073,25 @@ impl Gui { self.covers .as_mut() .unwrap() - .insert(id, GuiCover::new(id, Arc::clone(&self.get_con))); + .insert(id, GuiServerImage::new_cover(id, Arc::clone(&self.get_con))); } GuiAction::Do(mut f) => f(self), GuiAction::Exit => _ = self.event_sender.send_event(GuiEvent::Exit), GuiAction::SetIdle(v) => { - if let Some(gui) = self - .gui - .any_mut() - .downcast_mut::>() - { - if gui.inner.idle.0 != v { - gui.inner.idle = (v, Some(Instant::now())); - } - } + self.gui.idle.target = if v { 1.0 } else { 0.0 }; } GuiAction::OpenSettings(v) => { - if let Some(gui) = self - .gui - .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())); - } + self.gui.idle.target = 0.0; + self.gui.last_interaction = Instant::now(); + if self.gui.settings.0 != v { + self.gui.settings = (v, Some(Instant::now())); } } GuiAction::OpenMain => { - if let Some(gui) = self - .gui - .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())); - } - } - } - GuiAction::OpenEditPanel(p) => { - if let Some(gui) = self - .gui - .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())); - } - gui.inner.open_edit(p); - } - } - GuiAction::CloseEditPanel => { - if let Some(gui) = self - .gui - .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())); - } - gui.inner.close_edit(); + self.gui.idle.target = 0.0; + self.gui.last_interaction = Instant::now(); + if self.gui.settings.0 { + self.gui.settings = (false, Some(Instant::now())); } } } @@ -935,8 +1106,10 @@ impl WindowHandler for Gui { ); let mut dblock = self.database.lock().unwrap(); let mut covers = self.covers.take().unwrap(); - let cfg = self.gui_config.take().unwrap(); + let mut custom_images = self.custom_images.take().unwrap(); + let mut cfg = self.gui_config.take().unwrap(); let mut info = DrawInfo { + time: Instant::now(), actions: Vec::with_capacity(0), pos: Rectangle::new(Vec2::ZERO, self.size.into_f32()), database: &mut *dblock, @@ -944,15 +1117,19 @@ impl WindowHandler for Gui { mouse_pos: self.mouse_pos, get_con: Arc::clone(&self.get_con), covers: &mut covers, + custom_images: &mut custom_images, helper: Some(helper), has_keyboard_focus: false, child_has_keyboard_focus: true, line_height: self.line_height, + no_animations: self.no_animations, dragging: self.dragging.take(), - gui_config: &cfg, + context_menu: self.gui.c_context_menu.take(), + gui_config: &mut cfg, }; self.gui._draw(&mut info, graphics); let actions = std::mem::replace(&mut info.actions, Vec::with_capacity(0)); + self.gui.c_context_menu = info.context_menu.take(); self.dragging = info.dragging.take(); if let Some((d, f)) = &mut self.dragging { if let Some(f) = f { @@ -991,6 +1168,7 @@ impl WindowHandler for Gui { drop(info); self.gui_config = Some(cfg); self.covers = Some(covers); + self.custom_images = Some(custom_images); drop(dblock); for a in actions { self.exec_gui_action(a); @@ -1172,13 +1350,14 @@ impl WindowHandler for Gui { } } -pub enum GuiCover { +pub enum GuiServerImage { Loading(JoinHandle>>), Loaded(ImageHandle), Error, } -impl GuiCover { - pub fn new(id: CoverId, get_con: Arc>>) -> Self { +#[allow(unused)] +impl GuiServerImage { + pub fn new_cover(id: CoverId, get_con: Arc>>) -> Self { Self::Loading(std::thread::spawn(move || { get_con .lock() @@ -1188,12 +1367,25 @@ impl GuiCover { .and_then(|v| v.ok()) })) } + pub fn new_custom_file(file: String, get_con: Arc>>) -> Self { + Self::Loading(std::thread::spawn(move || { + get_con + .lock() + .unwrap() + .custom_file(&file) + .ok() + .and_then(|v| v.ok()) + })) + } pub fn get(&self) -> Option { match self { Self::Loaded(handle) => Some(handle.clone()), Self::Loading(_) | Self::Error => None, } } + pub fn is_err(&self) -> bool { + matches!(self, Self::Error) + } pub fn get_init(&mut self, g: &mut Graphics2D) -> Option { match self { Self::Loaded(handle) => Some(handle.clone()), diff --git a/musicdb-client/src/gui_anim.rs b/musicdb-client/src/gui_anim.rs new file mode 100644 index 0000000..db29b12 --- /dev/null +++ b/musicdb-client/src/gui_anim.rs @@ -0,0 +1,130 @@ +use std::{ + ops::{Add, AddAssign, Mul, MulAssign, Sub}, + time::{Duration, Instant}, +}; + +pub struct AnimationController { + pub last_updated: Instant, + pub value: F, + pub speed: F, + pub max_speed: F, + pub target: F, + /// while the time remaining to finish the animation is above this, we accelerate (higher -> stop acceleration earlier) + pub accel_until: F, + /// if the time remaining to finish the animation drops below this, we decelerate (higher -> start decelerating earlier) + pub decel_while: F, + pub acceleration: F, +} + +pub trait Float: + Sized + + Clone + + Copy + + Add + + Sub + + std::ops::Neg + + Mul + + MulAssign + + AddAssign + + PartialOrd +{ + fn zero() -> Self; + /// 1/1000 + fn milli() -> Self; + fn duration_secs(d: Duration) -> Self; + fn abs(self) -> Self { + if self < Self::zero() { + -self + } else { + self + } + } +} + +impl AnimationController { + pub fn new( + value: F, + target: F, + acceleration: F, + max_speed: F, + accel_until: F, + decel_while: F, + now: Instant, + ) -> Self { + AnimationController { + last_updated: now, + value, + speed: F::zero(), + max_speed, + target, + accel_until, + decel_while, + acceleration, + } + } + pub fn ignore_elapsed_time(&mut self, now: Instant) { + self.last_updated = now; + } + pub fn update(&mut self, now: Instant, instant: bool) -> bool { + let changed = if self.target != self.value { + if instant { + self.value = self.target; + } else { + let inc = self.target > self.value; + let seconds = F::duration_secs(now.duration_since(self.last_updated)); + let ref1 = self.value + self.speed * self.accel_until; + let ref2 = self.value + self.speed * self.decel_while; + let speed_diff = match (ref1 < self.target, ref2 > self.target) { + (true, false) => self.acceleration, + (false, true) => -self.acceleration, + (true, true) | (false, false) => F::zero(), + }; + self.speed += speed_diff; + if self.speed.abs() > self.max_speed { + if self.speed < F::zero() { + self.speed = -self.max_speed; + } else { + self.speed = self.max_speed; + } + } + self.value += self.speed * seconds; + self.speed += speed_diff; + if (self.target - self.value).abs() < self.speed * F::milli() + || inc != (self.target > self.value) + { + // overshoot or target reached + self.value = self.target; + self.speed = F::zero(); + } + } + true + } else { + false + }; + self.last_updated = now; + changed + } +} + +impl Float for f32 { + fn zero() -> Self { + 0.0 + } + fn milli() -> Self { + 0.001 + } + fn duration_secs(d: Duration) -> Self { + d.as_secs_f32().min(0.1) + } +} +impl Float for f64 { + fn zero() -> Self { + 0.0 + } + fn milli() -> Self { + 0.001 + } + fn duration_secs(d: Duration) -> Self { + d.as_secs_f64().min(0.1) + } +} diff --git a/musicdb-client/src/gui_base.rs b/musicdb-client/src/gui_base.rs index ccffd9d..85221aa 100755 --- a/musicdb-client/src/gui_base.rs +++ b/musicdb-client/src/gui_base.rs @@ -3,7 +3,7 @@ use std::{sync::Arc, time::Instant}; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::MouseButton}; use crate::{ - gui::{DrawInfo, GuiAction, GuiElemCfg, GuiElemTrait}, + gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemChildren}, gui_text::Label, }; @@ -15,24 +15,20 @@ Mostly containers for other GuiElems. */ /// A simple container for zero, one, or multiple child GuiElems. Can optionally fill the background with a color. -pub struct Panel { +pub struct Panel { config: GuiElemCfg, - pub children: Vec>, + pub children: C, pub background: Option, } -impl Panel { - pub fn new(config: GuiElemCfg, children: Vec>) -> Self { +impl Panel { + pub fn new(config: GuiElemCfg, children: C) -> Self { Self { config, children, background: None, } } - pub fn with_background( - config: GuiElemCfg, - children: Vec>, - background: Color, - ) -> Self { + pub fn with_background(config: GuiElemCfg, children: C, background: Color) -> Self { Self { config, children, @@ -40,15 +36,15 @@ impl Panel { } } } -impl GuiElemTrait for Panel { +impl GuiElem 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().map(|v| v.as_mut())) + fn children(&mut self) -> Box + '_> { + self.children.iter() } fn any(&self) -> &dyn std::any::Any { self @@ -56,10 +52,10 @@ impl GuiElemTrait for Panel { fn any_mut(&mut self) -> &mut dyn std::any::Any { self } - fn elem(&self) -> &dyn GuiElemTrait { + fn elem(&self) -> &dyn GuiElem { self } - fn elem_mut(&mut self) -> &mut dyn GuiElemTrait { + fn elem_mut(&mut self) -> &mut dyn GuiElem { self } fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { @@ -70,24 +66,25 @@ impl GuiElemTrait for Panel { } #[derive(Clone)] -pub struct Square { +pub struct Square { config: GuiElemCfg, pub inner: T, } -impl Square { +#[allow(unused)] +impl Square { pub fn new(mut config: GuiElemCfg, inner: T) -> Self { config.redraw = true; Self { config, inner } } } -impl GuiElemTrait for Square { +impl GuiElem for Square { fn config(&self) -> &GuiElemCfg { &self.config } fn config_mut(&mut self) -> &mut GuiElemCfg { &mut self.config } - fn children(&mut self) -> Box + '_> { + fn children(&mut self) -> Box + '_> { Box::new([self.inner.elem_mut()].into_iter()) } fn any(&self) -> &dyn std::any::Any { @@ -96,10 +93,10 @@ impl GuiElemTrait for Square { fn any_mut(&mut self) -> &mut dyn std::any::Any { self } - fn elem(&self) -> &dyn GuiElemTrait { + fn elem(&self) -> &dyn GuiElem { self } - fn elem_mut(&mut self) -> &mut dyn GuiElemTrait { + fn elem_mut(&mut self) -> &mut dyn GuiElem { self } fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { @@ -121,9 +118,10 @@ impl GuiElemTrait for Square { } } -pub struct ScrollBox { +pub struct ScrollBox { config: GuiElemCfg, - pub children: Vec<(Box, f32)>, + pub children: C, + pub children_heights: Vec, pub size_unit: ScrollBoxSizeUnit, pub scroll_target: f32, pub scroll_display: f32, @@ -136,20 +134,22 @@ pub struct ScrollBox { mouse_scroll_margin_right: f32, } #[derive(Clone)] +#[allow(unused)] pub enum ScrollBoxSizeUnit { Relative, Pixels, } -impl ScrollBox { +impl ScrollBox { pub fn new( config: GuiElemCfg, size_unit: ScrollBoxSizeUnit, - children: Vec<(Box, f32)>, + children: C, + children_heights: Vec, ) -> Self { - // config.redraw = true; Self { config: config.w_scroll().w_mouse(), children, + children_heights, size_unit, scroll_target: 0.0, scroll_display: 0.0, @@ -163,21 +163,15 @@ impl ScrollBox { } } } -impl GuiElemTrait for ScrollBox { +impl GuiElem 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() - .map(|(v, _)| v.as_mut()) - .skip_while(|v| v.config().pos.bottom_right().y < 0.0) - .take_while(|v| v.config().pos.top_left().y <= 1.0), - ) + fn children(&mut self) -> Box + '_> { + self.children.iter() } fn draw_rev(&self) -> bool { false @@ -188,10 +182,10 @@ impl GuiElemTrait for ScrollBox { fn any_mut(&mut self) -> &mut dyn std::any::Any { self } - fn elem(&self) -> &dyn GuiElemTrait { + fn elem(&self) -> &dyn GuiElem { self } - fn elem_mut(&mut self) -> &mut dyn GuiElemTrait { + fn elem_mut(&mut self) -> &mut dyn GuiElem { self } fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { @@ -204,22 +198,37 @@ impl GuiElemTrait for ScrollBox { } 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 { + if self.scroll_target != self.scroll_display { self.config.redraw = true; - if (self.scroll_display - self.scroll_target).abs() < 1.0 / info.pos.height() { + if info.no_animations { self.scroll_display = self.scroll_target; - } else if let Some(h) = &info.helper { - h.request_redraw(); + } else { + self.scroll_display = 0.2 * self.scroll_target + 0.8 * self.scroll_display; + 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 { + // adjust height vector length if necessary + if self.children_heights.len() != self.children.len() { + let target = self.children.len(); + while self.children_heights.len() < target { + self.children_heights.push(0.0); + } + while self.children_heights.len() > target { + self.children_heights.pop(); + } + } + // self.mouse_scroll_margin_right = info.line_height * 0.2; let max_x = 1.0 - self.mouse_scroll_margin_right / info.pos.width(); self.config.redraw = false; let mut y_pos = -self.scroll_display; - for (e, h) in self.children.iter_mut() { + for (e, h) in self.children.iter().zip(self.children_heights.iter()) { 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 { @@ -315,17 +324,17 @@ impl ScrollBoxSizeUnit { } } -pub struct Button { +pub struct Button { config: GuiElemCfg, - pub children: Vec>, + pub children: C, action: Arc Vec + 'static>, } -impl Button { +impl Button { /// automatically adds w_mouse to config pub fn new Vec + 'static>( config: GuiElemCfg, action: F, - children: Vec>, + children: C, ) -> Self { Self { config: config.w_mouse(), @@ -334,15 +343,15 @@ impl Button { } } } -impl GuiElemTrait for Button { +impl GuiElem 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().map(|v| v.as_mut())) + fn children(&mut self) -> Box + '_> { + self.children.iter() } fn any(&self) -> &dyn std::any::Any { self @@ -350,10 +359,10 @@ impl GuiElemTrait for Button { fn any_mut(&mut self) -> &mut dyn std::any::Any { self } - fn elem(&self) -> &dyn GuiElemTrait { + fn elem(&self) -> &dyn GuiElem { self } - fn elem_mut(&mut self) -> &mut dyn GuiElemTrait { + fn elem_mut(&mut self) -> &mut dyn GuiElem { self } fn mouse_pressed(&mut self, button: MouseButton) -> Vec { @@ -381,7 +390,7 @@ impl GuiElemTrait for Button { pub struct Slider { pub config: GuiElemCfg, - pub children: Vec>, + pub children: Vec>, pub slider_pos: Rectangle, pub min: f64, pub max: f64, @@ -395,6 +404,7 @@ pub struct Slider { pub display_since: Option, pub on_update: Arc, } +#[allow(unused)] 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 @@ -418,7 +428,7 @@ impl Slider { min: f64, max: f64, val: f64, - children: Vec>, + children: Vec>, on_update: F, ) -> Self { Self { @@ -507,15 +517,15 @@ impl Slider { ) } } -impl GuiElemTrait for Slider { +impl GuiElem for Slider { 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().map(|v| v.as_mut())) + fn children(&mut self) -> Box + '_> { + Box::new(self.children.iter_mut().map(|v| v.elem_mut())) } fn any(&self) -> &dyn std::any::Any { self @@ -523,10 +533,10 @@ impl GuiElemTrait for Slider { fn any_mut(&mut self) -> &mut dyn std::any::Any { self } - fn elem(&self) -> &dyn GuiElemTrait { + fn elem(&self) -> &dyn GuiElem { self } - fn elem_mut(&mut self) -> &mut dyn GuiElemTrait { + fn elem_mut(&mut self) -> &mut dyn GuiElem { self } fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { diff --git a/musicdb-client/src/gui_edit.rs b/musicdb-client/src/gui_edit.rs deleted file mode 100755 index 1222218..0000000 --- a/musicdb-client/src/gui_edit.rs +++ /dev/null @@ -1,637 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - sync::mpsc, -}; - -use musicdb_lib::{ - data::{ - album::Album, artist::Artist, queue::QueueContent, song::Song, AlbumId, ArtistId, CoverId, - SongId, - }, - server::Command, -}; -use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle}; - -use crate::{ - gui::{Dragging, DrawInfo, GuiAction, GuiElemCfg, GuiElemTrait}, - gui_base::{Button, Panel, ScrollBox}, - gui_text::{Label, TextField}, -}; - -pub struct GuiEdit { - config: GuiElemCfg, - children: Vec>, - editable: Editable, - editing: Editing, - reload: bool, - rebuild_main: bool, - rebuild_changes: bool, - send: bool, - apply_change: mpsc::Sender>, - change_recv: mpsc::Receiver>, -} -#[derive(Clone)] -pub enum Editable { - Artist(Vec), - Album(Vec), - Song(Vec), -} -#[derive(Clone)] -pub enum Editing { - NotLoaded, - Artist(Vec, Vec), - Album(Vec, Vec), - Song(Vec, Vec), -} -#[derive(Clone)] -pub enum ArtistChange { - SetName(String), - SetCover(Option), - AddAlbum(AlbumId), -} -#[derive(Clone)] -pub enum AlbumChange { - SetName(String), - SetCover(Option), - RemoveSong(SongId), - AddSong(SongId), -} -#[derive(Clone)] -pub enum SongChange { - SetTitle(String), - SetCover(Option), -} - -impl GuiEdit { - pub fn new(config: GuiElemCfg, edit: Editable) -> Self { - let (apply_change, change_recv) = mpsc::channel(); - let ac1 = apply_change.clone(); - let ac2 = apply_change.clone(); - Self { - config: config.w_drag_target(), - editable: edit, - editing: Editing::NotLoaded, - reload: true, - rebuild_main: true, - rebuild_changes: true, - send: false, - apply_change, - change_recv, - children: vec![ - Box::new(ScrollBox::new( - GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.6))), - crate::gui_base::ScrollBoxSizeUnit::Pixels, - vec![], - )), - Box::new(ScrollBox::new( - GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.6), (1.0, 0.9))), - crate::gui_base::ScrollBoxSizeUnit::Pixels, - vec![], - )), - Box::new(Button::new( - GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.95), (0.33, 1.0))), - |_| vec![GuiAction::CloseEditPanel], - vec![Box::new(Label::new( - GuiElemCfg::default(), - "Back".to_string(), - Color::WHITE, - None, - Vec2::new(0.5, 0.5), - ))], - )), - Box::new(Button::new( - GuiElemCfg::at(Rectangle::from_tuples((0.33, 0.95), (0.67, 1.0))), - move |_| { - _ = ac1.send(Box::new(|s| s.reload = true)); - vec![] - }, - vec![Box::new(Label::new( - GuiElemCfg::default(), - "Reload".to_string(), - Color::WHITE, - None, - Vec2::new(0.5, 0.5), - ))], - )), - Box::new(Button::new( - GuiElemCfg::at(Rectangle::from_tuples((0.67, 0.95), (1.0, 1.0))), - move |_| { - _ = ac2.send(Box::new(|s| s.send = true)); - vec![] - }, - vec![Box::new(Label::new( - GuiElemCfg::default(), - "Send".to_string(), - Color::WHITE, - None, - Vec2::new(0.5, 0.5), - ))], - )), - ], - } - } -} -impl GuiElemTrait for GuiEdit { - 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().map(|v| v.as_mut())) - } - fn any(&self) -> &dyn std::any::Any { - self - } - fn any_mut(&mut self) -> &mut dyn std::any::Any { - self - } - fn elem(&self) -> &dyn GuiElemTrait { - self - } - fn elem_mut(&mut self) -> &mut dyn GuiElemTrait { - self - } - fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) { - loop { - if let Ok(func) = self.change_recv.try_recv() { - func(self); - } else { - break; - } - } - if self.send { - self.send = false; - self.rebuild_main = true; - self.rebuild_changes = true; - self.config.redraw = true; - match &mut self.editing { - Editing::NotLoaded => {} - Editing::Artist(v, changes) => { - for change in changes.iter() { - match change { - ArtistChange::SetName(n) => { - for artist in v.iter_mut() { - artist.name = n.clone(); - info.actions.push(GuiAction::SendToServer( - Command::ModifyArtist(artist.clone()), - )); - } - } - ArtistChange::SetCover(c) => { - for artist in v.iter_mut() { - artist.cover = c.clone(); - info.actions.push(GuiAction::SendToServer( - Command::ModifyArtist(artist.clone()), - )); - } - } - ArtistChange::AddAlbum(id) => { - // use the first artist for the artist fields - let mut editing = v.first().unwrap().clone(); - if let Some(album) = info.database.albums().get(id) { - let mut album = album.clone(); - // find the previous artist for this album and remove them - if let Some(prev) = info.database.artists().get(&album.artist) { - let mut prev = prev.clone(); - if let Some(i) = prev.albums.iter().position(|v| v == id) { - prev.albums.remove(i); - info.actions.push(GuiAction::SendToServer( - Command::ModifyArtist(prev), - )); - } - } - // update the artist field on the album so it points to the new artist - album.artist = editing.id; - info.actions - .push(GuiAction::SendToServer(Command::ModifyAlbum(album))); - // add the album to the artist we are editing - if !editing.albums.contains(id) { - editing.albums.push(*id); - info.actions.push(GuiAction::SendToServer( - Command::ModifyArtist(editing), - )); - } - } - } - } - } - } - Editing::Album(v, changes) => { - for v in v { - let mut v = v.clone(); - for change in changes.iter() { - todo!() - } - info.actions - .push(GuiAction::SendToServer(Command::ModifyAlbum(v))); - } - } - Editing::Song(v, changes) => { - for v in v { - let mut v = v.clone(); - for change in changes.iter() { - todo!() - } - info.actions - .push(GuiAction::SendToServer(Command::ModifySong(v))); - } - } - } - } - if self.reload { - self.reload = false; - let prev = std::mem::replace(&mut self.editing, Editing::NotLoaded); - self.editing = match &self.editable { - Editable::Artist(id) => { - let v = id - .iter() - .filter_map(|id| info.database.artists().get(id).cloned()) - .collect::>(); - if !v.is_empty() { - Editing::Artist( - v, - if let Editing::Artist(_, c) = prev { - c - } else { - vec![] - }, - ) - } else { - Editing::NotLoaded - } - } - Editable::Album(id) => { - let v = id - .iter() - .filter_map(|id| info.database.albums().get(id).cloned()) - .collect::>(); - if !v.is_empty() { - Editing::Album( - v, - if let Editing::Album(_, c) = prev { - c - } else { - vec![] - }, - ) - } else { - Editing::NotLoaded - } - } - Editable::Song(id) => { - let v = id - .iter() - .filter_map(|id| info.database.songs().get(id).cloned()) - .collect::>(); - if !v.is_empty() { - Editing::Song( - v, - if let Editing::Song(_, c) = prev { - c - } else { - vec![] - }, - ) - } else { - Editing::NotLoaded - } - } - }; - self.config.redraw = true; - self.rebuild_main = true; - self.rebuild_changes = true; - } - if let Some(sb) = self.children[0].any_mut().downcast_mut::() { - for (c, _) in sb.children.iter() { - if let Some(p) = c - .any() - .downcast_ref::() - .and_then(|p| p.children.get(0)) - .and_then(|e| e.any().downcast_ref::()) - { - if p.label_input().content.will_redraw() { - if let Some((key, _)) = p.label_hint().content.get_text().split_once(':') { - match (&mut self.editing, key) { - (Editing::Artist(_, changes), "name") => { - let mut c = changes.iter_mut(); - loop { - if let Some(c) = c.next() { - if let ArtistChange::SetName(n) = c { - *n = p.label_input().content.get_text().clone(); - break; - } - } else { - changes.push(ArtistChange::SetName( - p.label_input().content.get_text().clone(), - )); - break; - } - } - self.rebuild_changes = true; - } - (Editing::Artist(_, changes), "cover") => { - let mut c = changes.iter_mut(); - loop { - if let Some(c) = c.next() { - if let ArtistChange::SetCover(n) = c { - *n = - p.label_input().content.get_text().parse().ok(); - break; - } - } else { - changes.push(ArtistChange::SetCover( - p.label_input().content.get_text().parse().ok(), - )); - break; - } - } - self.rebuild_changes = true; - } - _ => {} - } - } - } - } - } - } - if self.rebuild_main { - self.rebuild_main = false; - self.rebuild_main(info); - } - if self.rebuild_changes { - self.rebuild_changes = false; - self.rebuild_changes(info); - } - if self.config.redraw { - self.config.redraw = false; - if let Some(sb) = self.children[0].any_mut().downcast_mut::() { - for c in sb.children.iter_mut() { - c.1 = info.line_height; - } - } - } - } - fn dragged(&mut self, dragged: Dragging) -> Vec { - let dragged = match dragged { - Dragging::Artist(_) | Dragging::Album(_) | Dragging::Song(_) | Dragging::Queues(_) => { - dragged - } - Dragging::Queue(q) => match q.content() { - QueueContent::Song(id) => Dragging::Song(*id), - _ => Dragging::Queue(q), - }, - }; - match dragged { - Dragging::Artist(id) => { - if let Editing::Artist(a, _) = &self.editing { - self.editable = Editable::Artist(a.iter().map(|v| v.id).chain([id]).collect()) - } - } - Dragging::Album(id) => { - if let Editing::Album(a, _) = &self.editing { - self.editable = Editable::Album(a.iter().map(|v| v.id).chain([id]).collect()) - } - } - Dragging::Song(id) => { - if let Editing::Song(a, _) = &self.editing { - self.editable = Editable::Song(a.iter().map(|v| v.id).chain([id]).collect()) - } - } - Dragging::Queue(_) => return vec![], - Dragging::Queues(_) => return vec![], - } - self.reload = true; - vec![] - } -} -impl GuiEdit { - fn rebuild_main(&mut self, info: &mut DrawInfo) { - if let Some(sb) = self.children[0].any_mut().downcast_mut::() { - sb.children.clear(); - sb.config_mut().redraw = true; - match &self.editing { - Editing::NotLoaded => {} - Editing::Artist(v, _) => { - // name - let mut names = v - .iter() - .map(|v| &v.name) - .collect::>() - .into_iter() - .collect::>(); - names.sort_unstable(); - let name = if names.len() == 1 { - format!("name: {}", names[0]) - } else { - let mut name = format!("name: {}", names[0]); - for n in names.iter().skip(1) { - name.push_str(" / "); - name.push_str(n); - } - name - }; - sb.children.push(( - Box::new(Panel::new( - GuiElemCfg::default(), - vec![Box::new(TextField::new( - GuiElemCfg::default(), - name, - Color::LIGHT_GRAY, - Color::WHITE, - ))], - )), - info.line_height, - )); - // cover - let covers = v.iter().filter_map(|v| v.cover).collect::>(); - let cover = if covers.is_empty() { - format!("cover: None") - } else { - let mut cover = format!("cover: {}", covers[0]); - for c in covers.iter().skip(1) { - cover.push('/'); - cover.push_str(&format!("{c}")); - } - cover - }; - sb.children.push(( - Box::new(Panel::new( - GuiElemCfg::default(), - vec![Box::new(TextField::new( - GuiElemCfg::default(), - cover, - Color::LIGHT_GRAY, - Color::WHITE, - ))], - )), - info.line_height, - )); - // albums - let mut albums = HashMap::new(); - for v in v { - for album in &v.albums { - if let Some(count) = albums.get_mut(album) { - *count += 1; - } else { - albums.insert(*album, 1); - } - } - } - { - fn get_id(s: &mut GuiEdit) -> Option { - s.children[0] - .children() - .collect::>() - .into_iter() - .rev() - .nth(2) - .unwrap() - .any_mut() - .downcast_mut::() - .unwrap() - .children() - .next() - .unwrap() - .any_mut() - .downcast_mut::() - .unwrap() - .label_input() - .content - .get_text() - .parse() - .ok() - } - let add_button = { - let apply_change = self.apply_change.clone(); - Box::new(Button::new( - GuiElemCfg::at(Rectangle::from_tuples((0.9, 0.0), (1.0, 1.0))), - move |_| { - _ = apply_change.send(Box::new(move |s| { - if let Some(album_id) = get_id(s) { - if let Editing::Artist(_, c) = &mut s.editing { - if let Some(i) = c.iter().position(|c| { - matches!(c, ArtistChange::AddAlbum(id) if *id == album_id) - }) { - c.remove(i); - } - c.push(ArtistChange::AddAlbum(album_id)); - s.rebuild_changes = true; - } - } - })); - vec![] - }, - vec![Box::new(Label::new( - GuiElemCfg::default(), - format!("add"), - Color::GREEN, - None, - Vec2::new(0.5, 0.5), - ))], - )) - }; - let name = Box::new(TextField::new( - GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (0.9, 1.0))), - "add album by id".to_string(), - Color::LIGHT_GRAY, - Color::WHITE, - )); - sb.children.push(( - Box::new(Panel::new(GuiElemCfg::default(), vec![name, add_button])), - info.line_height * 2.0, - )); - } - for (album_id, count) in albums { - let album = info.database.albums().get(&album_id); - let name = Box::new(Button::new( - GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 1.0))), - move |_| { - vec![GuiAction::OpenEditPanel(Box::new(GuiEdit::new( - GuiElemCfg::default(), - Editable::Album(vec![album_id]), - )))] - }, - vec![Box::new(Label::new( - GuiElemCfg::default(), - if let Some(a) = album { - a.name.clone() - } else { - format!("#{album_id}") - }, - Color::WHITE, - None, - Vec2::new(0.0, 0.5), - ))], - )); - sb.children.push(( - Box::new(Panel::new(GuiElemCfg::default(), vec![name])), - info.line_height, - )); - } - } - Editing::Album(v, _) => {} - Editing::Song(v, _) => {} - } - } - } - fn rebuild_changes(&mut self, info: &mut DrawInfo) { - if let Some(sb) = self.children[1].any_mut().downcast_mut::() { - sb.children.clear(); - sb.config_mut().redraw = true; - match &self.editing { - Editing::NotLoaded => {} - Editing::Artist(_, a) => { - for (i, v) in a.iter().enumerate() { - let text = match v { - ArtistChange::SetName(v) => format!("set name to \"{v}\""), - ArtistChange::SetCover(c) => { - if let Some(c) = c { - format!("set cover to {c}") - } else { - "remove cover".to_string() - } - } - ArtistChange::AddAlbum(v) => format!("add album {v}"), - }; - let s = self.apply_change.clone(); - sb.children.push(( - Box::new(Button::new( - GuiElemCfg::default(), - move |_| { - _ = s.send(Box::new(move |s| { - if !s.rebuild_changes { - if let Editing::Artist(_, v) = &mut s.editing { - if i < v.len() { - v.remove(i); - } - s.rebuild_changes = true; - } - } - })); - vec![] - }, - vec![Box::new(Label::new( - GuiElemCfg::default(), - text, - Color::WHITE, - None, - Vec2::new(0.0, 0.5), - ))], - )), - info.line_height, - )); - } - } - _ => todo!(), - } - } - } -} - -impl Clone for GuiEdit { - fn clone(&self) -> Self { - Self::new(self.config.clone(), self.editable.clone()) - } -} diff --git a/musicdb-client/src/gui_idle_display.rs b/musicdb-client/src/gui_idle_display.rs new file mode 100644 index 0000000..a1c14c4 --- /dev/null +++ b/musicdb-client/src/gui_idle_display.rs @@ -0,0 +1,271 @@ +use std::{sync::Arc, time::Instant}; + +use musicdb_lib::data::ArtistId; +use speedy2d::{color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle}; + +use crate::{ + gui::{DrawInfo, GuiElem, GuiElemCfg, GuiServerImage}, + gui_anim::AnimationController, + gui_playback::{get_right_x, image_display, CurrentInfo}, + gui_text::AdvancedLabel, +}; + +pub struct IdleDisplay { + config: GuiElemCfg, + pub idle_mode: f32, + current_info: CurrentInfo, + current_artist_image: Option<(ArtistId, Option<(String, Option>)>)>, + c_top_label: AdvancedLabel, + c_side1_label: AdvancedLabel, + c_side2_label: AdvancedLabel, + cover_aspect_ratio: AnimationController, + artist_image_aspect_ratio: AnimationController, + cover_left: f32, + cover_top: f32, + cover_bottom: f32, + /// 0.0 -> same height as cover, + /// 0.5 -> lower half of cover + artist_image_top: f32, + artist_image_to_cover_margin: f32, +} + +impl IdleDisplay { + pub fn new(config: GuiElemCfg) -> Self { + Self { + config, + idle_mode: 0.0, + current_info: CurrentInfo::new(), + current_artist_image: None, + c_top_label: AdvancedLabel::new( + GuiElemCfg::at(Rectangle::from_tuples((0.05, 0.02), (0.95, 0.18))), + Vec2::new(0.5, 0.5), + vec![], + ), + c_side1_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]), + c_side2_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]), + cover_aspect_ratio: AnimationController::new( + 1.0, + 1.0, + 0.01, + 1.0, + 0.8, + 0.6, + Instant::now(), + ), + artist_image_aspect_ratio: AnimationController::new( + 0.0, + 0.0, + 0.01, + 1.0, + 0.8, + 0.6, + Instant::now(), + ), + cover_left: 0.01, + cover_top: 0.21, + cover_bottom: 0.79, + artist_image_top: 0.5, + artist_image_to_cover_margin: 0.01, + } + } +} + +impl GuiElem for IdleDisplay { + fn children(&mut self) -> Box + '_> { + Box::new( + [ + self.c_top_label.elem_mut(), + self.c_side1_label.elem_mut(), + self.c_side2_label.elem_mut(), + ] + .into_iter(), + ) + } + fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { + // draw background + g.draw_rectangle( + info.pos.clone(), + Color::from_rgba(0.0, 0.0, 0.0, 0.5 + 0.5 * self.idle_mode), + ); + // update current_info + self.current_info.update(info, g); + if self.current_info.new_song { + self.current_info.new_song = false; + self.c_top_label.content = if let Some(song) = self.current_info.current_song { + info.gui_config + .idle_top_text + .gen(&info.database, info.database.get_song(&song)) + } else { + vec![] + }; + self.c_top_label.config_mut().redraw = true; + self.c_side1_label.content = if let Some(song) = self.current_info.current_song { + info.gui_config + .idle_side1_text + .gen(&info.database, info.database.get_song(&song)) + } else { + vec![] + }; + self.c_side1_label.config_mut().redraw = true; + self.c_side2_label.content = if let Some(song) = self.current_info.current_song { + info.gui_config + .idle_side2_text + .gen(&info.database, info.database.get_song(&song)) + } else { + vec![] + }; + self.c_side2_label.config_mut().redraw = true; + // check artist + if let Some(artist_id) = self + .current_info + .current_song + .as_ref() + .and_then(|id| info.database.songs().get(id)) + .map(|song| song.artist) + { + if self.current_artist_image.is_none() + || self + .current_artist_image + .as_ref() + .is_some_and(|(a, _)| *a != artist_id) + { + self.current_artist_image = Some((artist_id, None)); + self.artist_image_aspect_ratio.target = 0.0; + if let Some(artist) = info.database.artists().get(&artist_id) { + for tag in &artist.general.tags { + if tag.starts_with("ImageExt=") { + let filename = format!("{}.{}", artist.name, &tag[9..]); + self.current_artist_image = + Some((artist_id, Some((filename.clone(), None)))); + if !info.custom_images.contains_key(&filename) { + info.custom_images.insert( + filename.clone(), + GuiServerImage::new_custom_file( + filename, + Arc::clone(&info.get_con), + ), + ); + } + break; + } + } + } + } + } else { + if self.current_artist_image.is_some() { + self.current_artist_image = None; + self.artist_image_aspect_ratio.target = 0.0; + } + } + } + if self.current_info.new_cover { + self.current_info.new_cover = false; + match self.current_info.current_cover { + None | Some((_, Some(None))) => { + self.cover_aspect_ratio.target = 0.0; + } + Some((_, None)) | Some((_, Some(Some(_)))) => {} + } + } + if let Some((_, Some((img, h)))) = &mut self.current_artist_image { + if h.is_none() { + if let Some(img) = info.custom_images.get_mut(img) { + if let Some(img) = img.get_init(g) { + *h = Some(Some(img)); + } else if img.is_err() { + *h = Some(None); + } + } + } + } + // draw cover + if let Some(Some(cover)) = self + .current_info + .current_cover + .as_ref() + .map(|v| v.1.as_ref()) + { + image_display( + g, + cover.as_ref(), + info.pos.top_left().x + info.pos.height() * self.cover_left, + info.pos.top_left().y + info.pos.height() * self.cover_top, + info.pos.top_left().y + info.pos.height() * self.cover_bottom, + &mut self.cover_aspect_ratio, + ); + } + // draw artist image + if let Some((_, Some((_, Some(img))))) = &self.current_artist_image { + let top = info.pos.top_left().y + info.pos.height() * self.cover_top; + let bottom = info.pos.top_left().y + info.pos.height() * self.cover_bottom; + image_display( + g, + img.as_ref(), + get_right_x( + info.pos.top_left().x + info.pos.height() * self.cover_left, + top, + bottom, + self.cover_aspect_ratio.value, + ) + info.pos.height() * self.artist_image_to_cover_margin, + top + (bottom - top) * self.artist_image_top, + bottom, + &mut self.artist_image_aspect_ratio, + ); + } + // move children to make space for cover + let ar_updated = self + .cover_aspect_ratio + .update(info.time.clone(), info.no_animations) + | self + .artist_image_aspect_ratio + .update(info.time.clone(), info.no_animations); + if ar_updated || info.pos.size() != self.config.pixel_pos.size() { + if let Some(h) = &info.helper { + h.request_redraw(); + } + // make thing be relative to width instead of to height by multiplying with this + let top = self.cover_top; + let bottom = self.cover_bottom; + let left = (get_right_x(self.cover_left, top, bottom, self.cover_aspect_ratio.value) + + self.artist_image_to_cover_margin) + * info.pos.height() + / info.pos.width(); + let ai_top = top + (bottom - top) * self.artist_image_top; + let max_right = 1.0 - self.cover_left * info.pos.height() / info.pos.width(); + self.c_side1_label.config_mut().pos = + Rectangle::from_tuples((left, top), (max_right, ai_top)); + let left = get_right_x( + left, + ai_top * info.pos.height() / info.pos.width(), + bottom * info.pos.height() / info.pos.width(), + self.artist_image_aspect_ratio.value, + ); + self.c_side2_label.config_mut().pos = + Rectangle::from_tuples((left, ai_top), (max_right, bottom)); + } + } + fn config(&self) -> &GuiElemCfg { + &self.config + } + fn config_mut(&mut self) -> &mut GuiElemCfg { + &mut self.config + } + 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 updated_library(&mut self) { + self.current_info.update = true; + } + fn updated_queue(&mut self) { + self.current_info.update = true; + } +} diff --git a/musicdb-client/src/gui_library.rs b/musicdb-client/src/gui_library.rs index 4bd73e3..ae20bb4 100755 --- a/musicdb-client/src/gui_library.rs +++ b/musicdb-client/src/gui_library.rs @@ -6,6 +6,7 @@ use std::{ atomic::{AtomicBool, AtomicUsize}, mpsc, Mutex, }, + time::Instant, }; use musicdb_lib::data::{ @@ -25,10 +26,13 @@ use speedy2d::{ }; use crate::{ - gui::{Dragging, DrawInfo, GuiAction, GuiElemCfg, GuiElemTrait}, + gui::{ + Dragging, DrawInfo, GuiAction, GuiConfig, GuiElem, GuiElemCfg, GuiElemChildren, + GuiElemWrapper, + }, + gui_anim::AnimationController, gui_base::{Button, Panel, ScrollBox}, gui_text::{self, AdvancedLabel, Label, TextField}, - gui_wrappers::WithFocusHotkey, }; use self::selected::Selected; @@ -42,7 +46,13 @@ with Regex search and drag-n-drop. pub struct LibraryBrowser { config: GuiElemCfg, - pub children: Vec>, + c_search_artist: TextField, + c_search_album: TextField, + c_search_song: TextField, + c_scroll_box: ScrollBox>, + c_filter_button: Button<[Label; 1]>, + c_filter_panel: FilterPanel, + c_selected_counter_panel: Panel<[Label; 1]>, // - - - library_sorted: Vec<(ArtistId, Vec, Vec<(AlbumId, Vec)>)>, library_filtered: Vec<( @@ -60,7 +70,7 @@ pub struct LibraryBrowser { search_song: String, search_song_regex: Option, filter_target_state: Arc, - filter_state: f32, + filter_state: AnimationController, library_updated: bool, search_settings_changed: Arc, search_is_case_sensitive: Arc, @@ -73,11 +83,6 @@ pub struct LibraryBrowser { do_something_receiver: mpsc::Receiver>, selected_popup_state: (f32, usize, usize, usize), } -impl Clone for LibraryBrowser { - fn clone(&self) -> Self { - Self::new(self.config.clone()) - } -} fn search_regex_new(pat: &str, case_insensitive: bool) -> Result, regex::Error> { if pat.is_empty() { Ok(None) @@ -95,31 +100,29 @@ const LP_LIB2: f32 = 1.0; const LP_LIB1S: f32 = 0.4; impl LibraryBrowser { pub fn new(config: GuiElemCfg) -> Self { - let search_artist = TextField::new( + let c_search_artist = TextField::new( GuiElemCfg::at(Rectangle::from_tuples((0.01, 0.01), (0.45, 0.05))), "artist".to_string(), Color::GRAY, Color::WHITE, ); - let search_album = TextField::new( + let c_search_album = TextField::new( GuiElemCfg::at(Rectangle::from_tuples((0.55, 0.01), (0.99, 0.05))), "album".to_string(), Color::GRAY, Color::WHITE, ); - let search_song = WithFocusHotkey::new_ctrl( - VirtualKeyCode::F, - TextField::new( - GuiElemCfg::at(Rectangle::from_tuples((0.01, 0.06), (0.99, 0.1))), - "song".to_string(), - Color::GRAY, - Color::WHITE, - ), + let c_search_song = TextField::new( + GuiElemCfg::at(Rectangle::from_tuples((0.01, 0.06), (0.99, 0.1))), + "song".to_string(), + Color::GRAY, + Color::WHITE, ); let library_scroll_box = ScrollBox::new( GuiElemCfg::at(Rectangle::from_tuples((0.0, LP_LIB1), (1.0, LP_LIB2))), crate::gui_base::ScrollBoxSizeUnit::Pixels, vec![], + vec![], ); let (do_something_sender, do_something_receiver) = mpsc::channel(); let search_settings_changed = Arc::new(AtomicBool::new(false)); @@ -129,7 +132,7 @@ impl LibraryBrowser { let search_prefer_start_matches = Arc::new(AtomicBool::new(search_prefers_start_matches)); let filter_target_state = Arc::new(AtomicBool::new(false)); let fts = Arc::clone(&filter_target_state); - let filter_button = Button::new( + let c_filter_button = Button::new( GuiElemCfg::at(Rectangle::from_tuples((0.46, 0.01), (0.54, 0.05))), move |_| { fts.store( @@ -138,13 +141,13 @@ impl LibraryBrowser { ); vec![] }, - vec![Box::new(Label::new( + [Label::new( GuiElemCfg::default(), "more".to_owned(), Color::GRAY, None, Vec2::new(0.5, 0.5), - ))], + )], ); let filter_songs = Arc::new(Mutex::new(Filter { and: true, @@ -161,34 +164,32 @@ impl LibraryBrowser { let selected = Selected::new(Arc::clone(&search_settings_changed)); Self { config: config.w_keyboard_watch(), - children: vec![ - Box::new(search_artist), - Box::new(search_album), - Box::new(search_song), - Box::new(library_scroll_box), - Box::new(filter_button), - Box::new(FilterPanel::new( - Arc::clone(&search_settings_changed), - Arc::clone(&search_is_case_sensitive), - Arc::clone(&search_prefer_start_matches), - Arc::clone(&filter_songs), - Arc::clone(&filter_albums), - Arc::clone(&filter_artists), - selected.clone(), - do_something_sender.clone(), - )), - Box::new(Panel::with_background( - GuiElemCfg::default().disabled(), - vec![Box::new(Label::new( - GuiElemCfg::default(), - String::new(), - Color::LIGHT_GRAY, - None, - Vec2::new(0.5, 0.5), - ))], - Color::from_rgba(0.0, 0.0, 0.0, 0.8), - )), - ], + c_search_artist, + c_search_album, + c_search_song, + c_scroll_box: library_scroll_box, + c_filter_button, + c_filter_panel: FilterPanel::new( + Arc::clone(&search_settings_changed), + Arc::clone(&search_is_case_sensitive), + Arc::clone(&search_prefer_start_matches), + Arc::clone(&filter_songs), + Arc::clone(&filter_albums), + Arc::clone(&filter_artists), + selected.clone(), + do_something_sender.clone(), + ), + c_selected_counter_panel: Panel::with_background( + GuiElemCfg::default().disabled(), + [Label::new( + GuiElemCfg::default(), + String::new(), + Color::LIGHT_GRAY, + None, + Vec2::new(0.5, 0.5), + )], + Color::from_rgba(0.0, 0.0, 0.0, 0.8), + ), // - - - library_sorted: vec![], library_filtered: vec![], @@ -201,7 +202,7 @@ impl LibraryBrowser { search_song: String::new(), search_song_regex: None, filter_target_state, - filter_state: 0.0, + filter_state: AnimationController::new(0.0, 0.0, 0.25, 25.0, 0.1, 0.2, Instant::now()), library_updated: true, search_settings_changed, search_is_case_sensitive, @@ -258,15 +259,26 @@ impl LibraryBrowser { }) } } -impl GuiElemTrait for LibraryBrowser { +impl GuiElem for LibraryBrowser { 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().map(|v| v.as_mut())) + fn children(&mut self) -> Box + '_> { + Box::new( + [ + self.c_search_artist.elem_mut(), + self.c_search_album.elem_mut(), + self.c_search_song.elem_mut(), + self.c_scroll_box.elem_mut(), + self.c_filter_button.elem_mut(), + self.c_filter_panel.elem_mut(), + self.c_selected_counter_panel.elem_mut(), + ] + .into_iter(), + ) } fn any(&self) -> &dyn std::any::Any { self @@ -274,10 +286,10 @@ impl GuiElemTrait for LibraryBrowser { fn any_mut(&mut self) -> &mut dyn std::any::Any { self } - fn elem(&self) -> &dyn GuiElemTrait { + fn elem(&self) -> &dyn GuiElem { self } - fn elem_mut(&mut self) -> &mut dyn GuiElemTrait { + fn elem_mut(&mut self) -> &mut dyn GuiElem { self } fn draw_rev(&self) -> bool { @@ -316,15 +328,7 @@ impl GuiElemTrait for LibraryBrowser { } } { - let v = &mut self.children[0] - .any_mut() - .downcast_mut::() - .unwrap() - .children[0] - .any_mut() - .downcast_mut::