use std::collections::VecDeque; use musicdb_lib::{ data::{ database::Database, queue::{Queue, QueueContent, QueueDuration, ShuffleState}, song::Song, AlbumId, ArtistId, }, server::Command, }; use speedy2d::{ color::Color, dimen::Vec2, shape::Rectangle, window::{ModifiersState, MouseButton, VirtualKeyCode}, }; use crate::{ gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, gui_base::{Panel, ScrollBox}, gui_text::{self, AdvancedLabel, Label}, }; /* This is responsible for showing the current queue, with drag-n-drop only if the mouse leaves the element before it is released, because simple clicks have to be GoTo events. */ #[derive(Clone)] pub struct QueueViewer { config: GuiElemCfg, children: Vec, queue_updated: bool, } const QP_QUEUE1: f32 = 0.0; const QP_QUEUE2: f32 = 0.95; const QP_INV1: f32 = QP_QUEUE2; const QP_INV2: f32 = 1.0; impl QueueViewer { pub fn new(config: GuiElemCfg) -> Self { Self { config, children: vec![ GuiElem::new(ScrollBox::new( GuiElemCfg::at(Rectangle::from_tuples((0.0, QP_QUEUE1), (1.0, QP_QUEUE2))), crate::gui_base::ScrollBoxSizeUnit::Pixels, vec![( GuiElem::new(Label::new( GuiElemCfg::default(), "loading...".to_string(), Color::DARK_GRAY, None, Vec2::new(0.5, 0.5), )), 1.0, )], )), GuiElem::new(QueueEmptySpaceDragHandler::new(GuiElemCfg::at( Rectangle::from_tuples((0.0, QP_QUEUE1), (1.0, QP_QUEUE2)), ))), GuiElem::new(Panel::new( GuiElemCfg::at(Rectangle::from_tuples((0.0, QP_INV1), (0.5, QP_INV2))), vec![ GuiElem::new( QueueLoop::new( GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (0.5, 0.5))) .w_mouse(), vec![], QueueContent::Loop( 0, 0, Box::new( QueueContent::Folder(0, vec![], "in loop".to_string()) .into(), ), ) .into(), false, ) .alwayscopy(), ), GuiElem::new( QueueLoop::new( GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.5), (0.5, 1.0))) .w_mouse(), vec![], QueueContent::Loop( 2, 0, Box::new( QueueContent::Folder(0, vec![], "in loop".to_string()) .into(), ), ) .into(), false, ) .alwayscopy(), ), GuiElem::new( QueueRandom::new( GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (1.0, 0.5))) .w_mouse(), vec![], QueueContent::Random(VecDeque::new()).into(), false, ) .alwayscopy(), ), GuiElem::new( QueueShuffle::new( GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.5), (1.0, 1.0))) .w_mouse(), vec![], QueueContent::Shuffle { inner: Box::new( QueueContent::Folder(0, vec![], String::new()).into(), ), state: ShuffleState::NotShuffled, } .into(), false, ) .alwayscopy(), ), ], )), GuiElem::new(AdvancedLabel::new( GuiElemCfg::at(Rectangle::from_tuples((0.5, QP_INV1), (1.0, QP_INV2))), Vec2::new(0.0, 0.5), vec![], )), ], queue_updated: false, } } } impl GuiElemTrait for QueueViewer { 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 self.queue_updated { self.queue_updated = false; let label = self.children[3] .inner .any_mut() .downcast_mut::() .unwrap(); fn fmt_dur(dur: QueueDuration) -> String { if dur.infinite { "∞".to_owned() } else { let seconds = dur.millis / 1000; let minutes = seconds / 60; let h = minutes / 60; let m = minutes % 60; let s = seconds % 60; if dur.random_counter == 0 { if h > 0 { format!("{h}:{m:0>2}:{s:0>2}") } else { format!("{m:0>2}:{s:0>2}") } } else { let r = dur.random_counter; if dur.millis > 0 { if h > 0 { format!("{h}:{m:0>2}:{s:0>2} + {r} random songs") } else { format!("{m:0>2}:{s:0>2} + {r} random songs") } } else { format!("{r} random songs") } } } } let dt = fmt_dur(info.database.queue.duration_total(&info.database)); let dr = fmt_dur(info.database.queue.duration_remaining(&info.database)); label.content = vec![ vec![( gui_text::Content::new(format!("Total: {dt}"), Color::GRAY), 1.0, 1.0, )], vec![( gui_text::Content::new(format!("Remaining: {dr}"), Color::GRAY), 1.0, 1.0, )], ]; label.config_mut().redraw = true; } if self.config.redraw || info.pos.size() != self.config.pixel_pos.size() { self.config.redraw = false; let mut c = vec![]; queue_gui( &info.database.queue, &info.database, 0.0, 0.02, info.line_height, &mut c, vec![], true, true, ); let mut scroll_box = self.children[0].try_as_mut::().unwrap(); scroll_box.children = c; scroll_box.config_mut().redraw = true; } } fn updated_queue(&mut self) { self.queue_updated = true; self.config.redraw = true; } } fn queue_gui( queue: &Queue, db: &Database, depth: f32, depth_inc_by: f32, line_height: f32, target: &mut Vec<(GuiElem, f32)>, path: Vec, current: bool, skip_folder: bool, ) { let cfg = GuiElemCfg::at(Rectangle::from_tuples((depth, 0.0), (1.0, 1.0))); match queue.content() { QueueContent::Song(id) => { if let Some(s) = db.songs().get(id) { target.push(( GuiElem::new(QueueSong::new( cfg, path, s.clone(), current, db, depth_inc_by * 0.33, )), line_height * 1.75, )); } } QueueContent::Folder(ia, q, _) => { if !skip_folder { target.push(( GuiElem::new(QueueFolder::new( cfg.clone(), path.clone(), queue.clone(), current, )), line_height * 0.8, )); } for (i, q) in q.iter().enumerate() { let mut p = path.clone(); p.push(i); queue_gui( q, db, depth + depth_inc_by, depth_inc_by, line_height, target, p, current && *ia == i, false, ); } if !skip_folder { let mut p1 = path; let p2 = p1.pop().unwrap_or(0) + 1; target.push(( GuiElem::new(QueueIndentEnd::new(cfg, (p1, p2))), line_height * 0.4, )); } } QueueContent::Loop(_, _, inner) => { let mut p = path.clone(); p.push(0); let mut p1 = path.clone(); let p2 = p1.pop().unwrap_or(0) + 1; target.push(( GuiElem::new(QueueLoop::new(cfg.clone(), path, queue.clone(), current)), line_height * 0.8, )); queue_gui( &inner, db, depth, depth_inc_by, line_height, target, p, current, true, ); target.push(( GuiElem::new(QueueIndentEnd::new(cfg, (p1, p2))), line_height * 0.4, )); } QueueContent::Random(q) => { target.push(( GuiElem::new(QueueRandom::new( cfg.clone(), path.clone(), queue.clone(), current, )), line_height, )); for (i, inner) in q.iter().enumerate() { let mut p = path.clone(); p.push(i); queue_gui( inner, db, depth + depth_inc_by, depth_inc_by, line_height, target, p, current && i == q.len().saturating_sub(2), false, ); } let mut p1 = path.clone(); let p2 = p1.pop().unwrap_or(0) + 1; target.push(( GuiElem::new(QueueIndentEnd::new(cfg, (p1, p2))), line_height * 0.4, )); } QueueContent::Shuffle { inner, state: _ } => { target.push(( GuiElem::new(QueueShuffle::new( cfg.clone(), path.clone(), queue.clone(), current, )), line_height * 0.8, )); let mut p = path.clone(); p.push(0); queue_gui( inner, db, depth + depth_inc_by, depth_inc_by, line_height, target, p, current, true, ); let mut p1 = path.clone(); let p2 = p1.pop().unwrap_or(0) + 1; target.push(( GuiElem::new(QueueIndentEnd::new(cfg, (p1, p2))), line_height * 0.4, )); } } } #[derive(Clone)] struct QueueEmptySpaceDragHandler { config: GuiElemCfg, children: Vec, } impl QueueEmptySpaceDragHandler { pub fn new(config: GuiElemCfg) -> Self { Self { config: config.w_drag_target(), children: vec![], } } } impl GuiElemTrait for QueueEmptySpaceDragHandler { 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 dragged(&mut self, dragged: Dragging) -> Vec { dragged_add_to_queue(dragged, |q, _| Command::QueueAdd(vec![], q)) } } fn generic_queue_draw( info: &mut DrawInfo, path: &Vec, mouse: &mut bool, copy_on_mouse_down: bool, ) -> bool { if *mouse && !info.pos.contains(info.mouse_pos) { *mouse = false; if !copy_on_mouse_down { info.actions .push(GuiAction::SendToServer(Command::QueueRemove(path.clone()))); } true } else { false } } #[derive(Clone)] struct QueueSong { config: GuiElemCfg, children: Vec, path: Vec, song: Song, current: bool, insert_below: bool, mouse: bool, mouse_pos: Vec2, copy: bool, always_copy: bool, copy_on_mouse_down: bool, } impl QueueSong { pub fn new( config: GuiElemCfg, path: Vec, song: Song, current: bool, db: &Database, sub_offset: f32, ) -> Self { Self { config: config.w_mouse().w_keyboard_watch().w_drag_target(), children: vec![ GuiElem::new(AdvancedLabel::new( GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.57))), Vec2::new(0.0, 0.5), vec![vec![ ( gui_text::Content::new( song.title.clone(), if current { Color::from_int_rgb(194, 76, 178) } else { Color::from_int_rgb(120, 76, 194) }, ), 1.0, 1.0, ), ( gui_text::Content::new( { let duration = song.duration_millis / 1000; format!(" {}:{:0>2}", duration / 60, duration % 60) }, if current { Color::GRAY } else { Color::DARK_GRAY }, ), 0.6, 1.0, ), ]], )), GuiElem::new(Label::new( GuiElemCfg::at(Rectangle::from_tuples((sub_offset, 0.57), (1.0, 1.0))), match ( db.artists().get(&song.artist), song.album.as_ref().and_then(|id| db.albums().get(id)), ) { (None, None) => String::new(), (Some(artist), None) => format!("by {}", artist.name), (None, Some(album)) => { if let Some(artist) = db.artists().get(&album.artist) { format!("on {} by {}", album.name, artist.name) } else { format!("on {}", album.name) } } (Some(artist), Some(album)) => { format!("by {} on {}", artist.name, album.name) } }, if current { Color::from_int_rgb(97, 38, 89) } else { Color::from_int_rgb(60, 38, 97) }, None, Vec2::new(0.0, 0.5), )), ], path, song, current, insert_below: false, mouse: false, mouse_pos: Vec2::ZERO, copy: false, always_copy: false, copy_on_mouse_down: false, } } fn alwayscopy(mut self) -> Self { self.always_copy = true; self.copy = true; self } } impl GuiElemTrait for QueueSong { 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_down(&mut self, button: MouseButton) -> Vec { if button == MouseButton::Left { self.mouse = true; self.copy_on_mouse_down = self.copy; } vec![] } fn mouse_up(&mut self, button: MouseButton) -> Vec { if self.mouse && button == MouseButton::Left { self.mouse = false; if !self.always_copy { vec![GuiAction::SendToServer(Command::QueueGoto( self.path.clone(), ))] } else { vec![] } } else { vec![] } } fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { self.insert_below = info.mouse_pos.y > info.pos.top_left().y + info.pos.height() * 0.5; if !self.always_copy && info.dragging.is_some() && info.pos.contains(info.mouse_pos) { g.draw_rectangle( if self.insert_below { Rectangle::new( Vec2::new( info.pos.top_left().x, info.pos.top_left().y + info.pos.height() * 0.75, ), *info.pos.bottom_right(), ) } else { Rectangle::new( *info.pos.top_left(), Vec2::new( info.pos.bottom_right().x, info.pos.top_left().y + info.pos.height() * 0.25, ), ) }, Color::from_rgba(1.0, 1.0, 1.0, 0.25), ); } if !self.mouse { self.mouse_pos = Vec2::new( info.mouse_pos.x - self.config.pixel_pos.top_left().x, info.mouse_pos.y - self.config.pixel_pos.top_left().y, ); } if generic_queue_draw(info, &self.path, &mut self.mouse, self.copy_on_mouse_down) { let mouse_pos = self.mouse_pos; let w = self.config.pixel_pos.width(); let h = self.config.pixel_pos.height(); let mut el = GuiElem::new(self.clone()); info.actions.push(GuiAction::SetDragging(Some(( Dragging::Queue(QueueContent::Song(self.song.id).into()), Some(Box::new(move |i, g| { let sw = i.pos.width(); let sh = i.pos.height(); let x = (i.mouse_pos.x - mouse_pos.x) / sw; let y = (i.mouse_pos.y - mouse_pos.y) / sh; el.inner.config_mut().pos = Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh)); el.draw(i, g) })), )))); } } fn key_watch( &mut self, modifiers: ModifiersState, _down: bool, _key: Option, _scan: speedy2d::window::KeyScancode, ) -> Vec { self.copy = self.always_copy || modifiers.ctrl(); vec![] } fn dragged(&mut self, dragged: Dragging) -> Vec { if !self.always_copy { let mut p = self.path.clone(); let insert_below = self.insert_below; dragged_add_to_queue(dragged, move |q, i| { if let Some(j) = p.pop() { Command::QueueInsert(p.clone(), if insert_below { j + 1 } else { j } + i, q) } else { Command::QueueAdd(p.clone(), q) } }) } else { vec![] } } } #[derive(Clone)] struct QueueFolder { config: GuiElemCfg, children: Vec, path: Vec, queue: Queue, current: bool, insert_into: bool, mouse: bool, mouse_pos: Vec2, copy: bool, always_copy: bool, copy_on_mouse_down: bool, } impl QueueFolder { pub fn new(config: GuiElemCfg, path: Vec, queue: Queue, current: bool) -> Self { Self { config: if path.is_empty() { config } else { config.w_mouse().w_keyboard_watch() } .w_drag_target(), children: vec![GuiElem::new(Label::new( GuiElemCfg::default(), match queue.content() { QueueContent::Folder(_, q, n) => format!( "{} ({})", if path.is_empty() && n.is_empty() { "Queue" } else { n }, q.len() ), _ => "[???]".to_string(), }, Color::from_int_rgb(52, 132, 50), None, Vec2::new(0.0, 0.5), ))], path, queue, current, insert_into: false, mouse: false, mouse_pos: Vec2::ZERO, copy: false, always_copy: false, copy_on_mouse_down: false, } } fn alwayscopy(mut self) -> Self { self.always_copy = true; self.copy = true; self } } impl GuiElemTrait for QueueFolder { 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) { self.insert_into = info.mouse_pos.y > info.pos.top_left().y + info.pos.height() * 0.5; if !self.always_copy && info.dragging.is_some() && info.pos.contains(info.mouse_pos) { g.draw_rectangle( if self.insert_into { Rectangle::new( Vec2::new( info.pos.top_left().x, info.pos.top_left().y + info.pos.height() * 0.5, ), *info.pos.bottom_right(), ) } else { Rectangle::new( *info.pos.top_left(), Vec2::new( info.pos.bottom_right().x, info.pos.top_left().y + info.pos.height() * 0.25, ), ) }, Color::from_rgba(1.0, 1.0, 1.0, 0.25), ); } if !self.mouse { self.mouse_pos = Vec2::new( info.mouse_pos.x - self.config.pixel_pos.top_left().x, info.mouse_pos.y - self.config.pixel_pos.top_left().y, ); } if generic_queue_draw(info, &self.path, &mut self.mouse, self.copy_on_mouse_down) { let mouse_pos = self.mouse_pos; let w = self.config.pixel_pos.width(); let h = self.config.pixel_pos.height(); let mut el = GuiElem::new(self.clone()); info.actions.push(GuiAction::SetDragging(Some(( Dragging::Queue(self.queue.clone()), Some(Box::new(move |i, g| { let sw = i.pos.width(); let sh = i.pos.height(); let x = (i.mouse_pos.x - mouse_pos.x) / sw; let y = (i.mouse_pos.y - mouse_pos.y) / sh; el.inner.config_mut().pos = Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh)); el.draw(i, g) })), )))); } } fn mouse_down(&mut self, button: MouseButton) -> Vec { if button == MouseButton::Left { self.mouse = true; self.copy_on_mouse_down = self.copy; } vec![] } fn mouse_up(&mut self, button: MouseButton) -> Vec { if self.mouse && button == MouseButton::Left { self.mouse = false; if !self.always_copy { vec![GuiAction::SendToServer(Command::QueueGoto( self.path.clone(), ))] } else { vec![] } } else { vec![] } } fn key_watch( &mut self, modifiers: ModifiersState, _down: bool, _key: Option, _scan: speedy2d::window::KeyScancode, ) -> Vec { self.copy = modifiers.ctrl(); vec![] } fn dragged(&mut self, dragged: Dragging) -> Vec { if !self.always_copy { if self.insert_into { let p = self.path.clone(); dragged_add_to_queue(dragged, move |q, _| Command::QueueAdd(p.clone(), q)) } else { let mut p = self.path.clone(); let j = p.pop().unwrap_or(0); dragged_add_to_queue(dragged, move |q, i| { Command::QueueInsert(p.clone(), j + i, q) }) } } else { vec![] } } } #[derive(Clone)] pub struct QueueIndentEnd { config: GuiElemCfg, children: Vec, path_insert: (Vec, usize), } impl QueueIndentEnd { pub fn new(config: GuiElemCfg, path_insert: (Vec, usize)) -> Self { Self { config: config.w_drag_target(), children: vec![], path_insert, } } } impl GuiElemTrait for QueueIndentEnd { 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 info.dragging.is_some() { g.draw_rectangle( info.pos.clone(), Color::from_rgba( 1.0, 1.0, 1.0, if info.pos.contains(info.mouse_pos) { 0.3 } else { 0.2 }, ), ); } } fn dragged(&mut self, dragged: Dragging) -> Vec { let (p, j) = self.path_insert.clone(); dragged_add_to_queue(dragged, move |q, i| { Command::QueueInsert(p.clone(), j + i, q) }) } } #[derive(Clone)] struct QueueLoop { config: GuiElemCfg, children: Vec, path: Vec, queue: Queue, current: bool, mouse: bool, mouse_pos: Vec2, copy: bool, always_copy: bool, copy_on_mouse_down: bool, } impl QueueLoop { pub fn new(config: GuiElemCfg, path: Vec, queue: Queue, current: bool) -> Self { Self { config: if path.is_empty() { config } else { config.w_mouse().w_keyboard_watch() } .w_drag_target(), children: vec![GuiElem::new(Label::new( GuiElemCfg::default(), Self::get_label_text(&queue), Color::from_int_rgb(217, 197, 65), None, Vec2::new(0.0, 0.5), ))], path, queue, current, mouse: false, mouse_pos: Vec2::ZERO, copy: false, always_copy: false, copy_on_mouse_down: false, } } fn alwayscopy(mut self) -> Self { self.always_copy = true; self.copy = true; self.config.scroll_events = true; self } fn get_label_text(queue: &Queue) -> String { match queue.content() { QueueContent::Loop(total, _current, _) => { if *total == 0 { format!("repeat forever") } else if *total == 1 { format!("repeat 1 time") } else { format!("repeat {total} times") } } _ => "[???]".to_string(), } } } impl GuiElemTrait for QueueLoop { 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_wheel(&mut self, diff: f32) -> Vec { if self.always_copy { if let QueueContent::Loop(total, _, _) = self.queue.content_mut() { if diff > 0.0 { *total += 1; } else if diff < 0.0 && *total > 0 { *total -= 1; } } *self.children[0] .inner .any_mut() .downcast_mut::