diff --git a/musicdb-client/src/config_gui.toml b/musicdb-client/src/config_gui.toml index abb4501..c407cf5 100755 --- a/musicdb-client/src/config_gui.toml +++ b/musicdb-client/src/config_gui.toml @@ -10,6 +10,10 @@ font = '' # \h1.0;: set the height-alignment (to the default: 1.0 / align text to be on one baseline) # \cRRGGBB: set color to this hex value. # \cFFFFFF: default color (white) +# \iCover:;: show cover with this ID (ID >= 0) +# \iCover:0; +# \iCustomFile::: show cover stored in custom files at the given path (path terminated by #, is another textcfg) +# \iCustomFile:my_image.jpg# # \: (\\ => \, \# => #, \% => %, ...) # custom properties: # %% @@ -31,8 +35,19 @@ font = '' status_bar = '''\t \s0.5;?\A#\c505050by \c593D6E\A##?\a#?\A# ##\c505050on \c264524\a##\c808080?%>Year=%# (%>Year=%)## | \d''' -idle_top = '''\t \s0.5;\c505050(\d?%>Genre=%#, %>Genre=%##) -?\A#\c505050by \c593D6E\A##?\a#?\A# ##\c505050on \c264524\a##\c808080?%>Year=%# (%>Year=%)##''' +# Two lines +# 1: +# - Title (Size 1.0, White) +# - Duration (and, if set, Genre) in brackets (Size 0.5, Gray) +# - (\h0.5; = set height-align to 0.5, only affects flag image in second line) +# 2: +# - if there is an Artist: +# - "by " (purple) +# - if there is a "Flag: " tag, show the image saved as ".png" +# - "on ", if there is an Album (green) +# - "()", if there is a Year= tag (gray) +idle_top = '''\t \s0.5;\c505050(\d?%>Genre=%#, %>Genre=%##)\h0.5; +?\A#\c505050by \c593D6E\A?%>Flag: %# \s0.25;\iCustomFile:%>Flag: %.png#\s0.5;####?\a#?\A# ##\c505050on \c264524\a##\c808080?%>Year=%# (%>Year=%)##''' idle_side1 = '' idle_side2 = '' diff --git a/musicdb-client/src/gui_edit_song.rs b/musicdb-client/src/gui_edit_song.rs index 411653b..99ed3b3 100644 --- a/musicdb-client/src/gui_edit_song.rs +++ b/musicdb-client/src/gui_edit_song.rs @@ -1,7 +1,4 @@ -use std::{ - sync::{atomic::AtomicU8, Arc}, - time::Instant, -}; +use std::time::Instant; use musicdb_lib::data::{song::Song, ArtistId}; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle}; diff --git a/musicdb-client/src/gui_library.rs b/musicdb-client/src/gui_library.rs index 1110931..db32dde 100755 --- a/musicdb-client/src/gui_library.rs +++ b/musicdb-client/src/gui_library.rs @@ -1004,12 +1004,18 @@ impl ListAlbum { Vec2::new(0.0, 0.5), vec![vec![ ( - gui_text::Content::new(name, Color::from_int_rgb(8, 61, 47)), + gui_text::AdvancedContent::Text(gui_text::Content::new( + name, + Color::from_int_rgb(8, 61, 47), + )), 1.0, 1.0, ), ( - gui_text::Content::new(half_sized_info, Color::GRAY), + gui_text::AdvancedContent::Text(gui_text::Content::new( + half_sized_info, + Color::GRAY, + )), 0.5, 1.0, ), @@ -1142,11 +1148,18 @@ impl ListSong { Vec2::new(0.0, 0.5), vec![vec![ ( - gui_text::Content::new(name, Color::from_int_rgb(175, 175, 175)), + gui_text::AdvancedContent::Text(gui_text::Content::new( + name, + Color::from_int_rgb(175, 175, 175), + )), 1.0, 1.0, ), - (gui_text::Content::new(duration, Color::GRAY), 0.6, 1.0), + ( + gui_text::AdvancedContent::Text(gui_text::Content::new(duration, Color::GRAY)), + 0.6, + 1.0, + ), ]], ); config.redraw = true; diff --git a/musicdb-client/src/gui_queue.rs b/musicdb-client/src/gui_queue.rs index 5cf4689..d6f2c1c 100755 --- a/musicdb-client/src/gui_queue.rs +++ b/musicdb-client/src/gui_queue.rs @@ -181,12 +181,18 @@ impl GuiElem for QueueViewer { let dr = fmt_dur(info.database.queue.duration_remaining(&info.database)); label.content = vec![ vec![( - gui_text::Content::new(format!("Total: {dt}"), Color::GRAY), + gui_text::AdvancedContent::Text(gui_text::Content::new( + format!("Total: {dt}"), + Color::GRAY, + )), 1.0, 1.0, )], vec![( - gui_text::Content::new(format!("Remaining: {dr}"), Color::GRAY), + gui_text::AdvancedContent::Text(gui_text::Content::new( + format!("Remaining: {dr}"), + Color::GRAY, + )), 1.0, 1.0, )], @@ -454,19 +460,19 @@ impl QueueSong { Vec2::new(0.0, 0.5), vec![vec![ ( - gui_text::Content::new( + gui_text::AdvancedContent::Text(gui_text::Content::new( song.title.clone(), if current { Color::from_int_rgb(194, 76, 178) } else { Color::from_int_rgb(120, 76, 194) }, - ), + )), 1.0, 1.0, ), ( - gui_text::Content::new( + gui_text::AdvancedContent::Text(gui_text::Content::new( { let duration = song.duration_millis / 1000; format!(" {}:{:0>2}", duration / 60, duration % 60) @@ -476,7 +482,7 @@ impl QueueSong { } else { Color::DARK_GRAY }, - ), + )), 0.6, 1.0, ), diff --git a/musicdb-client/src/gui_text.rs b/musicdb-client/src/gui_text.rs index 0af9811..e916c09 100755 --- a/musicdb-client/src/gui_text.rs +++ b/musicdb-client/src/gui_text.rs @@ -1,14 +1,16 @@ -use std::rc::Rc; +use std::{fmt::Display, rc::Rc, sync::Arc}; +use musicdb_lib::data::CoverId; use speedy2d::{ color::Color, dimen::Vec2, font::{FormattedTextBlock, TextLayout, TextOptions}, + image::ImageHandle, shape::Rectangle, window::{ModifiersState, MouseButton}, }; -use crate::gui::{GuiAction, GuiElem, GuiElemCfg}; +use crate::gui::{GuiAction, GuiElem, GuiElemCfg, GuiServerImage}; /* @@ -30,6 +32,7 @@ pub struct Content { background: Option, formatted: Option>, } + #[allow(unused)] impl Content { pub fn new(text: String, color: Color) -> Self { @@ -286,6 +289,36 @@ impl GuiElem for TextField { } } +#[derive(Clone)] +pub enum AdvancedContent { + Text(Content), + Image { + source: ImageSource, + handle: Option>, + }, +} +#[derive(Clone)] +pub enum ImageSource { + Cover(CoverId), + CustomFile(String), +} +impl AdvancedContent { + pub fn will_redraw(&self) -> bool { + match self { + Self::Text(c) => c.will_redraw(), + Self::Image { source: _, handle } => handle.is_none(), + } + } +} +impl Display for AdvancedContent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Text(c) => write!(f, "{}", c.text), + Self::Image { .. } => Ok(()), + } + } +} + /// More advanced version of `Label`. /// Allows stringing together multiple `Content`s in one line. pub struct AdvancedLabel { @@ -297,13 +330,17 @@ pub struct AdvancedLabel { pub align: Vec2, /// (Content, Size-Scale, Height) /// Size-Scale and Height should default to 1.0. - pub content: Vec>, + pub content: Vec>, /// the position from where content drawing starts. /// recalculated when layouting is performed. content_pos: Vec2, } impl AdvancedLabel { - pub fn new(config: GuiElemCfg, align: Vec2, content: Vec>) -> Self { + pub fn new( + config: GuiElemCfg, + align: Vec2, + content: Vec>, + ) -> Self { Self { config, children: vec![], @@ -350,13 +387,29 @@ impl GuiElem for AdvancedLabel { let mut len = 0.0; let mut height = 0.0; for (c, scale, _) in line { - let size = info - .font - .layout_text(&c.text, 1.0, TextOptions::new()) - .size(); - len += size.x * scale; - if size.y * scale > height { - height = size.y * scale; + match c { + AdvancedContent::Text(c) => { + let size = info + .font + .layout_text(&c.text, 1.0, TextOptions::new()) + .size(); + len += size.x * scale; + if size.y * scale > height { + height = size.y * scale; + } + } + AdvancedContent::Image { source, handle } => {} + } + } + for (c, scale, _) in line { + match c { + AdvancedContent::Text(_) => {} + AdvancedContent::Image { source, handle } => { + if let Some(Some(handle)) = handle { + let size = handle.size().into_f32(); + len += height * size.x / size.y; + } + } } } if len > max_len { @@ -382,11 +435,67 @@ impl GuiElem for AdvancedLabel { }; for line in &mut self.content { for (c, s, _) in line { - c.formatted = Some(info.font.layout_text( - &c.text, - scale * (*s), - TextOptions::new(), - )); + match c { + AdvancedContent::Text(c) => { + c.formatted = Some(info.font.layout_text( + &c.text, + scale * (*s), + TextOptions::new(), + )); + } + AdvancedContent::Image { source, handle } => { + if handle.is_none() { + match source { + ImageSource::Cover(id) => { + if let Some(img) = info.covers.get_mut(&id) { + if let Some(img) = img.get_init(g) { + *handle = Some(Some(img)); + } else { + match img { + GuiServerImage::Loading(_) => {} + GuiServerImage::Loaded(_) => {} + GuiServerImage::Error => { + *handle = Some(None) + } + } + } + } else { + info.covers.insert( + *id, + GuiServerImage::new_cover( + *id, + Arc::clone(&info.get_con), + ), + ); + } + } + ImageSource::CustomFile(path) => { + if let Some(img) = info.custom_images.get_mut(path) { + if let Some(img) = img.get_init(g) { + *handle = Some(Some(img)); + } else { + match img { + GuiServerImage::Loading(_) => {} + GuiServerImage::Loaded(_) => {} + GuiServerImage::Error => { + *handle = Some(None) + } + } + } + } else { + info.custom_images.insert( + path.clone(), + GuiServerImage::new_custom_file( + path.clone(), + Arc::clone(&info.get_con), + ), + ); + } + } + } + } + } + } } } } @@ -395,20 +504,51 @@ impl GuiElem for AdvancedLabel { let mut pos_y = info.pos.top_left().y + self.content_pos.y; for line in &self.content { let mut pos_x = pos_x_start; - let height = line + let height_div_by = line .iter() - .filter_map(|v| v.0.formatted.as_ref()) + .map(|(_, scale, _)| *scale) + .reduce(f32::max) + .unwrap_or(1.0); + let line_height = line + .iter() + .filter_map(|(v, _, _)| { + if let AdvancedContent::Text(c) = v { + Some(c) + } else { + None + } + }) + .filter_map(|v| v.formatted.as_ref()) .map(|f| f.height()) .reduce(f32::max) .unwrap_or(0.0); - for (c, _, h) in line { - if let Some(f) = &c.formatted { - let y = pos_y + (height - f.height()) * h; - g.draw_text(Vec2::new(pos_x, y), c.color, f); - pos_x += f.width(); + for (c, scale, placement_height) in line { + // not super accurate, but pretty good + let rel_scale = f32::min(1.0, scale / height_div_by); + match c { + AdvancedContent::Text(c) => { + if let Some(f) = &c.formatted { + let y = pos_y + (line_height - f.height()) * placement_height; + g.draw_text(Vec2::new(pos_x, y), c.color, f); + pos_x += f.width(); + } + } + AdvancedContent::Image { source: _, handle } => { + if let Some(Some(handle)) = handle { + let size = handle.size().into_f32(); + let h = line_height * rel_scale; + let w = h * size.x / size.y; + let y = pos_y + (line_height - h) * placement_height; + g.draw_rectangle_image( + Rectangle::from_tuples((pos_x, y), (pos_x + w, y + h)), + handle, + ); + pos_x += w; + } + } } } - pos_y += height; + pos_y += line_height; } } } diff --git a/musicdb-client/src/textcfg.rs b/musicdb-client/src/textcfg.rs index 390d224..7ba15d5 100755 --- a/musicdb-client/src/textcfg.rs +++ b/musicdb-client/src/textcfg.rs @@ -3,10 +3,10 @@ use std::{ str::{Chars, FromStr}, }; -use musicdb_lib::data::{database::Database, song::Song, GeneralData}; +use musicdb_lib::data::{database::Database, song::Song, CoverId, GeneralData}; use speedy2d::color::Color; -use crate::gui_text::Content; +use crate::gui_text::{AdvancedContent, Content, ImageSource}; #[derive(Debug)] pub struct TextBuilder(pub Vec); @@ -34,9 +34,15 @@ pub enum TextPart { /// If `1` is something, uses `2`. /// If `1` is nothing, uses `3`. If(TextBuilder, TextBuilder, TextBuilder), + ImgCover(CoverId), + ImgCustom(TextBuilder), } impl TextBuilder { - pub fn gen(&self, db: &Database, current_song: Option<&Song>) -> Vec> { + pub fn gen( + &self, + db: &Database, + current_song: Option<&Song>, + ) -> Vec> { let mut out = vec![]; let mut line = vec![]; let mut c = Color::WHITE; @@ -58,15 +64,31 @@ impl TextBuilder { &self, db: &Database, current_song: Option<&Song>, - out: &mut Vec>, - line: &mut Vec<(Content, f32, f32)>, + out: &mut Vec>, + line: &mut Vec<(AdvancedContent, f32, f32)>, scale: &mut f32, align: &mut f32, color: &mut Color, ) { macro_rules! push { ($e:expr) => { - line.push((Content::new($e, *color), *scale, *align)) + line.push(( + AdvancedContent::Text(Content::new($e, *color)), + *scale, + *align, + )) + }; + } + macro_rules! push_img { + ($e:expr) => { + line.push(( + AdvancedContent::Image { + source: $e, + handle: None, + }, + *scale, + *align, + )) }; } fn all_general<'a>( @@ -168,6 +190,17 @@ impl TextBuilder { no.gen_to(db, current_song, out, line, scale, align, color); } } + TextPart::ImgCover(id) => { + push_img!(ImageSource::Cover(*id)); + } + TextPart::ImgCustom(path) => { + push_img!(ImageSource::CustomFile( + path.gen(db, current_song) + .into_iter() + .flat_map(|v| v.into_iter().map(|(v, _, _)| v.to_string())) + .collect() + )); + } } } } @@ -285,6 +318,41 @@ impl TextBuilder { } })); } + Some('i') => { + done!(); + let mut src = String::new(); + loop { + match chars.next() { + None => { + return Err(TextBuilderParseError::InvalidImageSourceName( + src, + )) + } + 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)) + } + }); + } Some(ch) => current.push(ch), }, '%' => { @@ -337,6 +405,8 @@ pub enum TextBuilderParseError { TooFewCharsForColor, ColorNotHex, CouldntParse(String, String), + InvalidImageSourceName(String), + InvalidImageCoverId(String), } impl Display for TextBuilderParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -351,6 +421,8 @@ impl Display for TextBuilderParseError { Self::TooFewCharsForColor => write!(f, "Too few chars for color: Syntax is \\cRRGGBB."), Self::ColorNotHex => write!(f, "Color value wasn't a hex number! Syntax is \\cRRGGBB, where R, G, and B are values from 0-9 and A-F (hex 0-F)."), Self::CouldntParse(v, t) => write!(f, "Couldn't parse value '{v}' to type '{t}'."), + Self::InvalidImageSourceName(name) => write!(f, "Invalid image source name: '{name}'."), + Self::InvalidImageCoverId(id) => write!(f, "Invalid image cover id: '{id}'."), } } }