use std::time::Instant; use musicdb_lib::{ data::{song::Song, ArtistId}, server::Command, }; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle}; use crate::{ color_scale, gui::{GuiAction, GuiElem, GuiElemCfg, GuiElemChildren}, gui_anim::AnimationController, gui_base::{Button, Panel, ScrollBox}, gui_text::{Label, TextField}, }; // TODO: Fix bug where after selecting an artist you can't mouse-click the buttons anymore (to change it) const ELEM_HEIGHT: f32 = 32.0; pub struct EditorForSongs { config: GuiElemCfg, songs: Vec, c_title: Label, c_scrollbox: ScrollBox, c_buttons: Panel<[Button<[Label; 1]>; 2]>, c_background: Panel<()>, created: Option, event_sender: std::sync::mpsc::Sender, event_recv: std::sync::mpsc::Receiver, } pub enum Event { Close, Apply, SetArtist(String, Option), } pub struct EditorForSongElems { c_title: TextField, c_artist: EditorForSongArtistChooser, c_album: Label, } impl GuiElemChildren for EditorForSongElems { fn iter(&mut self) -> Box + '_> { Box::new( [ self.c_title.elem_mut(), self.c_artist.elem_mut(), self.c_album.elem_mut(), ] .into_iter(), ) } fn len(&self) -> usize { 3 } } impl EditorForSongs { pub fn new(songs: Vec) -> Self { let (sender, recv) = std::sync::mpsc::channel(); Self { config: GuiElemCfg::at(Rectangle::from_tuples((0.0, 1.0), (1.0, 2.0))), c_title: Label::new( GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.05))), format!("Editing {} songs", songs.len()), Color::LIGHT_GRAY, None, Vec2::new(0.5, 0.5), ), c_scrollbox: ScrollBox::new( GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.05), (1.0, 0.95))), crate::gui_base::ScrollBoxSizeUnit::Pixels, EditorForSongElems { c_title: TextField::new( GuiElemCfg::default(), format!( "Title ({})", songs .iter() .enumerate() .map(|(i, s)| format!( "{}{}", if i == 0 { "" } else { ", " }, s.title )) .collect::() ), color_scale(Color::MAGENTA, 0.6, 0.6, 0.6, Some(0.75)), Color::MAGENTA, ), c_artist: EditorForSongArtistChooser::new(sender.clone()), c_album: Label::new( GuiElemCfg::default(), format!("(todo...)"), Color::GRAY, None, Vec2::new(0.0, 0.5), ), }, vec![], ELEM_HEIGHT, ), c_buttons: Panel::new( GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.95), (1.0, 1.0))), [ { let sender = sender.clone(); Button::new( GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (0.5, 1.0))), move |_| { sender.send(Event::Close).unwrap(); vec![] }, [Label::new( GuiElemCfg::default(), "Close".to_owned(), Color::WHITE, None, Vec2::new(0.5, 0.5), )], ) }, { let sender = sender.clone(); Button::new( GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (1.0, 1.0))), move |_| { sender.send(Event::Apply).unwrap(); vec![] }, [Label::new( GuiElemCfg::default(), "Apply".to_owned(), Color::WHITE, None, Vec2::new(0.5, 0.5), )], ) }, ], ), c_background: Panel::with_background(GuiElemCfg::default(), (), Color::BLACK), created: Some(Instant::now()), songs, event_sender: sender, event_recv: recv, } } } impl GuiElem for EditorForSongs { fn children(&mut self) -> Box + '_> { Box::new( [ self.c_title.elem_mut(), self.c_scrollbox.elem_mut(), self.c_buttons.elem_mut(), self.c_background.elem_mut(), ] .into_iter(), ) } fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) { loop { match self.event_recv.try_recv() { Ok(e) => match e { Event::Close => info.actions.push(GuiAction::Do(Box::new(|gui| { gui.gui.c_editing_songs = None; gui.gui.set_normal_ui_enabled(true); }))), Event::Apply => { for song in &self.songs { let mut song = song.clone(); let new_title = self .c_scrollbox .children .c_title .c_input .content .get_text() .trim(); if !new_title.is_empty() { song.title = new_title.to_owned(); } if let Some(artist_id) = self.c_scrollbox.children.c_artist.chosen_id { song.artist = artist_id; song.album = None; } info.actions .push(GuiAction::SendToServer(Command::ModifySong(song))); } } Event::SetArtist(name, id) => { self.c_scrollbox.children.c_artist.chosen_id = id; self.c_scrollbox.children.c_artist.last_search = name.to_lowercase(); self.c_scrollbox.children.c_artist.open_prog.target = 1.0; *self .c_scrollbox .children .c_artist .c_name .c_input .content .text() = name; self.c_scrollbox.children.c_artist.config_mut().redraw = true; } }, Err(_) => break, } } // animation if let Some(created) = &self.created { if let Some(h) = &info.helper { h.request_redraw(); } let open_prog = created.elapsed().as_secs_f32() / 0.5; if open_prog >= 1.0 { self.created = None; self.config.pos = Rectangle::from_tuples((0.0, 0.0), (1.0, 1.0)); info.actions.push(GuiAction::Do(Box::new(|gui| { gui.gui.set_normal_ui_enabled(false); }))); } else { let offset = 1.0 - open_prog; let offset = offset * offset; self.config.pos = Rectangle::from_tuples((0.0, offset), (1.0, 1.0 + offset)); } } // artist sel if self .c_scrollbox .children .c_artist .open_prog .update(Instant::now(), false) { if let Some(v) = self.c_scrollbox.children_heights.get_mut(1) { *v = ELEM_HEIGHT * self.c_scrollbox.children.c_artist.open_prog.value; self.c_scrollbox.config_mut().redraw = true; } if let Some(h) = &info.helper { h.request_redraw(); } } } 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 } } pub struct EditorForSongArtistChooser { config: GuiElemCfg, event_sender: std::sync::mpsc::Sender, /// `1.0` = collapsed, `self.expand_to` = expanded (shows `c_picker` of height 7-1=6) open_prog: AnimationController, expand_to: f32, chosen_id: Option, c_name: TextField, c_picker: ScrollBox>>, last_search: String, } impl EditorForSongArtistChooser { pub fn new(event_sender: std::sync::mpsc::Sender) -> Self { let expand_to = 7.0; Self { config: GuiElemCfg::default(), event_sender, open_prog: AnimationController::new(1.0, 1.0, 0.3, 8.0, 0.5, 0.6, Instant::now()), expand_to, chosen_id: None, c_name: TextField::new( GuiElemCfg::default(), "artist".to_owned(), Color::DARK_GRAY, Color::WHITE, ), c_picker: ScrollBox::new( GuiElemCfg::default().disabled(), crate::gui_base::ScrollBoxSizeUnit::Pixels, vec![], vec![], ELEM_HEIGHT, ), last_search: String::from("\n"), } } } impl GuiElem for EditorForSongArtistChooser { fn draw(&mut self, info: &mut crate::gui::DrawInfo, _g: &mut speedy2d::Graphics2D) { let picker_enabled = self.open_prog.value > 1.0; self.c_picker.config_mut().enabled = picker_enabled; if picker_enabled { let split = 1.0 / self.open_prog.value; self.c_name.config_mut().pos = Rectangle::from_tuples((0.0, 0.0), (1.0, split)); self.c_picker.config_mut().pos = Rectangle::from_tuples((0.0, split), (1.0, 1.0)); } else { self.c_name.config_mut().pos = Rectangle::from_tuples((0.0, 0.0), (1.0, 1.0)); } let search = self.c_name.c_input.content.get_text().to_lowercase(); let search_changed = &self.last_search != &search; if self.config.redraw || search_changed { *self.c_name.c_input.content.color() = if self.chosen_id.is_some() { Color::GREEN } else { Color::WHITE }; if search_changed { self.chosen_id = None; self.open_prog.target = self.expand_to; if search.is_empty() { self.open_prog.target = 1.0; } } let artists = info .database .artists() .values() .filter(|artist| artist.name.to_lowercase().contains(&search)) // .take(self.open_prog.value as _) .map(|artist| (artist.name.clone(), artist.id)) .collect::>(); let chosen_id = self.chosen_id; self.c_picker.children = artists .iter() .map(|a| { let sender = self.event_sender.clone(); let name = a.0.clone(); let id = a.1; Button::new( GuiElemCfg::default(), move |_| { sender .send(Event::SetArtist(name.clone(), Some(id))) .unwrap(); vec![] }, [Label::new( GuiElemCfg::default(), a.0.clone(), if chosen_id.is_some_and(|c| c == a.1) { Color::WHITE } else { Color::LIGHT_GRAY }, None, Vec2::new(0.0, 0.5), )], ) }) .collect(); self.c_picker.config_mut().redraw = true; self.last_search = search; } } fn config(&self) -> &GuiElemCfg { &self.config } fn config_mut(&mut self) -> &mut GuiElemCfg { &mut self.config } fn children(&mut self) -> Box + '_> { Box::new([self.c_name.elem_mut(), self.c_picker.elem_mut()].into_iter()) } 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 } }