add option to show images in AdvancedLabel, available through config via textcfg

This commit is contained in:
Mark 2023-12-27 16:57:46 +01:00
parent 8c434743f8
commit b1998e3316
6 changed files with 289 additions and 46 deletions

View File

@ -10,6 +10,10 @@ font = ''
# \h1.0;: set the height-alignment (to the default: 1.0 / align text to be on one baseline) # \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. # \cRRGGBB: set color to this hex value.
# \cFFFFFF: default color (white) # \cFFFFFF: default color (white)
# \iCover:<id>;: show cover with this ID (ID >= 0)
# \iCover:0;
# \iCustomFile:<path>:: show cover stored in custom files at the given path (path terminated by #, is another textcfg)
# \iCustomFile:my_image.jpg#
# \<char>: <char> (\\ => \, \# => #, \% => %, ...) # \<char>: <char> (\\ => \, \# => #, \% => %, ...)
# custom properties: # custom properties:
# %<mode><search text>% # %<mode><search text>%
@ -31,8 +35,19 @@ font = ''
status_bar = '''\t status_bar = '''\t
\s0.5;?\A#\c505050by \c593D6E\A##?\a#?\A# ##\c505050on \c264524\a##\c808080?%>Year=%# (%>Year=%)## | \d''' \s0.5;?\A#\c505050by \c593D6E\A##?\a#?\A# ##\c505050on \c264524\a##\c808080?%>Year=%# (%>Year=%)## | \d'''
idle_top = '''\t \s0.5;\c505050(\d?%>Genre=%#, %>Genre=%##) # Two lines
?\A#\c505050by \c593D6E\A##?\a#?\A# ##\c505050on \c264524\a##\c808080?%>Year=%# (%>Year=%)##''' # 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 <Artist>" (purple)
# - if there is a "Flag: <name>" tag, show the image saved as "<name>.png"
# - "on <Album>", if there is an Album (green)
# - "(<year>)", if there is a Year=<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_side1 = ''
idle_side2 = '' idle_side2 = ''

View File

@ -1,7 +1,4 @@
use std::{ use std::time::Instant;
sync::{atomic::AtomicU8, Arc},
time::Instant,
};
use musicdb_lib::data::{song::Song, ArtistId}; use musicdb_lib::data::{song::Song, ArtistId};
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle}; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle};

View File

