diff --git a/musicdb-client/Cargo.toml b/musicdb-client/Cargo.toml index ff03499..d18b41c 100755 --- a/musicdb-client/Cargo.toml +++ b/musicdb-client/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +directories = "5.0.1" musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" } regex = "1.9.3" speedy2d = { version = "1.12.0", optional = true } diff --git a/musicdb-client/src/gui.rs b/musicdb-client/src/gui.rs index f7ad338..8196322 100755 --- a/musicdb-client/src/gui.rs +++ b/musicdb-client/src/gui.rs @@ -1,15 +1,17 @@ use std::{ any::Any, + collections::HashMap, eprintln, - io::{Read, Write}, + io::Cursor, net::TcpStream, sync::{Arc, Mutex}, + thread::JoinHandle, time::Instant, usize, }; use musicdb_lib::{ - data::{database::Database, queue::Queue, AlbumId, ArtistId, SongId}, + data::{database::Database, queue::Queue, AlbumId, ArtistId, CoverId, SongId}, load::ToFromBytes, server::{get, Command}, }; @@ -17,6 +19,7 @@ use speedy2d::{ color::Color, dimen::{UVec2, Vec2}, font::Font, + image::ImageHandle, shape::Rectangle, window::{ KeyScancode, ModifiersState, MouseButton, MouseScrollDistance, UserEventSender, @@ -34,10 +37,10 @@ pub enum GuiEvent { Exit, } -pub fn main( +pub fn main( database: Arc>, connection: TcpStream, - get_con: get::Client, + get_con: get::Client, event_sender_arc: Arc>>>, ) { let mut config_file = super::get_config_file_path(); @@ -104,7 +107,10 @@ pub fn main( let window = speedy2d::Window::::new_with_user_events( "MusicDB Client", - WindowCreationOptions::new_fullscreen_borderless(), + WindowCreationOptions::new_windowed( + speedy2d::window::WindowSize::MarginPhysicalPixels(0), + None, + ), ) .expect("couldn't open window"); *event_sender_arc.lock().unwrap() = Some(window.create_user_event_sender()); @@ -113,7 +119,7 @@ pub fn main( font, database, connection, - get_con, + Arc::new(Mutex::new(get_con)), event_sender_arc, sender, line_height, @@ -127,10 +133,12 @@ pub struct Gui { pub event_sender: UserEventSender, pub database: Arc>, pub connection: TcpStream, + pub get_con: Arc>>, pub gui: GuiElem, pub size: UVec2, pub mouse_pos: Vec2, pub font: Font, + pub covers: Option>, pub last_draw: Instant, pub modifiers: ModifiersState, pub dragging: Option<( @@ -144,11 +152,11 @@ pub struct Gui { pub scroll_pages_multiplier: f64, } impl Gui { - fn new( + fn new( font: Font, database: Arc>, connection: TcpStream, - get_con: get::Client, + get_con: Arc>>, event_sender_arc: Arc>>>, event_sender: UserEventSender, line_height: f32, @@ -192,11 +200,11 @@ impl Gui { event_sender, database, connection, + get_con, gui: GuiElem::new(WithFocusHotkey::new_noshift( VirtualKeyCode::Escape, GuiScreen::new( GuiElemCfg::default(), - get_con, line_height, scroll_pixels_multiplier, scroll_lines_multiplier, @@ -206,6 +214,7 @@ impl Gui { size: UVec2::ZERO, mouse_pos: Vec2::ZERO, font, + covers: Some(HashMap::new()), // font: Font::new(include_bytes!("/usr/share/fonts/TTF/FiraSans-Regular.ttf")).unwrap(), last_draw: Instant::now(), modifiers: ModifiersState::default(), @@ -226,8 +235,12 @@ 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. + /// 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 + '_>; + /// defaults to true. + fn draw_rev(&self) -> bool { + true + } fn any(&self) -> &dyn Any; fn any_mut(&mut self) -> &mut dyn Any; fn clone_gui(&self) -> Box; @@ -381,6 +394,7 @@ pub enum GuiAction { )>, ), SetLineHeight(f32), + LoadCover(CoverId), /// Run a custom closure with mutable access to the Gui struct Do(Box), Exit, @@ -403,6 +417,8 @@ pub struct DrawInfo<'a> { /// compare this to `pos` to find the mouse's relative position. pub mouse_pos: Vec2, pub helper: Option<&'a mut WindowHelper>, + pub get_con: Arc>>, + pub covers: &'a mut HashMap, pub has_keyboard_focus: bool, pub child_has_keyboard_focus: bool, /// the height of one line of text (in pixels) @@ -456,16 +472,23 @@ impl GuiElem { 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); + if self.inner.draw_rev() { + 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); + } + } else { + for (i, c) in self.inner.children().enumerate() { + 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; @@ -697,6 +720,12 @@ impl Gui { self.gui .recursive_all(&mut |e| e.inner.config_mut().redraw = true); } + GuiAction::LoadCover(id) => { + self.covers + .as_mut() + .unwrap() + .insert(id, GuiCover::new(id, Arc::clone(&self.get_con))); + } GuiAction::Do(mut f) => f(self), GuiAction::Exit => _ = self.event_sender.send_event(GuiEvent::Exit), GuiAction::SetIdle(v) => { @@ -784,12 +813,15 @@ impl WindowHandler for Gui { Color::BLACK, ); let mut dblock = self.database.lock().unwrap(); + let mut covers = self.covers.take().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, + get_con: Arc::clone(&self.get_con), + covers: &mut covers, helper: Some(helper), has_keyboard_focus: false, child_has_keyboard_focus: true, @@ -827,7 +859,9 @@ impl WindowHandler for Gui { } } } + // cleanup drop(info); + self.covers = Some(covers); drop(dblock); for a in actions { self.exec_gui_action(a); @@ -1013,3 +1047,62 @@ impl WindowHandler for Gui { .recursive_all(&mut |e| e.inner.config_mut().redraw = true); } } + +pub enum GuiCover { + Loading(JoinHandle>>), + Loaded(ImageHandle), + Error, +} +impl GuiCover { + pub fn new(id: CoverId, get_con: Arc>>) -> Self { + Self::Loading(std::thread::spawn(move || { + get_con + .lock() + .unwrap() + .cover_bytes(id) + .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 get_init(&mut self, g: &mut Graphics2D) -> Option { + match self { + Self::Loaded(handle) => Some(handle.clone()), + Self::Error => None, + Self::Loading(t) => { + if t.is_finished() { + let s = std::mem::replace(self, Self::Error); + if let Self::Loading(t) = s { + match t.join().unwrap() { + Some(bytes) => match g.create_image_from_file_bytes( + None, + speedy2d::image::ImageSmoothingMode::Linear, + Cursor::new(bytes), + ) { + Ok(handle) => { + *self = Self::Loaded(handle.clone()); + Some(handle) + } + Err(e) => { + eprintln!("[info] couldn't load cover from bytes: {e}"); + None + } + }, + None => None, + } + } else { + *self = s; + None + } + } else { + None + } + } + } + } +} diff --git a/musicdb-client/src/gui_base.rs b/musicdb-client/src/gui_base.rs index dcc18e6..75db9a6 100755 --- a/musicdb-client/src/gui_base.rs +++ b/musicdb-client/src/gui_base.rs @@ -123,6 +123,9 @@ pub struct ScrollBox { /// 0.max(height_bottom - 1) max_scroll: f32, last_height_px: f32, + mouse_in_scrollbar: bool, + mouse_scrolling: bool, + mouse_scroll_margin_right: f32, } #[derive(Clone)] pub enum ScrollBoxSizeUnit { @@ -131,13 +134,13 @@ pub enum ScrollBoxSizeUnit { } impl ScrollBox { pub fn new( - mut config: GuiElemCfg, + config: GuiElemCfg, size_unit: ScrollBoxSizeUnit, children: Vec<(GuiElem, f32)>, ) -> Self { // config.redraw = true; Self { - config: config.w_scroll(), + config: config.w_scroll().w_mouse(), children, size_unit, scroll_target: 0.0, @@ -146,6 +149,9 @@ impl ScrollBox { height_bottom: 0.0, max_scroll: 0.0, last_height_px: 0.0, + mouse_in_scrollbar: false, + mouse_scrolling: false, + mouse_scroll_margin_right: 0.0, } } } @@ -160,12 +166,14 @@ impl GuiElemTrait for ScrollBox { Box::new( self.children .iter_mut() - .rev() .map(|(v, _)| v) .skip_while(|v| v.inner.config().pos.bottom_right().y < 0.0) - .take_while(|v| v.inner.config().pos.top_left().y < 1.0), + .take_while(|v| v.inner.config().pos.top_left().y <= 1.0), ) } + fn draw_rev(&self) -> bool { + false + } fn any(&self) -> &dyn std::any::Any { self } @@ -196,6 +204,8 @@ impl GuiElemTrait for ScrollBox { } // recalculate positions if self.config.redraw { + 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() { @@ -206,7 +216,10 @@ impl GuiElemTrait for ScrollBox { cfg.enabled = true; cfg.pos = Rectangle::new( Vec2::new(cfg.pos.top_left().x, 0.0f32.max(y_rel)), - Vec2::new(cfg.pos.bottom_right().x, 1.0f32.min(y_rel + h_rel)), + Vec2::new( + cfg.pos.bottom_right().x.min(max_x), + 1.0f32.min(y_rel + h_rel), + ), ); } else { e.inner.config_mut().enabled = false; @@ -217,6 +230,39 @@ impl GuiElemTrait for ScrollBox { self.max_scroll = 0.0f32.max(self.height_bottom - self.size_unit.from_rel(0.75, info.pos.height())); } + // scroll bar + self.mouse_in_scrollbar = info.mouse_pos.y >= info.pos.top_left().y + && info.mouse_pos.y <= info.pos.bottom_right().y + && info.mouse_pos.x <= info.pos.bottom_right().x + && info.mouse_pos.x >= (info.pos.bottom_right().x - self.mouse_scroll_margin_right); + if self.mouse_scrolling { + self.scroll_target = (self.max_scroll * (info.mouse_pos.y - info.pos.top_left().y) + / info.pos.height()) + .max(0.0) + .min(self.max_scroll); + } + if self.mouse_in_scrollbar + || self.mouse_scrolling + || (self.scroll_display - self.scroll_target).abs() + > self.size_unit.from_abs(1.0, info.pos.height()) + { + let y1 = info.pos.top_left().y + + info.pos.height() * self.scroll_display.min(self.scroll_target) / self.max_scroll + - 1.0; + let y2 = info.pos.top_left().y + + info.pos.height() * self.scroll_display.max(self.scroll_target) / self.max_scroll + + 1.0; + g.draw_rectangle( + Rectangle::from_tuples( + ( + info.pos.bottom_right().x - self.mouse_scroll_margin_right, + y1, + ), + (info.pos.bottom_right().x, y2), + ), + Color::WHITE, + ); + } } fn mouse_wheel(&mut self, diff: f32) -> Vec { self.scroll_target = (self.scroll_target @@ -224,6 +270,18 @@ impl GuiElemTrait for ScrollBox { .max(0.0); Vec::with_capacity(0) } + fn mouse_down(&mut self, button: MouseButton) -> Vec { + if button == MouseButton::Left && self.mouse_in_scrollbar { + self.mouse_scrolling = true; + } + vec![] + } + fn mouse_up(&mut self, button: MouseButton) -> Vec { + if button == MouseButton::Left { + self.mouse_scrolling = false; + } + vec![] + } } impl ScrollBoxSizeUnit { fn to_rel(&self, val: f32, draw_height: f32) -> f32 { diff --git a/musicdb-client/src/gui_edit.rs b/musicdb-client/src/gui_edit.rs index 5fe8492..3b6aac0 100755 --- a/musicdb-client/src/gui_edit.rs +++ b/musicdb-client/src/gui_edit.rs @@ -1,13 +1,19 @@ -use std::sync::{atomic::AtomicBool, mpsc, Arc}; +use std::{ + collections::{HashMap, HashSet}, + sync::{atomic::AtomicBool, mpsc, Arc, Mutex}, +}; use musicdb_lib::{ - data::{album::Album, artist::Artist, song::Song, AlbumId, ArtistId, SongId}, + 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::{GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, + gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, gui_base::{Button, Panel, ScrollBox}, gui_text::{Label, TextField}, }; @@ -18,37 +24,73 @@ pub struct GuiEdit { 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(ArtistId), - Album(AlbumId), - Song(SongId), + Artist(Vec), + Album(Vec), + Song(Vec), } #[derive(Clone)] pub enum Editing { NotLoaded, - Artist(Artist), - Album(Album), - Song(Song), + Artist(Vec, Vec), + Album(Vec, Vec), + Song(Vec, Vec), } +#[derive(Clone)] +pub enum ArtistChange { + SetName(String), + SetCover(Option), + RemoveAlbum(AlbumId), + AddAlbum(AlbumId), + RemoveSong(SongId), + AddSong(SongId), +} +#[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: 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![ + GuiElem::new(ScrollBox::new( + GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.6))), + crate::gui_base::ScrollBoxSizeUnit::Pixels, + vec![], + )), + GuiElem::new(ScrollBox::new( + GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.6), (1.0, 0.9))), + crate::gui_base::ScrollBoxSizeUnit::Pixels, + vec![], + )), GuiElem::new(Button::new( GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.95), (0.33, 1.0))), |_| vec![GuiAction::CloseEditPanel], @@ -123,181 +165,536 @@ impl GuiElemTrait for GuiEdit { self.send = false; match &self.editing { Editing::NotLoaded => {} - Editing::Artist(v) => info - .actions - .push(GuiAction::SendToServer(Command::ModifyArtist(v.clone()))), - Editing::Album(v) => info - .actions - .push(GuiAction::SendToServer(Command::ModifyAlbum(v.clone()))), - Editing::Song(v) => info - .actions - .push(GuiAction::SendToServer(Command::ModifySong(v.clone()))), + Editing::Artist(v, changes) => { + for v in v { + let mut v = v.clone(); + for change in changes.iter() { + match change { + ArtistChange::SetName(n) => v.name = n.clone(), + ArtistChange::SetCover(c) => v.cover = c.clone(), + ArtistChange::RemoveAlbum(id) => { + if let Some(i) = v.albums.iter().position(|id| id == id) { + v.albums.remove(i); + } + } + ArtistChange::AddAlbum(id) => { + if !v.albums.contains(id) { + v.albums.push(*id); + } + } + ArtistChange::RemoveSong(id) => { + if let Some(i) = v.singles.iter().position(|id| id == id) { + v.singles.remove(i); + } + } + ArtistChange::AddSong(id) => { + if !v.singles.contains(id) { + v.singles.push(*id); + } + } + } + } + info.actions + .push(GuiAction::SendToServer(Command::ModifyArtist(v))); + } + } + 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) => { - if let Some(v) = info.database.artists().get(id).cloned() { - Editing::Artist(v) + 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) => { - if let Some(v) = info.database.albums().get(id).cloned() { - Editing::Album(v) + 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) => { - if let Some(v) = info.database.songs().get(id).cloned() { - Editing::Song(v) + 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].inner.any_mut().downcast_mut::() { + for (c, _) in sb.children.iter() { + if let Some(p) = c + .inner + .any() + .downcast_ref::() + .and_then(|p| p.children.get(0)) + .and_then(|e| e.inner.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; - let scrollbox = if self.children.len() > 3 { - let o = self.children.pop(); - while self.children.len() > 3 { - self.children.pop(); + if let Some(sb) = self.children[0].inner.any_mut().downcast_mut::() { + for c in sb.children.iter_mut() { + c.1 = info.line_height; } - o - } else { - None - }; + } + } + } + fn dragged(&mut self, dragged: Dragging) -> Vec { + let dragged = match dragged { + Dragging::Artist(_) | Dragging::Album(_) | Dragging::Song(_) => 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![], + } + self.reload = true; + vec![] + } +} +impl GuiEdit { + fn rebuild_main(&mut self, info: &mut DrawInfo) { + if let Some(sb) = self.children[0].inner.any_mut().downcast_mut::() { + sb.children.clear(); + sb.config_mut().redraw = true; match &self.editing { - Editing::NotLoaded => { - self.children.push(GuiElem::new(Label::new( - GuiElemCfg::default(), - "nothing here".to_string(), - Color::WHITE, - None, - Vec2::new(0.5, 0.5), - ))); - } - Editing::Artist(artist) => { - self.children.push(GuiElem::new(Label::new( - GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (0.8, 0.08))), - artist.name.clone(), - Color::WHITE, - None, - Vec2::new(0.1, 0.5), - ))); - self.children.push(GuiElem::new(Label::new( - GuiElemCfg::at(Rectangle::from_tuples((0.8, 0.0), (1.0, 0.04))), - "Artist".to_string(), - Color::WHITE, - None, - Vec2::new(0.8, 0.5), - ))); - self.children.push(GuiElem::new(Label::new( - GuiElemCfg::at(Rectangle::from_tuples((0.8, 0.04), (1.0, 0.08))), - format!("#{}", artist.id), - Color::WHITE, - None, - Vec2::new(0.8, 0.5), - ))); - let mut elems = vec![]; - elems.push(( + 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(( GuiElem::new(Panel::new( GuiElemCfg::default(), - vec![ - GuiElem::new(Label::new( - GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (0.6, 1.0))), - format!( - "{} album{}", - artist.albums.len(), - if artist.albums.len() != 1 { "s" } else { "" } - ), - Color::LIGHT_GRAY, - None, - Vec2::new(0.0, 0.5), - )), - GuiElem::new(TextField::new( - GuiElemCfg::at(Rectangle::from_tuples((0.6, 0.0), (0.8, 1.0))), - "id".to_string(), - Color::DARK_GRAY, - Color::WHITE, - )), - GuiElem::new(Button::new( - GuiElemCfg::at(Rectangle::from_tuples((0.8, 0.0), (1.0, 1.0))), - { - let apply_change = self.apply_change.clone(); - let my_index = elems.len(); - move |_| { - _ = apply_change.send(Box::new(move |s| { - s.config.redraw = true; - if let Ok(id) = s - .children - .last_mut() - .unwrap() - .inner - .any_mut() - .downcast_mut::() - .unwrap() - .children[my_index] - .0 - .inner - .children() - .nth(1) - .unwrap() - .inner - .children() - .next() - .unwrap() - .inner - .any() - .downcast_ref::