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); } }