From e3293fffe6c572c8b8460c1fd3a4e23075a39072 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 1 Aug 2026 13:09:43 +0200 Subject: [PATCH] feat: add right click to songs in queue --- musicdb-client/src/gui.rs | 117 ++++++---- musicdb-client/src/gui_base.rs | 48 ++-- musicdb-client/src/gui_edit_any.rs | 85 ++++--- musicdb-client/src/gui_edit_song.rs | 208 +++++++++-------- musicdb-client/src/gui_idle_display.rs | 33 ++- musicdb-client/src/gui_library.rs | 161 ++++++------- musicdb-client/src/gui_notif.rs | 8 +- musicdb-client/src/gui_playback.rs | 2 +- musicdb-client/src/gui_queue.rs | 130 ++++++----- musicdb-client/src/gui_screen.rs | 38 +-- musicdb-client/src/gui_settings.rs | 30 ++- musicdb-client/src/gui_song_adder.rs | 16 +- musicdb-client/src/gui_statusbar.rs | 4 +- musicdb-client/src/gui_text.rs | 6 +- musicdb-client/src/gui_wrappers.rs | 13 +- musicdb-client/src/main.rs | 28 +-- musicdb-client/src/textcfg.rs | 311 ++++++++++++------------- 17 files changed, 625 insertions(+), 613 deletions(-) diff --git a/musicdb-client/src/gui.rs b/musicdb-client/src/gui.rs index 9e8c7d8..c3bcfb8 100755 --- a/musicdb-client/src/gui.rs +++ b/musicdb-client/src/gui.rs @@ -658,11 +658,9 @@ pub(crate) trait GuiElemInternal: GuiElem { // adjust info let npos = adjust_area(&info.pos, &self.config_mut().pos); let ppos = std::mem::replace(&mut info.pos, npos); - if info.child_has_keyboard_focus { - if self.config().keyboard_focus_index == usize::MAX { - info.has_keyboard_focus = true; - info.child_has_keyboard_focus = false; - } + if info.child_has_keyboard_focus && self.config().keyboard_focus_index == usize::MAX { + info.has_keyboard_focus = true; + info.child_has_keyboard_focus = false; } info.mouse_pos_in_bounds = info.pos.contains(info.mouse_pos); if !info.mouse_pos_in_bounds { @@ -716,12 +714,11 @@ pub(crate) trait GuiElemInternal: GuiElem { ) -> Option> { if self.config().enabled || allow_deactivated { for c in &mut self.children() { - if c.config().enabled { - if c.config().pixel_pos.contains(pos) { - if let Some(v) = c._mouse_event(e, allow_deactivated, condition, pos) { - return Some(v); - } - } + if c.config().enabled + && c.config().pixel_pos.contains(pos) + && let Some(v) = c._mouse_event(e, allow_deactivated, condition, pos) + { + return Some(v); } } condition(self.elem_mut(), e) @@ -739,10 +736,10 @@ pub(crate) trait GuiElemInternal: GuiElem { e, false, &mut |v, e| { - if v.config().drag_target { - if let Some(d) = dragged.take() { - return Some(v.dragged(e, d)); - } + if v.config().drag_target + && let Some(d) = dragged.take() + { + return Some(v.dragged(e, d)); } None }, @@ -887,7 +884,7 @@ pub(crate) trait GuiElemInternal: GuiElem { } } fn _keyboard_move_focus(&mut self, decrement: bool, refocus: bool) -> bool { - if self.config().enabled == false { + if !self.config().enabled { return false; } let mut focus_index = if refocus { @@ -1133,7 +1130,8 @@ 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, + redraw: bool, + redraw2: bool, /// will be set to false after `draw`. /// can be used to, for example, add the keybinds for your element. pub init: bool, @@ -1193,9 +1191,27 @@ impl GuiElemCfg { self.drag_target = true; self } - pub fn force_redraw(mut self) -> Self { + pub fn redraw(&self) -> bool { + self.redraw + } + /// sets `redraw`, causing this element to redraw itself on the next frame. + pub fn redraw_once(&mut self) { self.redraw = true; - self + } + /// same as calling `redraw()` and then + /// calling `redraw()` again on the next frame. + /// in other words, the next 2 frames will see a redraw of this item. + pub fn redraw_twice(&mut self) { + self.redraw = true; + self.redraw2 = true; + } + /// sets `redraw` back to `false` (usually) + pub fn redrawn(&mut self) { + if self.redraw2 { + self.redraw2 = false; + } else { + self.redraw = false; + } } pub fn disabled(mut self) -> Self { self.enabled = false; @@ -1207,6 +1223,7 @@ impl Default for GuiElemCfg { Self { enabled: true, redraw: false, + redraw2: false, init: true, pos: Rectangle::new(Vec2::ZERO, Vec2::new(1.0, 1.0)), pixel_pos: Rectangle::ZERO, @@ -1270,6 +1287,7 @@ pub enum Dragging { Queue(Result>), Queues(Vec), } +#[allow(clippy::enum_variant_names)] pub enum SpecificGuiElem { SearchArtist, SearchAlbum, @@ -1322,7 +1340,7 @@ impl Gui { pub fn exec_gui_action(&mut self, action: GuiAction) { match action { GuiAction::Build(f) => { - let actions = f(&mut *self.database.lock().unwrap()); + let actions = f(&mut self.database.lock().unwrap()); for action in actions { self.exec_gui_action(action); } @@ -1478,7 +1496,7 @@ impl WindowHandler for Gui { time: draw_start_time, actions: Vec::with_capacity(0), pos: Rectangle::new(Vec2::ZERO, self.size.into_f32()), - database: &mut *dblock, + database: &mut dblock, font: &self.font, mouse_pos: self.mouse_pos, mouse_pos_in_bounds: false, @@ -1572,9 +1590,9 @@ impl WindowHandler for Gui { } } fn on_mouse_button_down(&mut self, helper: &mut WindowHelper, button: MouseButton) { - if let Some(a) = - self.gui - ._mouse_button(&mut EventInfo::new(), button, true, self.mouse_pos.clone()) + if let Some(a) = self + .gui + ._mouse_button(&mut EventInfo::new(), button, true, self.mouse_pos) { for a in a { self.exec_gui_action(a) @@ -1586,9 +1604,9 @@ impl WindowHandler for Gui { if self.dragging.is_some() { let (dr, _) = self.dragging.take().unwrap(); let mut opt = Some(dr); - if let Some(a) = - self.gui - ._release_drag(&mut EventInfo::new(), &mut opt, self.mouse_pos.clone()) + if let Some(a) = self + .gui + ._release_drag(&mut EventInfo::new(), &mut opt, self.mouse_pos) { for a in a { self.exec_gui_action(a) @@ -1609,7 +1627,7 @@ impl WindowHandler for Gui { } if let Some(a) = self.gui - ._mouse_button(&mut EventInfo::new(), button, false, self.mouse_pos.clone()) + ._mouse_button(&mut EventInfo::new(), button, false, self.mouse_pos) { for a in a { self.exec_gui_action(a) @@ -1639,7 +1657,7 @@ impl WindowHandler for Gui { }; if let Some(a) = self .gui - ._mouse_wheel(&mut EventInfo::new(), dist, self.mouse_pos.clone()) + ._mouse_wheel(&mut EventInfo::new(), dist, self.mouse_pos) { for a in a { self.exec_gui_action(a) @@ -1673,10 +1691,10 @@ impl WindowHandler for Gui { 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); - } + if let Some(VirtualKeyCode::Tab) = virtual_key_code + && !(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( &mut EventInfo::new(), @@ -1717,18 +1735,19 @@ impl WindowHandler for Gui { // handle keybinds unless settings are open, opening or closing let mut e = EventInfo::new(); let mut post_action = None; - if self.gui.settings.0 == false && self.gui.settings.1.is_none() { - if let Some(key) = virtual_key_code { - let keybind = KeyBinding::new(&self.modifiers, key); - if let Some(action) = self.keybinds.get(&keybind) { - if action.has_priority() { - e.take(); - for a in self.key_actions.get(&action.id()).execute() { - self.exec_gui_action(a); - } - } else { - post_action = Some(action.id()); + if !self.gui.settings.0 + && self.gui.settings.1.is_none() + && let Some(key) = virtual_key_code + { + let keybind = KeyBinding::new(&self.modifiers, key); + if let Some(action) = self.keybinds.get(&keybind) { + if action.has_priority() { + e.take(); + for a in self.key_actions.get(&action.id()).execute() { + self.exec_gui_action(a); } + } else { + post_action = Some(action.id()); } } } @@ -1760,11 +1779,11 @@ impl WindowHandler for Gui { ) { self.exec_gui_action(a); } - if let Some(post_action) = post_action.take() { - if e.take() { - for a in self.key_actions.get(&post_action).execute() { - self.exec_gui_action(a); - } + if let Some(post_action) = post_action.take() + && e.take() + { + for a in self.key_actions.get(&post_action).execute() { + self.exec_gui_action(a); } } } diff --git a/musicdb-client/src/gui_base.rs b/musicdb-client/src/gui_base.rs index 187eaf9..170da61 100755 --- a/musicdb-client/src/gui_base.rs +++ b/musicdb-client/src/gui_base.rs @@ -73,7 +73,7 @@ pub struct Square { #[allow(unused)] impl Square { pub fn new(mut config: GuiElemCfg, inner: T) -> Self { - config.redraw = true; + config.redraw_once(); Self { config, inner } } } @@ -101,10 +101,10 @@ impl GuiElem for Square { } fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { if info.pos.size() != self.config.pixel_pos.size() { - self.config.redraw = true; + self.config.redraw_once(); } - if self.config.redraw { - self.config.redraw = false; + if self.config.redraw() { + self.config.redrawn(); if info.pos.width() > info.pos.height() { let w = 0.5 * info.pos.height() / info.pos.width(); self.inner.config_mut().pos = @@ -193,7 +193,7 @@ impl GuiElem for ScrollBox { } fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { if self.config.pixel_pos.size() != info.pos.size() { - self.config.redraw = true; + self.config.redraw_once(); } // smooth scrolling animation if self.scroll_target > self.max_scroll { @@ -202,7 +202,7 @@ impl GuiElem for ScrollBox { self.scroll_target = 0.0; } if self.scroll_target != self.scroll_display { - self.config.redraw = true; + self.config.redraw_once(); if info.high_performance { self.scroll_display = self.scroll_target; } else { @@ -215,7 +215,7 @@ impl GuiElem for ScrollBox { } } // recalculate positions - if self.config.redraw { + if self.config.redraw() { // adjust height vector length if necessary if self.children_heights.len() != self.children.len() { let target = self.children.len(); @@ -229,7 +229,7 @@ impl GuiElem for ScrollBox { // 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; + self.config.redrawn(); let mut y_pos = -self.scroll_display; for (e, h) in self.children.iter().zip(self.children_heights.iter()) { let h_rel = self.size_unit.to_rel(*h, info.pos.height()); @@ -292,8 +292,8 @@ impl GuiElem for ScrollBox { } } fn mouse_wheel(&mut self, e: &mut EventInfo, diff: f32) -> Vec { - let nst = (self.scroll_target - self.size_unit.from_abs(diff as f32, self.last_height_px)) - .max(0.0); + let nst = + (self.scroll_target - self.size_unit.from_abs(diff, self.last_height_px)).max(0.0); // only take the event if this would actually scroll, and only scroll if we can actually take the event if nst != self.scroll_target && e.take() { self.scroll_target = nst; @@ -323,12 +323,14 @@ impl ScrollBoxSizeUnit { Self::Pixels => val / draw_height, } } + #[allow(clippy::wrong_self_convention)] fn from_rel(&self, val: f32, draw_height: f32) -> f32 { match self { Self::Relative => val, Self::Pixels => val * draw_height, } } + #[allow(clippy::wrong_self_convention)] fn from_abs(&self, val: f32, draw_height: f32) -> f32 { match self { Self::Relative => val / draw_height, @@ -545,21 +547,13 @@ impl Slider { { if since >= 1.0 { s.display_since = None; - if s.display { - 1.0 - } else { - 0.0 - } + if s.display { 1.0 } else { 0.0 } } else { if let Some(h) = &i.helper { h.request_redraw(); } - s.config.redraw = true; - if s.display { - since - } else { - 1.0 - since - } + s.config.redraw_once(); + if s.display { since } else { 1.0 - since } } } else { 1.0 @@ -613,7 +607,7 @@ impl GuiElem for Slider { if self.display != (self.config.mouse_down.0 || info.pos.contains(info.mouse_pos)) { self.display = !self.display; self.display_since = Some(info.time); - self.config.redraw = true; + self.config.redraw_once(); } let dot_size = (info.pos.height() * 0.9).min(info.pos.width() * 0.25); let y_mid_line = 0.5 * (info.pos.top_left().y + info.pos.bottom_right().y); @@ -634,10 +628,8 @@ impl GuiElem for Slider { (info.mouse_pos.x - line_pos.top_left().x) as f64 / line_pos.width() as f64, )); self.val_changed = true; - for v in &mut self.val_changed_subs { - *v = true; - } - self.config.redraw = true; + self.val_changed_subs.fill(true); + self.config.redraw_once(); } let line_color = Color::from_int_rgb(50, 50, 100); g.draw_circle( @@ -660,8 +652,8 @@ impl GuiElem for Slider { 0.5 * dot_size, Color::CYAN, ); - if self.config.redraw { - self.config.redraw = false; + if self.config.redraw() { + self.config.redrawn(); (Arc::clone(&self.on_update))(self, info); } } diff --git a/musicdb-client/src/gui_edit_any.rs b/musicdb-client/src/gui_edit_any.rs index 30c7f20..a1d6c7d 100644 --- a/musicdb-client/src/gui_edit_any.rs +++ b/musicdb-client/src/gui_edit_any.rs @@ -1,16 +1,16 @@ use std::{collections::BTreeSet, time::Instant}; use speedy2d::{ + Graphics2D, color::Color, dimen::{Vec2, Vector2}, shape::Rectangle, - Graphics2D, }; use crate::{ gui::{DrawInfo, GuiElem, GuiElemCfg}, gui_anim::AnimationController, - gui_base::{Button, ScrollBox}, + gui_base::{Button, Panel, ScrollBox}, gui_text::{Label, TextField}, }; @@ -24,36 +24,62 @@ pub enum Event { pub struct EditorForAnyTagInList { config: GuiElemCfg, pub tag: String, - label: Label, - rm_button: Button<[IconDelete; 1]>, + panel: Panel<(Label, Button<[IconDelete; 1]>)>, } impl EditorForAnyTagInList { pub fn new + 'static>( tag: String, + index: usize, sender: std::sync::mpsc::Sender, config: GuiElemCfg, ) -> Self { - Self { - config, - tag: tag.clone(), - label: Label::new( - GuiElemCfg::default(), - tag.clone(), - Color::WHITE, - None, - Vector2::new(0.0, 0.5), - ), - rm_button: Button::new( - GuiElemCfg::default(), + let label = Label::new( + GuiElemCfg::default(), + tag.clone(), + Color::WHITE, + None, + Vector2::new(0.0, 0.5), + ); + let rm_button = Button::new( + GuiElemCfg::default(), + { + let tag = tag.clone(); move |btn| { btn.disable(); sender.send(Event::RemoveTag(tag.clone()).into()).unwrap(); vec![] - }, - [IconDelete::new(GuiElemCfg::default())], - ), - } + } + }, + [IconDelete::new(GuiElemCfg::default())], + ); + let panel = Panel::with_background( + GuiElemCfg::default(), + (label, rm_button), + match index % 2 { + 1 => Color::from_rgba(0.0, 1.0, 0.0, 0.1), + _ => Color::from_rgba(0.0, 0.0, 1.0, 0.14), + }, + ); + Self { config, tag, panel } + } + fn row(&self) -> &(Label, Button<[IconDelete; 1]>) { + &self.panel.children + } + fn row_mut(&mut self) -> &mut (Label, Button<[IconDelete; 1]>) { + &mut self.panel.children + } + fn label(&self) -> &Label { + &self.panel.children.0 + } + fn rm_button(&self) -> &Button<[IconDelete; 1]> { + &self.panel.children.1 + } + fn label_mut(&mut self) -> &mut Label { + &mut self.panel.children.0 + } + fn rm_button_mut(&mut self) -> &mut Button<[IconDelete; 1]> { + &mut self.panel.children.1 } } @@ -63,11 +89,11 @@ impl GuiElem for EditorForAnyTagInList { let rm_button_padding = (info.pos.height() - rm_button_size) / 2.0; let label_padding = info.pos.height() * 0.05; let x_split = (info.pos.width() - rm_button_size) / info.pos.width(); - self.rm_button.config_mut().pos = Rectangle::from_tuples( + self.rm_button_mut().config_mut().pos = Rectangle::from_tuples( (x_split, rm_button_padding / info.pos.height()), (1.0, 1.0 - rm_button_padding / info.pos.height()), ); - self.label.config_mut().pos = Rectangle::from_tuples( + self.label_mut().config_mut().pos = Rectangle::from_tuples( (0.0, label_padding / info.pos.height()), (x_split, 1.0 - label_padding / info.pos.height()), ); @@ -79,7 +105,7 @@ impl GuiElem for EditorForAnyTagInList { &mut self.config } fn children(&mut self) -> Box + '_> { - Box::new([self.label.elem_mut(), self.rm_button.elem_mut()].into_iter()) + Box::new([self.panel.elem_mut()].into_iter()) } fn any(&self) -> &dyn std::any::Any { self @@ -162,7 +188,7 @@ impl + 'static> EditorForAnyTagAdder { expand_to, c_value: TextField::new( GuiElemCfg::default(), - "artist".to_owned(), + "tag".to_owned(), Color::DARK_GRAY, Color::WHITE, ), @@ -180,7 +206,7 @@ impl + 'static> EditorForAnyTagAdder { self.last_search = "\n".to_owned(); self.c_value.c_input.content.text().clear(); self.open_prog.set_target(now, 1.0); - self.config_mut().redraw = true; + self.config_mut().redraw_once(); } } impl + 'static> GuiElem for EditorForAnyTagAdder { @@ -196,8 +222,8 @@ impl + 'static> GuiElem for EditorForAnyTagAdder { } let search = self.c_value.c_input.content.get_text().to_lowercase(); - let search_changed = &self.last_search != &search; - if self.config.redraw || search_changed { + let search_changed = self.last_search != search; + if self.config.redraw() || search_changed { *self.c_value.c_input.content.color() = Color::WHITE; if search_changed { if search.is_empty() { @@ -224,7 +250,7 @@ impl + 'static> GuiElem for EditorForAnyTagAdder { .flat_map(|s| s.general.tags.iter()), ) .filter(|tag| tag.to_lowercase().contains(&search)) - .map(|tag| tag.clone()) + .cloned() .collect::>(); if !tags.contains(self.c_value.c_input.content.get_text()) { tags.insert(self.c_value.c_input.content.get_text().clone()); @@ -252,8 +278,9 @@ impl + 'static> GuiElem for EditorForAnyTagAdder { ) }) .collect(); - self.c_picker.config_mut().redraw = true; + self.c_picker.config_mut().redraw_once(); self.last_search = search; + self.config.redrawn(); } } fn config(&self) -> &GuiElemCfg { diff --git a/musicdb-client/src/gui_edit_song.rs b/musicdb-client/src/gui_edit_song.rs index a6b4289..82c993c 100644 --- a/musicdb-client/src/gui_edit_song.rs +++ b/musicdb-client/src/gui_edit_song.rs @@ -1,7 +1,7 @@ use std::time::Instant; use musicdb_lib::{ - data::{song::Song, ArtistId}, + data::{ArtistId, song::Song}, server::{Action, Req}, }; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle}; @@ -11,7 +11,7 @@ use crate::{ gui::{GuiAction, GuiElem, GuiElemCfg, GuiElemChildren}, gui_anim::AnimationController, gui_base::{Button, Panel, ScrollBox}, - gui_edit_any::{EditorForAnyTagAdder, EditorForAnyTagInList, SpacerForScrollBox, ELEM_HEIGHT}, + gui_edit_any::{ELEM_HEIGHT, EditorForAnyTagAdder, EditorForAnyTagInList, SpacerForScrollBox}, gui_text::{Label, TextField}, }; @@ -28,6 +28,7 @@ pub struct EditorForSongs { event_sender: std::sync::mpsc::Sender, event_recv: std::sync::mpsc::Receiver, } +#[allow(clippy::enum_variant_names)] pub enum Event { Close, Apply, @@ -102,7 +103,7 @@ impl EditorForSongs { c_artist: EditorForSongArtistChooser::new(sender.clone()), c_album: Label::new( GuiElemCfg::default(), - format!("(todo...)"), + "(todo...)".to_owned(), Color::GRAY, None, Vec2::new(0.0, 0.5), @@ -117,9 +118,11 @@ impl EditorForSongs { } } tags.into_iter() - .map(|tag| { + .enumerate() + .map(|(i, tag)| { EditorForAnyTagInList::new( tag.to_owned(), + i, sender.clone(), GuiElemCfg::default(), ) @@ -198,110 +201,112 @@ impl GuiElem for EditorForSongs { ) } 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 => { - let mut actions = Vec::new(); - for song in self.songs.iter() { - let mut song = song.clone(); + while let Ok(e) = self.event_recv.try_recv() { + 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 => { + let mut actions = Vec::new(); + for song in self.songs.iter() { + 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; - } - actions.push(Action::ModifySong(song, Req::none())); - } - if actions.len() == 1 { - info.actions - .push(GuiAction::SendToServer(actions.pop().unwrap())); - } else if actions.len() > 1 { - info.actions - .push(GuiAction::SendToServer(Action::Multiple(actions))); - } - } - 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 - .set_target(info.time, 1.0); - *self + let new_title = self .c_scrollbox .children - .c_artist - .c_name + .c_title .c_input .content - .text() = name; - self.c_scrollbox.children.c_artist.config_mut().redraw = true; + .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; + } + actions.push(Action::ModifySong(song, Req::none())); } - Event::GeneralEvent(e) => { - use super::gui_edit_any::Event as GeneralEvent; - match e { - GeneralEvent::RemoveTag(tag) => { - for song in self.songs.iter_mut() { - if let Some(i) = - song.general.tags.iter().position(|t| *t == tag) - { - song.general.tags.remove(i); - } - } - if let Some(i) = (&self.c_scrollbox.children.c_tags) - .into_iter() - .position(|e| e.tag == tag) - { - self.c_scrollbox.children.c_tags.remove(i); - self.c_scrollbox.config_mut().redraw = true; + if actions.len() == 1 { + info.actions + .push(GuiAction::SendToServer(actions.pop().unwrap())); + } else if actions.len() > 1 { + info.actions + .push(GuiAction::SendToServer(Action::Multiple(actions))); + } + } + 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 + .set_target(info.time, 1.0); + *self + .c_scrollbox + .children + .c_artist + .c_name + .c_input + .content + .text() = name; + self.c_scrollbox + .children + .c_artist + .config_mut() + .redraw_once(); + } + Event::GeneralEvent(e) => { + use super::gui_edit_any::Event as GeneralEvent; + match e { + GeneralEvent::RemoveTag(tag) => { + for song in self.songs.iter_mut() { + if let Some(i) = song.general.tags.iter().position(|t| *t == tag) { + song.general.tags.remove(i); } } - GeneralEvent::AddTag(tag) => { - self.c_scrollbox.children.c_new_tag.clear(info.time); - for song in self.songs.iter_mut() { - if !song.general.tags.contains(&tag) { - song.general.tags.push(tag.clone()); - } - } - if !(&self.c_scrollbox.children.c_tags) - .into_iter() - .any(|e| e.tag == tag) - { - self.c_scrollbox.children_heights.insert( - 3 + self.c_scrollbox.children.c_tags.len(), - ELEM_HEIGHT, - ); - self.c_scrollbox.children.c_tags.push( - EditorForAnyTagInList::new( - tag, - self.event_sender.clone(), - GuiElemCfg::default(), - ), - ); - self.c_scrollbox.config_mut().redraw = true; + if let Some(i) = (&self.c_scrollbox.children.c_tags) + .into_iter() + .position(|e| e.tag == tag) + { + self.c_scrollbox.children.c_tags.remove(i); + self.c_scrollbox.config_mut().redraw_once(); + } + } + GeneralEvent::AddTag(tag) => { + self.c_scrollbox.children.c_new_tag.clear(info.time); + for song in self.songs.iter_mut() { + if !song.general.tags.contains(&tag) { + song.general.tags.push(tag.clone()); } } + if !(&self.c_scrollbox.children.c_tags) + .into_iter() + .any(|e| e.tag == tag) + { + self.c_scrollbox.children_heights.insert( + 3 + self.c_scrollbox.children.c_tags.len(), + ELEM_HEIGHT, + ); + let i = self.c_scrollbox.children.c_tags.len(); + self.c_scrollbox + .children + .c_tags + .push(EditorForAnyTagInList::new( + tag, + i, + self.event_sender.clone(), + GuiElemCfg::default(), + )); + self.c_scrollbox.config_mut().redraw_once(); + } } } - }, - Err(_) => break, + } } } // animation @@ -332,7 +337,7 @@ impl GuiElem for EditorForSongs { { if let Some(v) = self.c_scrollbox.children_heights.get_mut(1) { *v = ELEM_HEIGHT * val as f32; - self.c_scrollbox.config_mut().redraw = true; + self.c_scrollbox.config_mut().redraw_once(); } if let Some(h) = &info.helper { h.request_redraw(); @@ -351,7 +356,7 @@ impl GuiElem for EditorForSongs { .get_mut(3 + self.c_scrollbox.children.c_tags.len()) { *v = ELEM_HEIGHT * val as f32; - self.c_scrollbox.config_mut().redraw = true; + self.c_scrollbox.config_mut().redraw_once(); } if let Some(h) = &info.helper { h.request_redraw(); @@ -428,8 +433,8 @@ impl GuiElem for EditorForSongArtistChooser { } 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 { + 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 { @@ -480,8 +485,9 @@ impl GuiElem for EditorForSongArtistChooser { ) }) .collect(); - self.c_picker.config_mut().redraw = true; + self.c_picker.config_mut().redraw_once(); self.last_search = search; + self.config.redrawn(); } } fn config(&self) -> &GuiElemCfg { diff --git a/musicdb-client/src/gui_idle_display.rs b/musicdb-client/src/gui_idle_display.rs index de2f346..ec8c40a 100644 --- a/musicdb-client/src/gui_idle_display.rs +++ b/musicdb-client/src/gui_idle_display.rs @@ -126,27 +126,27 @@ impl GuiElem for IdleDisplay { self.c_top_label.content = if let Some(song) = self.current_info.current_song { info.gui_config .idle_top_text - .gen_new(&info.database, info.database.get_song(&song)) + .gen_new(info.database, info.database.get_song(&song)) } else { vec![] }; - self.c_top_label.config_mut().redraw = true; + self.c_top_label.config_mut().redraw_once(); self.c_side1_label.content = if let Some(song) = self.current_info.current_song { info.gui_config .idle_side1_text - .gen_new(&info.database, info.database.get_song(&song)) + .gen_new(info.database, info.database.get_song(&song)) } else { vec![] }; - self.c_side1_label.config_mut().redraw = true; + self.c_side1_label.config_mut().redraw_once(); self.c_side2_label.content = if let Some(song) = self.current_info.current_song { info.gui_config .idle_side2_text - .gen_new(&info.database, info.database.get_song(&song)) + .gen_new(info.database, info.database.get_song(&song)) } else { vec![] }; - self.c_side2_label.config_mut().redraw = true; + self.c_side2_label.config_mut().redraw_once(); // check artist if let Some(artist_id) = self .current_info @@ -165,8 +165,8 @@ impl GuiElem for IdleDisplay { self.artist_image_aspect_ratio.set_target(info.time, 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..]); + if let Some(tag) = tag.strip_prefix("ImageExt=") { + let filename = format!("{}.{}", artist.name, tag); self.current_artist_image = Some((artist_id, Some((filename.clone(), None)))); if !info.custom_images.contains_key(&filename) { @@ -199,15 +199,14 @@ impl GuiElem for IdleDisplay { 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); - } - } + if let Some((_, Some((img, h)))) = &mut self.current_artist_image + && h.is_none() + && 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 diff --git a/musicdb-client/src/gui_library.rs b/musicdb-client/src/gui_library.rs index e9453a3..c3d3715 100755 --- a/musicdb-client/src/gui_library.rs +++ b/musicdb-client/src/gui_library.rs @@ -3,18 +3,19 @@ use std::{ collections::HashSet, sync::Arc, sync::{ + Mutex, atomic::{AtomicBool, AtomicUsize}, - mpsc, Mutex, + mpsc, }, }; use musicdb_lib::data::{ + AlbumId, ArtistId, GeneralData, SongId, album::Album, artist::Artist, database::Database, queue::{Queue, QueueContent}, song::Song, - AlbumId, ArtistId, GeneralData, SongId, }; use regex::{Regex, RegexBuilder}; use speedy2d::{ @@ -296,12 +297,8 @@ impl GuiElem for LibraryBrowser { false } fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { - loop { - if let Ok(action) = self.do_something_receiver.try_recv() { - action(self); - } else { - break; - } + while let Ok(action) = self.do_something_receiver.try_recv() { + action(self); } // search let mut search_changed = false; @@ -396,7 +393,7 @@ impl GuiElem for LibraryBrowser { // - if self.library_updated { self.library_updated = false; - self.update_local_library(&info.database, |(_, a), (_, b)| a.name.cmp(&b.name)); + self.update_local_library(info.database, |(_, a), (_, b)| a.name.cmp(&b.name)); search_changed = true; } if search_changed { @@ -404,7 +401,7 @@ impl GuiElem for LibraryBrowser { s: &LibraryBrowser, pat: &str, regex: &Option, - search_text: &String, + search_text: &str, filter: &Filter, search_gd: &GeneralData, ) -> f32 { @@ -414,7 +411,7 @@ impl GuiElem for LibraryBrowser { if let Some(r) = regex { if s.search_prefers_start_matches { r.find_iter(pat) - .map(|m| match pat[0..m.start()].chars().rev().next() { + .map(|m| match pat[0..m.start()].chars().next_back() { // found at the start of h, reaches to the end (whole pattern is part of the match) None if m.end() == pat.len() => 6.0, // found at start of h @@ -433,11 +430,7 @@ impl GuiElem for LibraryBrowser { }) .fold(0.0, f32::max) } else { - if r.is_match(pat) { - 2.0 - } else { - 0.0 - } + if r.is_match(pat) { 2.0 } else { 0.0 } } } else if search_text.is_empty() { 1.0 @@ -448,7 +441,7 @@ impl GuiElem for LibraryBrowser { let allow_singles = self.search_album.is_empty() && self.filter_albums.lock().unwrap().filters.is_empty(); self.filter_local_library( - &info.database, + info.database, |s, artist| { filter( s, @@ -496,35 +489,35 @@ impl GuiElem for LibraryBrowser { self.selected_popup_state.1 = artists; self.selected_popup_state.2 = albums; self.selected_popup_state.3 = songs; - if artists > 0 || albums > 0 || songs > 0 { - if let Some(text) = match (artists, albums, songs) { + if (artists > 0 || albums > 0 || songs > 0) + && let Some(text) = match (artists, albums, songs) { (0, 0, 0) => None, - (0, 0, 1) => Some(format!("1 song selected")), + (0, 0, 1) => Some("1 song selected".to_owned()), (0, 0, s) => Some(format!("{s} songs selected")), - (0, 1, 0) => Some(format!("1 album selected")), + (0, 1, 0) => Some("1 album selected".to_owned()), (0, al, 0) => Some(format!("{al} albums selected")), - (1, 0, 0) => Some(format!("1 artist selected")), + (1, 0, 0) => Some("1 artist selected".to_owned()), (ar, 0, 0) => Some(format!("{ar} artists selected")), - (0, 1, 1) => Some(format!("1 song and 1 album selected")), + (0, 1, 1) => Some("1 song and 1 album selected".to_owned()), (0, 1, s) => Some(format!("{s} songs and 1 album selected")), (0, al, 1) => Some(format!("1 song and {al} albums selected")), (0, al, s) => Some(format!("{s} songs and {al} albums selected")), - (1, 0, 1) => Some(format!("1 song and 1 artist selected")), + (1, 0, 1) => Some("1 song and 1 artist selected".to_owned()), (1, 0, s) => Some(format!("{s} songs and 1 artist selected")), (ar, 0, 1) => Some(format!("1 song and {ar} artists selected")), (ar, 0, s) => Some(format!("{s} songs and {ar} artists selected")), - (1, 1, 0) => Some(format!("1 album and 1 artist selected")), + (1, 1, 0) => Some("1 album and 1 artist selected".to_owned()), (1, al, 0) => Some(format!("{al} albums and 1 artist selected")), (ar, 1, 0) => Some(format!("1 album and {ar} artists selected")), (ar, al, 0) => Some(format!("{al} albums and {ar} artists selected")), - (1, 1, 1) => Some(format!("1 song, 1 album and 1 artist selected")), + (1, 1, 1) => Some("1 song, 1 album and 1 artist selected".to_owned()), (1, 1, s) => Some(format!("{s} songs, 1 album and 1 artist selected")), (1, al, 1) => { Some(format!("1 song, {al} albums and 1 artist selected")) @@ -544,14 +537,13 @@ impl GuiElem for LibraryBrowser { (ar, al, s) => { Some(format!("{s} songs, {al} albums and {ar} artists selected")) } - } { - *self.c_selected_counter_panel.children[0].content.text() = text; } - } else { + { + *self.c_selected_counter_panel.children[0].content.text() = text; } } } - self.config.redraw = true; + self.config.redraw_once(); } // selected popup { @@ -571,7 +563,7 @@ impl GuiElem for LibraryBrowser { } else { if self.selected_popup_state.0 != 0.0 { redraw = true; - self.selected_popup_state.0 = 0.7 * self.selected_popup_state.0; + self.selected_popup_state.0 *= 0.7; if self.selected_popup_state.0 < 0.01 { self.selected_popup_state.0 = 0.0; self.c_selected_counter_panel.config_mut().enabled = false; @@ -588,9 +580,9 @@ impl GuiElem for LibraryBrowser { } } } - if self.config.redraw || info.pos.size() != self.config.pixel_pos.size() { - self.config.redraw = false; - self.update_ui(&info.database, info.line_height); + if self.config.redraw() || info.pos.size() != self.config.pixel_pos.size() { + self.config.redrawn(); + self.update_ui(info.database, info.line_height); } } fn updated_library(&mut self) { @@ -631,13 +623,13 @@ impl LibraryBrowser { self.library_sorted = artists .into_iter() .map(|(ar_id, artist)| { - let singles = artist.singles.iter().map(|id| *id).collect(); + let singles = artist.singles.clone(); let albums = artist .albums .iter() .map(|id| { let songs = if let Some(album) = db.albums().get(id) { - album.songs.iter().map(|id| *id).collect() + album.songs.clone() } else { eprintln!("[warn] No album with id {id} found in db!"); vec![] @@ -762,7 +754,7 @@ impl LibraryBrowser { let library_scroll_box = &mut self.c_scroll_box; library_scroll_box.children = elems; library_scroll_box.children_heights = elemh; - library_scroll_box.config_mut().redraw = true; + library_scroll_box.config_mut().redraw_once(); } fn build_ui_element_artist(&self, id: ArtistId, db: &Database, h: f32) -> (ListElement, f32) { ( @@ -878,7 +870,7 @@ impl ListArtist { None, Vec2::new(0.0, 0.5), ); - config.redraw = true; + config.redraw_once(); Self { config: config.w_mouse(), id, @@ -913,8 +905,8 @@ impl GuiElem for ListArtist { self } fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { - if self.config.redraw { - self.config.redraw = false; + if self.config.redraw() { + self.config.redrawn(); let sel = self.selected.contains_artist(&self.id); if sel != self.sel { self.sel = sel; @@ -972,7 +964,7 @@ impl GuiElem for ListArtist { fn mouse_up(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec { if self.mouse && button == MouseButton::Left { self.mouse = false; - self.config.redraw = true; + self.config.redraw_once(); if e.take() { if !self.sel { self.selected.insert_artist(self.id); @@ -1024,7 +1016,7 @@ impl ListAlbum { ), ]], ); - config.redraw = true; + config.redraw_once(); Self { config: config.w_mouse(), id, @@ -1059,8 +1051,8 @@ impl GuiElem for ListAlbum { self } fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { - if self.config.redraw { - self.config.redraw = false; + if self.config.redraw() { + self.config.redrawn(); let sel = self.selected.contains_album(&self.id); if sel != self.sel { self.sel = sel; @@ -1118,7 +1110,7 @@ impl GuiElem for ListAlbum { fn mouse_up(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec { if self.mouse && button == MouseButton::Left { self.mouse = false; - self.config.redraw = true; + self.config.redraw_once(); if e.take() { if !self.sel { self.selected.insert_album(self.id); @@ -1167,7 +1159,7 @@ impl ListSong { ), ]], ); - config.redraw = true; + config.redraw_once(); Self { config: config.w_mouse(), id, @@ -1202,8 +1194,8 @@ impl GuiElem for ListSong { self } fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { - if self.config.redraw { - self.config.redraw = false; + if self.config.redraw() { + self.config.redrawn(); let sel = self.selected.contains_song(&self.id); if sel != self.sel { self.sel = sel; @@ -1261,7 +1253,7 @@ impl GuiElem for ListSong { fn mouse_up(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec { if self.mouse && button == MouseButton::Left { self.mouse = false; - self.config.redraw = true; + self.config.redraw_once(); if e.take() { if !self.sel { self.selected.insert_song(self.id); @@ -1288,7 +1280,7 @@ impl GuiElem for ListSong { }, [Label::new( GuiElemCfg::default(), - format!("Edit this song"), + "Edit this song".to_owned(), Color::WHITE, None, Vec2::new_y(0.5), @@ -1315,7 +1307,7 @@ impl GuiElem for ListSong { }, [Label::new( GuiElemCfg::default(), - format!("Edit selected songs"), + "Edit selected songs".to_owned(), Color::WHITE, None, Vec2::new_y(0.5), @@ -1354,6 +1346,7 @@ struct FilterTab { buttons: Vec>, filters: Vec, } +#[allow(clippy::large_enum_variant)] enum FilterLine { Joiner(Button<[Label; 1]>), Not(Label), @@ -1394,10 +1387,10 @@ impl GuiElemChildren for FilterTab { self.buttons.len() + self.filters.len() } } -const FP_CASESENS_N: &'static str = "search is case-insensitive"; -const FP_CASESENS_Y: &'static str = "search is case-sensitive!"; -const FP_PREFSTART_N: &'static str = "simple search"; -const FP_PREFSTART_Y: &'static str = "will prefer matches at the start of a word"; +const FP_CASESENS_N: &str = "search is case-insensitive"; +const FP_CASESENS_Y: &str = "search is case-sensitive!"; +const FP_PREFSTART_N: &str = "simple search"; +const FP_PREFSTART_Y: &str = "will prefer matches at the start of a word"; impl FilterPanel { pub fn new( search_settings_changed: Arc, @@ -1697,8 +1690,8 @@ impl FilterPanel { .iter() .cloned() .map(|(text, preset)| { - let f = Arc::clone(&filter); - let oc = Arc::clone(&on_change); + let f = Arc::clone(filter); + let oc = Arc::clone(on_change); Button::new( GuiElemCfg::default(), move |_| { @@ -1876,26 +1869,24 @@ impl FilterPanel { let oc = Arc::clone(on_change); let p = path.clone(); tf1.on_changed = Some(Box::new(move |text| { - if let Ok(n) = text.parse() { - if let Some(Ok(FilterType::TagWithValueInt(_, v, _))) = + if let Ok(n) = text.parse() + && let Some(Ok(FilterType::TagWithValueInt(_, v, _))) = mx.lock().unwrap().get_mut(&p) - { - *v = n; - oc(false); - } + { + *v = n; + oc(false); } })); let mx = Arc::clone(mutex); let oc = Arc::clone(on_change); let p = path.clone(); tf2.on_changed = Some(Box::new(move |text| { - if let Ok(n) = text.parse() { - if let Some(Ok(FilterType::TagWithValueInt(_, _, v))) = + if let Ok(n) = text.parse() + && let Some(Ok(FilterType::TagWithValueInt(_, _, v))) = mx.lock().unwrap().get_mut(&p) - { - *v = n; - oc(false); - } + { + *v = n; + oc(false); } })); children.push(FilterLine::TagWithValueInt(Panel::new( @@ -1922,22 +1913,20 @@ impl GuiElem for FilterPanel { fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { // set line height if info.line_height != self.line_height { - for h in &mut self.c_tab_main.children_heights { - *h = info.line_height; - } - for h in &mut self.c_tab_filters_songs.children_heights { - *h = info.line_height; - } - for h in &mut self.c_tab_filters_albums.children_heights { - *h = info.line_height; - } - for h in &mut self.c_tab_filters_artists.children_heights { - *h = info.line_height; - } - self.c_tab_main.config_mut().redraw = true; - self.c_tab_filters_songs.config_mut().redraw = true; - self.c_tab_filters_albums.config_mut().redraw = true; - self.c_tab_filters_artists.config_mut().redraw = true; + self.c_tab_main.children_heights.fill(info.line_height); + self.c_tab_filters_songs + .children_heights + .fill(info.line_height); + self.c_tab_filters_albums + .children_heights + .fill(info.line_height); + self.c_tab_filters_artists + .children_heights + .fill(info.line_height); + self.c_tab_main.config_mut().redraw_once(); + self.c_tab_filters_songs.config_mut().redraw_once(); + self.c_tab_filters_albums.config_mut().redraw_once(); + self.c_tab_filters_artists.config_mut().redraw_once(); self.line_height = info.line_height; } // maybe switch tabs @@ -2001,7 +1990,7 @@ impl GuiElem for FilterPanel { ); sb.children = ft; sb.children_heights = heights; - sb.config_mut().redraw = true; + sb.config_mut().redraw_once(); } _ => {} } diff --git a/musicdb-client/src/gui_notif.rs b/musicdb-client/src/gui_notif.rs index 35b781e..6bc6c34 100755 --- a/musicdb-client/src/gui_notif.rs +++ b/musicdb-client/src/gui_notif.rs @@ -177,10 +177,10 @@ impl GuiElem for NotifOverlay { } } // redraw - if !self.notifs.is_empty() { - if let Some(h) = &info.helper { - h.request_redraw(); - } + if !self.notifs.is_empty() + && let Some(h) = &info.helper + { + h.request_redraw(); } } fn draw_rev(&self) -> bool { diff --git a/musicdb-client/src/gui_playback.rs b/musicdb-client/src/gui_playback.rs index 51c87b5..0b12f85 100755 --- a/musicdb-client/src/gui_playback.rs +++ b/musicdb-client/src/gui_playback.rs @@ -86,7 +86,7 @@ impl CurrentInfo { GuiElemCfg::default(), [Label::new( GuiElemCfg::default(), - format!("Couldn't load cover"), + "Couldn't load cover".to_owned(), Color::WHITE, None, Vec2::new(0.5, 0.5), diff --git a/musicdb-client/src/gui_queue.rs b/musicdb-client/src/gui_queue.rs index d580571..d41ba1f 100755 --- a/musicdb-client/src/gui_queue.rs +++ b/musicdb-client/src/gui_queue.rs @@ -1,9 +1,9 @@ use musicdb_lib::{ data::{ + AlbumId, ArtistId, database::Database, queue::{Queue, QueueContent, QueueDuration}, song::Song, - AlbumId, ArtistId, }, server::{Action, Req}, }; @@ -16,7 +16,7 @@ use speedy2d::{ use crate::{ gui::{Dragging, DrawInfo, EventInfo, GuiAction, GuiElem, GuiElemCfg}, - gui_base::{Panel, ScrollBox}, + gui_base::{Button, Panel, ScrollBox}, gui_text::{self, AdvancedLabel, Label, TextField}, }; @@ -95,7 +95,7 @@ impl QueueViewer { musicdb_lib::data::queue::QueueFolder { index: 0, content: vec![], - name: format!("folder name"), + name: "folder name".to_owned(), order: None, }, false, @@ -104,7 +104,7 @@ impl QueueViewer { { let mut tf = TextField::new( GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.5), (1.0, 1.0))), - format!("folder name"), + "folder name".to_owned(), Color::from_rgb(0.0, 0.33, 0.0), Color::from_rgb(0.0, 0.67, 0.0), ); @@ -219,8 +219,8 @@ impl GuiElem for QueueViewer { } } } - let dt = fmt_dur(info.database.queue.duration_total(&info.database)); - let dr = fmt_dur(info.database.queue.duration_remaining(&info.database)); + 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::AdvancedContent::Text(gui_text::Content::new( @@ -239,15 +239,15 @@ impl GuiElem for QueueViewer { 1.0, )], ]; - label.config_mut().redraw = true; + label.config_mut().redraw_once(); } - if self.config.redraw || info.pos.size() != self.config.pixel_pos.size() { - self.config.redraw = false; + if self.config.redraw() || info.pos.size() != self.config.pixel_pos.size() { + self.config.redrawn(); let mut c = vec![]; let mut h = vec![]; queue_gui( &info.database.queue, - &info.database, + info.database, 0.0, 0.02, info.line_height, @@ -260,12 +260,12 @@ impl GuiElem for QueueViewer { let scroll_box = &mut self.c_scroll_box; scroll_box.children = c; scroll_box.children_heights = h; - scroll_box.config_mut().redraw = true; + scroll_box.config_mut().redraw_once(); } } fn updated_queue(&mut self) { self.queue_updated = true; - self.config.redraw = true; + self.config.redraw_once(); } } @@ -353,7 +353,7 @@ fn queue_gui( Box::new(QueueLoop::new(cfg.clone(), path, queue.clone(), current)), ); if let Some(mut inner) = queue_gui( - &inner, + inner, db, depth, depth_inc_by, @@ -423,7 +423,7 @@ impl GuiElem for QueueEmptySpaceDragHandler { fn generic_queue_draw( info: &mut DrawInfo, - path: &Vec, + path: &[usize], queue: impl FnOnce() -> Queue, mouse: &mut bool, copy_on_mouse_down: bool, @@ -435,7 +435,7 @@ fn generic_queue_draw( Dragging::Queue(if copy_on_mouse_down { Ok(queue()) } else { - Err(path.clone()) + Err(path.to_vec()) }), None, )))); @@ -567,8 +567,24 @@ impl GuiElem for QueueSong { if button == MouseButton::Left && e.take() { self.mouse = true; self.copy_on_mouse_down = self.copy; + vec![] + } else if button == MouseButton::Right && e.take() { + let me = self.song.clone(); + let menu_actions: Vec> = vec![Box::new(Button::new( + GuiElemCfg::default(), + move |_| vec![GuiAction::EditSongs(vec![me.clone()])], + [Label::new( + GuiElemCfg::default(), + "Edit this song".to_owned(), + Color::WHITE, + None, + Vec2::new_y(0.5), + )], + ))]; + vec![GuiAction::ContextMenu(Some(menu_actions))] + } else { + vec![] } - vec![] } fn mouse_up(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec { if self.mouse && button == MouseButton::Left { @@ -648,10 +664,8 @@ impl GuiElem for QueueSong { } }, move |mut p, q| { - if insert_below { - if let Some(l) = p.last_mut() { - *l += 1; - } + if insert_below && let Some(l) = p.last_mut() { + *l += 1; } Action::QueueMove(q, p) }, @@ -971,9 +985,9 @@ impl QueueLoop { match queue.content() { QueueContent::Loop(total, _current, _) => { if *total == 0 { - format!("repeat forever") + "repeat forever".to_owned() } else if *total == 1 { - format!("repeat 1 time") + "repeat 1 time".to_owned() } else { format!("repeat {total} times") } @@ -1141,46 +1155,38 @@ fn dragged_add_to_queue( } fn add_to_queue_album_by_id(id: AlbumId, db: &Database) -> Option { - if let Some(album) = db.albums().get(&id) { - Some( - QueueContent::Folder(musicdb_lib::data::queue::QueueFolder { - index: 0, - content: album - .songs - .iter() - .map(|id| QueueContent::Song(*id).into()) - .collect(), - name: album.name.clone(), - order: None, - }) - .into(), - ) - } else { - None - } + db.albums().get(&id).map(|album| { + QueueContent::Folder(musicdb_lib::data::queue::QueueFolder { + index: 0, + content: album + .songs + .iter() + .map(|id| QueueContent::Song(*id).into()) + .collect(), + name: album.name.clone(), + order: None, + }) + .into() + }) } fn add_to_queue_artist_by_id(id: ArtistId, db: &Database) -> Option { - if let Some(artist) = db.artists().get(&id) { - Some( - QueueContent::Folder(musicdb_lib::data::queue::QueueFolder { - index: 0, - content: artist - .singles - .iter() - .map(|id| QueueContent::Song(*id).into()) - .chain( - artist - .albums - .iter() - .filter_map(|id| add_to_queue_album_by_id(*id, db)), - ) - .collect(), - name: artist.name.clone(), - order: None, - }) - .into(), - ) - } else { - None - } + db.artists().get(&id).map(|artist| { + QueueContent::Folder(musicdb_lib::data::queue::QueueFolder { + index: 0, + content: artist + .singles + .iter() + .map(|id| QueueContent::Song(*id).into()) + .chain( + artist + .albums + .iter() + .filter_map(|id| add_to_queue_album_by_id(*id, db)), + ) + .collect(), + name: artist.name.clone(), + order: None, + }) + .into() + }) } diff --git a/musicdb-client/src/gui_screen.rs b/musicdb-client/src/gui_screen.rs index 1519036..be795dd 100755 --- a/musicdb-client/src/gui_screen.rs +++ b/musicdb-client/src/gui_screen.rs @@ -4,8 +4,8 @@ use musicdb_lib::{ data::queue::{QueueContent, QueueFolder}, server::{Action, Req}, }; -use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::VirtualKeyCode, Graphics2D}; -use uianimator::{default_animator_f64_quadratic::DefaultAnimatorF64Quadratic, Animator}; +use speedy2d::{Graphics2D, color::Color, dimen::Vec2, shape::Rectangle, window::VirtualKeyCode}; +use uianimator::{Animator, default_animator_f64_quadratic::DefaultAnimatorF64Quadratic}; use crate::{ gui::{ @@ -54,7 +54,6 @@ pub struct GuiScreen { pub c_main_view: Panel, pub c_context_menu: Option>, pub idle: DefaultAnimatorF64Quadratic, - pub idle_prev_val: f32, // pub settings: (bool, Option), pub settings: (bool, Option), pub last_interaction: Instant, @@ -172,7 +171,6 @@ impl GuiScreen { c_context_menu: None, hotkey: Hotkey::new_noshift(VirtualKeyCode::Escape), idle: DefaultAnimatorF64Quadratic::new(0.0, 0.67), - idle_prev_val: 0.0, settings: (false, None), last_interaction: Instant::now(), idle_timeout: Some(60.0), @@ -184,17 +182,9 @@ impl GuiScreen { let prog = since.elapsed().as_secs_f32() / seconds; if prog >= 1.0 { v.1 = None; - if v.0 { - 1.0 - } else { - 0.0 - } + if v.0 { 1.0 } else { 0.0 } } else { - if v.0 { - prog - } else { - 1.0 - prog - } + if v.0 { prog } else { 1.0 - prog } } } else if v.0 { 1.0 @@ -222,12 +212,11 @@ impl GuiScreen { self.idle.set_target(0.0, Instant::now()); } fn idle_check(&mut self) { - if self.idle.target() == 0.0 { - if let Some(dur) = &self.idle_timeout { - if self.last_interaction.elapsed().as_secs_f64() > *dur { - self.idle.set_target(1.0, Instant::now()); - } - } + if self.idle.target() == 0.0 + && let Some(dur) = &self.idle_timeout + && self.last_interaction.elapsed().as_secs_f64() > *dur + { + self.idle.set_target(1.0, Instant::now()); } } @@ -253,7 +242,7 @@ impl GuiElem for GuiScreen { ] .into_iter() .chain(self.c_editing_songs.as_mut().map(|v| v.elem_mut())) - .chain(self.c_song_adder.as_mut().map(|v| v.elem_mut()).into_iter()) + .chain(self.c_song_adder.as_mut().map(|v| v.elem_mut())) .chain([ self.c_status_bar.elem_mut(), self.c_settings.elem_mut(), @@ -404,15 +393,12 @@ impl GuiElem for GuiScreen { }; // request_redraw for animations let idle_value = self.idle.get_value(info.time) as f32; - let idle_changed = self.idle_prev_val != idle_value; + let idle_changed = self.idle.target() as f32 != idle_value; if idle_changed || idle_exit_anim || self.settings.1.is_some() { - self.idle_prev_val = idle_value; if let Some(h) = &info.helper { h.request_redraw() } - } - // animations: idle - if idle_changed { + // animations: idle let enable_normal_ui = idle_value < 1.0; self.set_normal_ui_enabled(enable_normal_ui); if let Some(h) = &info.helper { diff --git a/musicdb-client/src/gui_settings.rs b/musicdb-client/src/gui_settings.rs index 1bf05af..50b6d57 100755 --- a/musicdb-client/src/gui_settings.rs +++ b/musicdb-client/src/gui_settings.rs @@ -1,12 +1,12 @@ -use std::sync::{atomic::AtomicBool, Arc, Mutex}; +use std::sync::{Arc, Mutex, atomic::AtomicBool}; use musicdb_lib::server::Action; use speedy2d::{ + Graphics2D, color::Color, dimen::Vec2, shape::Rectangle, window::{KeyScancode, ModifiersState, MouseButton, VirtualKeyCode}, - Graphics2D, }; use crate::{ @@ -32,7 +32,7 @@ impl Settings { scroll_sensitivity_lines: f64, scroll_sensitivity_pages: f64, ) -> Self { - config.redraw = true; + config.redraw_once(); Self { config, c_scroll_box: ScrollBox::new( @@ -53,11 +53,7 @@ impl Settings { } pub fn get_timeout_val(&self) -> Option { let v = self.c_scroll_box.children.idle_time.children.1.val; - if v > 0.0 { - Some(v * v) - } else { - None - } + if v > 0.0 { Some(v * v) } else { None } } } pub struct SettingsContent { @@ -125,7 +121,7 @@ impl KeybindInput { b.key, ) } else { - format!("") + String::new() }, Color::WHITE, None, @@ -429,7 +425,7 @@ impl SettingsContent { } if hours == 0 && minutes < 10 && (seconds > 0 || minutes == 0) { s.push_str(&seconds.to_string()); - s.push_str("s"); + s.push('s'); } else if s.ends_with(" ") { s.pop(); } @@ -532,7 +528,7 @@ impl GuiElem for Settings { } fn draw(&mut self, info: &mut DrawInfo, _g: &mut Graphics2D) { if self.c_scroll_box.children.draw(info) { - self.c_scroll_box.config_mut().redraw = true; + self.c_scroll_box.config_mut().redraw_once(); } let scrollbox = &mut self.c_scroll_box; let background = &mut self.c_background; @@ -547,9 +543,9 @@ impl GuiElem for Settings { settings_opacity_slider.val as _, ); } - if self.config.redraw { - self.config.redraw = false; - scrollbox.config_mut().redraw = true; + if self.config.redraw() { + self.config.redrawn(); + scrollbox.config_mut().redraw_once(); if scrollbox.children_heights.len() == scrollbox.children.len() { for (i, h) in scrollbox.children_heights.iter_mut().enumerate() { *h = if i == 0 || i >= 8 { @@ -560,7 +556,7 @@ impl GuiElem for Settings { } } else { // try again next frame (scrollbox will autofill the children_heights vec) - self.config.redraw = true; + self.config.redraw_once(); } } } @@ -591,7 +587,7 @@ pub fn build_keybind_elems( vec![ vec![( AdvancedContent::Text(Content::new( - format!("{}", v.title), + v.title.to_string(), if v.enabled { Color::WHITE } else { @@ -603,7 +599,7 @@ pub fn build_keybind_elems( )], vec![( AdvancedContent::Text(Content::new( - format!("{}", v.description), + v.description.to_string(), if v.enabled { Color::LIGHT_GRAY } else { diff --git a/musicdb-client/src/gui_song_adder.rs b/musicdb-client/src/gui_song_adder.rs index 2cdf176..6988e1f 100644 --- a/musicdb-client/src/gui_song_adder.rs +++ b/musicdb-client/src/gui_song_adder.rs @@ -1,5 +1,5 @@ use musicdb_lib::data::{AlbumId, ArtistId}; -use speedy2d::{color::Color, dimen::Vec2, Graphics2D}; +use speedy2d::{Graphics2D, color::Color, dimen::Vec2}; use crate::{ gui::{DrawInfo, GuiElem, GuiElemCfg}, @@ -30,13 +30,13 @@ impl SongAdder { scroll_sensitivity_lines: f64, scroll_sensitivity_pages: f64, ) -> Self { - config.redraw = true; + config.redraw_once(); Self { config, state: 0, c_loading: Some(Label::new( GuiElemCfg::default(), - format!("Loading..."), + "Loading...".to_owned(), Color::GRAY, None, Vec2::new(0.5, 0.5), @@ -99,7 +99,7 @@ impl GuiElem for SongAdder { .iter() .map(|(path, is_bad)| AddableSong::new(path.to_owned(), *is_bad)) .collect(); - self.c_scroll_box.config_mut().redraw = true; + self.c_scroll_box.config_mut().redraw_once(); self.data = Some( data.into_iter() .map(|(p, b)| AddSong { @@ -125,9 +125,9 @@ impl GuiElem for SongAdder { } } - if self.config.redraw { - self.config.redraw = false; - self.c_scroll_box.config_mut().redraw = true; + if self.config.redraw() { + self.config.redrawn(); + self.c_scroll_box.config_mut().redraw_once(); } } } @@ -147,7 +147,7 @@ impl AddableSong { |_| vec![], [Label::new( GuiElemCfg::default(), - format!("{path}"), + path.to_string(), if is_bad { Color::LIGHT_GRAY } else { diff --git a/musicdb-client/src/gui_statusbar.rs b/musicdb-client/src/gui_statusbar.rs index 72c0435..7fc419c 100644 --- a/musicdb-client/src/gui_statusbar.rs +++ b/musicdb-client/src/gui_statusbar.rs @@ -61,11 +61,11 @@ impl GuiElem for StatusBar { self.c_song_label.content = if let Some(song) = self.current_info.current_song { info.gui_config .status_bar_text - .gen_new(&info.database, info.database.get_song(&song)) + .gen_new(info.database, info.database.get_song(&song)) } else { vec![] }; - self.c_song_label.config_mut().redraw = true; + self.c_song_label.config_mut().redraw_once(); } if self.current_info.new_cover { self.current_info.new_cover = false; diff --git a/musicdb-client/src/gui_text.rs b/musicdb-client/src/gui_text.rs index 0a293e5..81be377 100755 --- a/musicdb-client/src/gui_text.rs +++ b/musicdb-client/src/gui_text.rs @@ -387,14 +387,14 @@ impl GuiElem for AdvancedLabel { self } fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) { - if self.config.redraw + if self.config.redraw() || self.config.pixel_pos.size() != info.pos.size() || self .content .iter() .any(|v| v.iter().any(|(c, _, _)| c.will_redraw())) { - self.config.redraw = false; + self.config.redrawn(); let mut max_len = 0.0; let mut total_height = 0.0; for line in &self.content { @@ -461,7 +461,7 @@ impl GuiElem for AdvancedLabel { if handle.is_none() { match source { ImageSource::Cover(id) => { - if let Some(img) = info.covers.get_mut(&id) { + if let Some(img) = info.covers.get_mut(id) { if let Some(img) = img.get_init(g) { *handle = Some(Some(img)); } else { diff --git a/musicdb-client/src/gui_wrappers.rs b/musicdb-client/src/gui_wrappers.rs index fd22fb4..3f48e37 100755 --- a/musicdb-client/src/gui_wrappers.rs +++ b/musicdb-client/src/gui_wrappers.rs @@ -18,14 +18,13 @@ impl Hotkey { if self.modifiers == u8::MAX { return false; } - down == false + !down && key.is_some_and(|v| v == self.key) - && (self.modifiers & 0b10 == 1 || (self.modifiers & 0b01 == 1) == modifiers.ctrl()) - && (self.modifiers & 0b1000 == 1 || (self.modifiers & 0b0100 == 1) == modifiers.shift()) - && (self.modifiers & 0b100000 == 1 - || (self.modifiers & 0b010000 == 1) == modifiers.alt()) - && (self.modifiers & 0b10000000 == 1 - || (self.modifiers & 0b01000000 == 1) == modifiers.logo()) + && (self.modifiers & 0b10 > 0 || (self.modifiers & 0b01 > 0) == modifiers.ctrl()) + && (self.modifiers & 0b1000 > 0 || (self.modifiers & 0b0100 > 0) == modifiers.shift()) + && (self.modifiers & 0b100000 > 0 || (self.modifiers & 0b010000 > 0) == modifiers.alt()) + && (self.modifiers & 0b10000000 > 0 + || (self.modifiers & 0b01000000 > 0) == modifiers.logo()) } /// unlike noshift, this ignores the shift modifier pub fn new_key(key: VirtualKeyCode) -> Self { diff --git a/musicdb-client/src/main.rs b/musicdb-client/src/main.rs index bd461c5..42ff02c 100755 --- a/musicdb-client/src/main.rs +++ b/musicdb-client/src/main.rs @@ -1,5 +1,7 @@ #![allow(dead_code)] #![allow(unused_variables)] +#![allow(clippy::type_complexity)] +#![allow(clippy::too_many_arguments)] use std::{ io::{BufReader, Write}, @@ -18,8 +20,8 @@ use musicdb_lib::data::cache_manager::CacheManager; use musicdb_lib::player::{Player, PlayerBackendFeat}; use musicdb_lib::{ data::{ - database::{ClientIo, Database}, CoverId, SongId, + database::{ClientIo, Database}, }, load::ToFromBytes, server::Command, @@ -107,7 +109,9 @@ fn main() { #[cfg(not(feature = "speedy2d"))] #[cfg(not(feature = "mers"))] #[cfg(not(feature = "playback"))] - compile_error!("None of the optional features are enabled. Without at least one of these, the application is useless! See Cargo.toml for info."); + compile_error!( + "None of the optional features are enabled. Without at least one of these, the application is useless! See Cargo.toml for info." + ); // parse args let args = Args::parse(); // start @@ -215,7 +219,7 @@ fn main() { } #[cfg(feature = "playback")] if let Some(player) = &mut player { - player.update_dont_uncache(&mut *db); + player.update_dont_uncache(&mut db); } drop(db); #[cfg(feature = "speedy2d")] @@ -250,10 +254,12 @@ fn main() { Some(Arc::clone(&get_con)); } let occasional_refresh_sender = Arc::clone(&sender); - thread::spawn(move || loop { - std::thread::sleep(std::time::Duration::from_secs(1)); - if let Some(v) = &*occasional_refresh_sender.lock().unwrap() { - v.send_event(GuiEvent::Refresh).unwrap(); + thread::spawn(move || { + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + if let Some(v) = &*occasional_refresh_sender.lock().unwrap() { + v.send_event(GuiEvent::Refresh).unwrap(); + } } }); gui::main( @@ -328,12 +334,8 @@ fn main() { pub fn accumulate Option, T>(mut f: F) -> Vec { let mut o = vec![]; - loop { - if let Some(v) = f() { - o.push(v); - } else { - break; - } + while let Some(v) = f() { + o.push(v); } o } diff --git a/musicdb-client/src/textcfg.rs b/musicdb-client/src/textcfg.rs index 0bc698f..6d4c27b 100755 --- a/musicdb-client/src/textcfg.rs +++ b/musicdb-client/src/textcfg.rs @@ -113,7 +113,7 @@ impl TextBuilder { } for part in &self.0 { match part { - TextPart::LineBreak => out.push(std::mem::replace(line, vec![])), + TextPart::LineBreak => out.push(std::mem::take(line)), TextPart::SetColor(c) => *color = *c, TextPart::SetScale(v) => *scale = *v, TextPart::SetHeightAlign(v) => *align = *v, @@ -124,17 +124,17 @@ impl TextBuilder { } } TextPart::AlbumName => { - if let Some(s) = current_song { - if let Some(album) = s.album.and_then(|id| db.albums().get(&id)) { - push!(album.name.to_owned()); - } + if let Some(s) = current_song + && let Some(album) = s.album.and_then(|id| db.albums().get(&id)) + { + push!(album.name.to_owned()); } } TextPart::ArtistName => { - if let Some(s) = current_song { - if let Some(artist) = db.artists().get(&s.artist) { - push!(artist.name.to_owned()); - } + if let Some(s) = current_song + && let Some(artist) = db.artists().get(&s.artist) + { + push!(artist.name.to_owned()); } } TextPart::SongDuration(show_millis) => { @@ -152,7 +152,7 @@ impl TextBuilder { } TextPart::TagEq(p) => { for (i, g) in all_general(db, ¤t_song).into_iter().enumerate() { - if let Some(_) = g.and_then(|g| g.tags.iter().find(|t| *t == p)) { + if g.and_then(|g| g.tags.iter().find(|t| *t == p)).is_some() { push!( match i { 0 => 's', @@ -223,174 +223,165 @@ impl TextBuilder { if current.starts_with(' ') { current = current.replacen(' ', "\u{00A0}", 1); } - vec.push(TextPart::Literal(std::mem::replace( - &mut current, - String::new(), - ))); + vec.push(TextPart::Literal(std::mem::take(&mut current))); } }; } - loop { - if let Some(ch) = chars.next() { - match ch { - '\n' => { + while let Some(ch) = chars.next() { + match ch { + '\n' => { + done!(); + vec.push(TextPart::LineBreak); + } + '\\' => match chars.next() { + None => current.push('\\'), + Some('t') => { done!(); - vec.push(TextPart::LineBreak); + vec.push(TextPart::SongTitle); } - '\\' => match chars.next() { - None => current.push('\\'), - Some('t') => { - done!(); - vec.push(TextPart::SongTitle); - } - Some('a') => { - done!(); - vec.push(TextPart::AlbumName); - } - Some('A') => { - done!(); - vec.push(TextPart::ArtistName); - } - Some('d') => { - done!(); - vec.push(TextPart::SongDuration(false)); - } - Some('D') => { - done!(); - vec.push(TextPart::SongDuration(true)); - } - Some('s') => { - done!(); - vec.push(TextPart::SetScale({ - let mut str = String::new(); - loop { - match chars.next() { - None | Some(';') => break, - Some(c) => str.push(c), - } - } - if let Ok(v) = str.parse() { - v - } else { - return Err(TextBuilderParseError::CouldntParse( - str, - "number (float)".to_string(), - )); - } - })) - } - Some('h') => { - done!(); - vec.push(TextPart::SetHeightAlign({ - let mut str = String::new(); - loop { - match chars.next() { - None | Some(';') => break, - Some(c) => str.push(c), - } - } - if let Ok(v) = str.parse() { - v - } else { - return Err(TextBuilderParseError::CouldntParse( - str, - "number (float)".to_string(), - )); - } - })) - } - Some('c') => { - done!(); - vec.push(TextPart::SetColor({ - let mut str = String::new(); - for _ in 0..6 { - if let Some(ch) = chars.next() { - str.push(ch); - } else { - return Err(TextBuilderParseError::TooFewCharsForColor); - } - } - if let Ok(i) = u32::from_str_radix(&str, 16) { - Color::from_hex_rgb(i) - } else { - return Err(TextBuilderParseError::ColorNotHex); - } - })); - } - Some('i') => { - done!(); - let mut src = String::new(); + Some('a') => { + done!(); + vec.push(TextPart::AlbumName); + } + Some('A') => { + done!(); + vec.push(TextPart::ArtistName); + } + Some('d') => { + done!(); + vec.push(TextPart::SongDuration(false)); + } + Some('D') => { + done!(); + vec.push(TextPart::SongDuration(true)); + } + Some('s') => { + done!(); + vec.push(TextPart::SetScale({ + let mut str = String::new(); loop { match chars.next() { - None => { - return Err(TextBuilderParseError::InvalidImageSourceName( - src, - )); - } - Some(':') => break, - Some(c) => src.push(c), + None | Some(';') => break, + Some(c) => str.push(c), } } - vec.push(match src.as_str() { - "Cover" => { - let mut id = String::new(); - loop { - match chars.next() { - None | Some(';') => break, - Some(c) => id.push(c), - } - } - if let Ok(id) = id.parse() { - TextPart::ImgCover(id) - } else { - return Err(TextBuilderParseError::InvalidImageCoverId(id)); - } - } - "CustomFile" => TextPart::ImgCustom(Self::from_chars(chars)?), - _ => { - return Err(TextBuilderParseError::InvalidImageSourceName(src)); - } - }); - } - Some(ch) => current.push(ch), - }, - '%' => { + if let Ok(v) = str.parse() { + v + } else { + return Err(TextBuilderParseError::CouldntParse( + str, + "number (float)".to_string(), + )); + } + })) + } + Some('h') => { done!(); - let mode = if let Some(ch) = chars.next() { - ch - } else { - return Err(TextBuilderParseError::UnclosedPercent); - }; + vec.push(TextPart::SetHeightAlign({ + let mut str = String::new(); + loop { + match chars.next() { + None | Some(';') => break, + Some(c) => str.push(c), + } + } + if let Ok(v) = str.parse() { + v + } else { + return Err(TextBuilderParseError::CouldntParse( + str, + "number (float)".to_string(), + )); + } + })) + } + Some('c') => { + done!(); + vec.push(TextPart::SetColor({ + let mut str = String::new(); + for _ in 0..6 { + if let Some(ch) = chars.next() { + str.push(ch); + } else { + return Err(TextBuilderParseError::TooFewCharsForColor); + } + } + if let Ok(i) = u32::from_str_radix(&str, 16) { + Color::from_hex_rgb(i) + } else { + return Err(TextBuilderParseError::ColorNotHex); + } + })); + } + Some('i') => { + done!(); + let mut src = String::new(); loop { match chars.next() { - Some('%') => { - let s = std::mem::replace(&mut current, String::new()); - vec.push(match mode { - '=' => TextPart::TagEq(s), - '>' => TextPart::TagEnd(s), - '_' => TextPart::TagContains(s), - c => return Err(TextBuilderParseError::TagModeUnknown(c)), - }); - break; + None => { + return Err(TextBuilderParseError::InvalidImageSourceName(src)); } - Some(ch) => current.push(ch), - None => return Err(TextBuilderParseError::UnclosedPercent), + Some(':') => break, + Some(c) => src.push(c), } } + vec.push(match src.as_str() { + "Cover" => { + let mut id = String::new(); + loop { + match chars.next() { + None | Some(';') => break, + Some(c) => id.push(c), + } + } + if let Ok(id) = id.parse() { + TextPart::ImgCover(id) + } else { + return Err(TextBuilderParseError::InvalidImageCoverId(id)); + } + } + "CustomFile" => TextPart::ImgCustom(Self::from_chars(chars)?), + _ => { + return Err(TextBuilderParseError::InvalidImageSourceName(src)); + } + }); } - '?' => { - done!(); - vec.push(TextPart::If( - Self::from_chars(chars)?, - Self::from_chars(chars)?, - Self::from_chars(chars)?, - )); + Some(ch) => current.push(ch), + }, + '%' => { + done!(); + let mode = if let Some(ch) = chars.next() { + ch + } else { + return Err(TextBuilderParseError::UnclosedPercent); + }; + loop { + match chars.next() { + Some('%') => { + let s = std::mem::take(&mut current); + vec.push(match mode { + '=' => TextPart::TagEq(s), + '>' => TextPart::TagEnd(s), + '_' => TextPart::TagContains(s), + c => return Err(TextBuilderParseError::TagModeUnknown(c)), + }); + break; + } + Some(ch) => current.push(ch), + None => return Err(TextBuilderParseError::UnclosedPercent), + } } - '#' => break, - ch => current.push(ch), } - } else { - break; + '?' => { + done!(); + vec.push(TextPart::If( + Self::from_chars(chars)?, + Self::from_chars(chars)?, + Self::from_chars(chars)?, + )); + } + '#' => break, + ch => current.push(ch), } } done!();