@ -1004,12 +1004,18 @@ impl ListAlbum {
Vec2::new(0.0, 0.5), Vec2::new(0.0, 0.5),
vec![vec![ 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,
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, 0.5,
1.0, 1.0,
), ),
@ -1142,11 +1148,18 @@ impl ListSong {
Vec2::new(0.0, 0.5), Vec2::new(0.0, 0.5),
vec![vec![ 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,
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; config.redraw = true;

View File

@ -181,12 +181,18 @@ impl GuiElem for QueueViewer {
let dr = fmt_dur(info.database.queue.duration_remaining(&info.database)); let dr = fmt_dur(info.database.queue.duration_remaining(&info.database));
label.content = vec![ label.content = vec![
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,
1.0, 1.0,
)], )],
vec![( 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,
1.0, 1.0,
)], )],
@ -454,19 +460,19 @@ impl QueueSong {
Vec2::new(0.0, 0.5), Vec2::new(0.0, 0.5),
vec![vec![ vec![vec![
( (
gui_text::Content::new( gui_text::AdvancedContent::Text(gui_text::Content::new(
song.title.clone(), song.title.clone(),
if current { if current {
Color::from_int_rgb(194, 76, 178) Color::from_int_rgb(194, 76, 178)
} else { } else {
Color::from_int_rgb(120, 76, 194) Color::from_int_rgb(120, 76, 194)
}, },
), )),
1.0, 1.0,
1.0, 1.0,
), ),
( (
gui_text::Content::new( gui_text::AdvancedContent::Text(gui_text::Content::new(
{ {
let duration = song.duration_millis / 1000; let duration = song.duration_millis / 1000;
format!(" {}:{:0>2}", duration / 60, duration % 60) format!(" {}:{:0>2}", duration / 60, duration % 60)
@ -476,7 +482,7 @@ impl QueueSong {
} else { } else {
Color::DARK_GRAY Color::DARK_GRAY
}, },
), )),
0.6, 0.6,
1.0, 1.0,
), ),

View File

@ -1,14 +1,16 @@
use std::rc::Rc; use std::{fmt::Display, rc::Rc, sync::Arc};
use musicdb_lib::data::CoverId;
use speedy2d::{ use speedy2d::{
color::Color, color::Color,
dimen::Vec2, dimen::Vec2,
font::{FormattedTextBlock, TextLayout, TextOptions}, font::{FormattedTextBlock, TextLayout, TextOptions},
image::ImageHandle,
shape::Rectangle, shape::Rectangle,
window::{ModifiersState, MouseButton}, 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<Color>, background: Option<Color>,
formatted: Option<Rc<FormattedTextBlock>>, formatted: Option<Rc<FormattedTextBlock>>,
} }
#[allow(unused)] #[allow(unused)]
impl Content { impl Content {
pub fn new(text: String, color: Color) -> Self { 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<Option<ImageHandle>>,
},
}
#[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`. /// More advanced version of `Label`.
/// Allows stringing together multiple `Content`s in one line. /// Allows stringing together multiple `Content`s in one line.
pub struct AdvancedLabel { pub struct AdvancedLabel {
@ -297,13 +330,17 @@ pub struct AdvancedLabel {
pub align: Vec2, pub align: Vec2,
/// (Content, Size-Scale, Height) /// (Content, Size-Scale, Height)
/// Size-Scale and Height should default to 1.0. /// Size-Scale and Height should default to 1.0.
pub content: Vec<Vec<(Content, f32, f32)>>, pub content: Vec<Vec<(AdvancedContent, f32, f32)>>,
/// the position from where content drawing starts. /// the position from where content drawing starts.
/// recalculated when layouting is performed. /// recalculated when layouting is performed.
content_pos: Vec2, content_pos: Vec2,
} }
impl AdvancedLabel { impl AdvancedLabel {
pub fn new(config: GuiElemCfg, align: Vec2, content: Vec<Vec<(Content, f32, f32)>>) -> Self { pub fn new(
config: GuiElemCfg,
align: Vec2,
content: Vec<Vec<(AdvancedContent, f32, f32)>>,
) -> Self {
Self { Self {
config, config,
children: vec![], children: vec![],
@ -350,6 +387,8 @@ impl GuiElem for AdvancedLabel {
let mut len = 0.0; let mut len = 0.0;
let mut height = 0.0; let mut height = 0.0;
for (c, scale, _) in line { for (c, scale, _) in line {
match c {
AdvancedContent::Text(c) => {
let size = info let size = info
.font .font
.layout_text(&c.text, 1.0, TextOptions::new()) .layout_text(&c.text, 1.0, TextOptions::new())
@ -359,6 +398,20 @@ impl GuiElem for AdvancedLabel {
height = size.y * scale; 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 { if len > max_len {
max_len = len; max_len = len;
} }
@ -382,12 +435,68 @@ impl GuiElem for AdvancedLabel {
}; };
for line in &mut self.content { for line in &mut self.content {
for (c, s, _) in line { for (c, s, _) in line {
match c {
AdvancedContent::Text(c) => {
c.formatted = Some(info.font.layout_text( c.formatted = Some(info.font.layout_text(
&c.text, &c.text,
scale * (*s), scale * (*s),
TextOptions::new(), 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; let mut pos_y = info.pos.top_left().y + self.content_pos.y;
for line in &self.content { for line in &self.content {
let mut pos_x = pos_x_start; let mut pos_x = pos_x_start;
let height = line let height_div_by = line
.iter() .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()) .map(|f| f.height())
.reduce(f32::max) .reduce(f32::max)
.unwrap_or(0.0); .unwrap_or(0.0);
for (c, _, h) in line { 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 { if let Some(f) = &c.formatted {
let y = pos_y + (height - f.height()) * h; let y = pos_y + (line_height - f.height()) * placement_height;
g.draw_text(Vec2::new(pos_x, y), c.color, f); g.draw_text(Vec2::new(pos_x, y), c.color, f);
pos_x += f.width(); pos_x += f.width();
} }
} }
pos_y += height; 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 += line_height;
} }
} }
} }

View File

@ -3,10 +3,10 @@ use std::{
str::{Chars, FromStr}, 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 speedy2d::color::Color;
use crate::gui_text::Content; use crate::gui_text::{AdvancedContent, Content, ImageSource};
#[derive(Debug)] #[derive(Debug)]
pub struct TextBuilder(pub Vec<TextPart>); pub struct TextBuilder(pub Vec<TextPart>);
@ -34,9 +34,15 @@ pub enum TextPart {
/// If `1` is something, uses `2`. /// If `1` is something, uses `2`.
/// If `1` is nothing, uses `3`. /// If `1` is nothing, uses `3`.
If(TextBuilder, TextBuilder, TextBuilder), If(TextBuilder, TextBuilder, TextBuilder),
ImgCover(CoverId),
ImgCustom(TextBuilder),
} }
impl TextBuilder { impl TextBuilder {
pub fn gen(&self, db: &Database, current_song: Option<&Song>) -> Vec<Vec<(Content, f32, f32)>> { pub fn gen(
&self,
db: &Database,
current_song: Option<&Song>,
) -> Vec<Vec<(AdvancedContent, f32, f32)>> {
let mut out = vec![]; let mut out = vec![];
let mut line = vec![]; let mut line = vec![];
let mut c = Color::WHITE; let mut c = Color::WHITE;
@ -58,15 +64,31 @@ impl TextBuilder {
&self, &self,
db: &Database, db: &Database,
current_song: Option<&Song>, current_song: Option<&Song>,
out: &mut Vec<Vec<(Content, f32, f32)>>, out: &mut Vec<Vec<(AdvancedContent, f32, f32)>>,
line: &mut Vec<(Content, f32, f32)>, line: &mut Vec<(AdvancedContent, f32, f32)>,
scale: &mut f32, scale: &mut f32,
align: &mut f32, align: &mut f32,
color: &mut Color, color: &mut Color,
) { ) {
macro_rules! push { macro_rules! push {
($e:expr) => { ($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>( fn all_general<'a>(
@ -168,6 +190,17 @@ impl TextBuilder {
no.gen_to(db, current_song, out, line, scale, align, color); 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), Some(ch) => current.push(ch),
}, },
'%' => { '%' => {
@ -337,6 +405,8 @@ pub enum TextBuilderParseError {
TooFewCharsForColor, TooFewCharsForColor,
ColorNotHex, ColorNotHex,
CouldntParse(String, String), CouldntParse(String, String),
InvalidImageSourceName(String),
InvalidImageCoverId(String),
} }
impl Display for TextBuilderParseError { impl Display for TextBuilderParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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::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::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::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}'."),
} }
} }
} }