mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-03-10 14:13:53 +01:00
tags can now be used
filldb: Year and Genre will be read from MP3/ID3 tags and added as Tags. client: Tags can (and will) be displayed in the status bar. client: StatusBar text can be configured via ~/.config/musicdb-client/config_gui.toml client: Added a proper default config file at src/config_gui.toml (note: this is in src/ so that include_bytes! can use a path without /, so it will compile on windows) client: users will need to add the new `[text]` section to their gui_config.toml!
This commit is contained in:
parent
c6b75180bb
commit
f429f17876
32
musicdb-client/src/config_gui.toml
Normal file
32
musicdb-client/src/config_gui.toml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
font = ''
|
||||||
|
|
||||||
|
[text]
|
||||||
|
# define the text displayed in the application.
|
||||||
|
# escape sequences:
|
||||||
|
# \t: song title
|
||||||
|
# \a: album name
|
||||||
|
# \A: artist name
|
||||||
|
# \s1.0;: set the scale (to the default: 1.0)
|
||||||
|
# \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)
|
||||||
|
# \<char>: <char> (\\ => \, \# => #, \% => %, ...)
|
||||||
|
# custom properties:
|
||||||
|
# %<mode><search text>%
|
||||||
|
# %_word% returns the first property that includes "word"
|
||||||
|
# %>Year=% returns the end of the first property that starts with "Year=",
|
||||||
|
# so if a song has "Year=2019", this would return "2019".
|
||||||
|
# %=Value% returns something if a property "Value" is found.
|
||||||
|
# IF:
|
||||||
|
# ?<condition>#<then>#<else>#
|
||||||
|
# If <condition> is not empty, the entire block will be replaced by the value generated by <then>.
|
||||||
|
# If <condition> is empty, the entire block will be replaced by the value generated by <else>.
|
||||||
|
# Examples:
|
||||||
|
# ?\A#by \A##
|
||||||
|
# If we know the artist's name, write "by " followed by the name,
|
||||||
|
# if not, don't write anything (## -> <else> is empty)
|
||||||
|
# ?\t#\t#(no title found)#
|
||||||
|
# If we know the title, write it. If not, write "(no title found)" instead.
|
||||||
|
|
||||||
|
status_bar = '''\t
|
||||||
|
\s0.5;?\A#\c505050by \c593D6E\A##?\a#?\A# ##\c505050on \c264524\a##?%>Year=%#\c808080 (%>Year=%)##'''
|
@ -28,7 +28,7 @@ use speedy2d::{
|
|||||||
Graphics2D,
|
Graphics2D,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{gui_screen::GuiScreen, gui_wrappers::WithFocusHotkey};
|
use crate::{gui_screen::GuiScreen, gui_wrappers::WithFocusHotkey, textcfg};
|
||||||
|
|
||||||
pub enum GuiEvent {
|
pub enum GuiEvent {
|
||||||
Refresh,
|
Refresh,
|
||||||
@ -50,6 +50,7 @@ pub fn main(
|
|||||||
let mut scroll_pixels_multiplier = 1.0;
|
let mut scroll_pixels_multiplier = 1.0;
|
||||||
let mut scroll_lines_multiplier = 3.0;
|
let mut scroll_lines_multiplier = 3.0;
|
||||||
let mut scroll_pages_multiplier = 0.75;
|
let mut scroll_pages_multiplier = 0.75;
|
||||||
|
let status_bar_text;
|
||||||
match std::fs::read_to_string(&config_file) {
|
match std::fs::read_to_string(&config_file) {
|
||||||
Ok(cfg) => {
|
Ok(cfg) => {
|
||||||
if let Ok(table) = cfg.parse::<toml::Table>() {
|
if let Ok(table) = cfg.parse::<toml::Table>() {
|
||||||
@ -85,8 +86,26 @@ pub fn main(
|
|||||||
{
|
{
|
||||||
scroll_pages_multiplier = v;
|
scroll_pages_multiplier = v;
|
||||||
}
|
}
|
||||||
|
if let Some(t) = table.get("text").and_then(|v| v.as_table()) {
|
||||||
|
if let Some(v) = t.get("status_bar").and_then(|v| v.as_str()) {
|
||||||
|
match v.parse() {
|
||||||
|
Ok(v) => status_bar_text = v,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[toml] `text.status_bar couldn't be parsed: {e}`");
|
||||||
|
std::process::exit(30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("[toml] missing the required `text.status_bar` string value.");
|
||||||
|
std::process::exit(30);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("[toml] missing the required `[text]` section!");
|
||||||
|
std::process::exit(30);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Couldn't parse config file {config_file:?} as toml!");
|
eprintln!("Couldn't parse config file {config_file:?} as toml!");
|
||||||
|
std::process::exit(30);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -94,7 +113,9 @@ pub fn main(
|
|||||||
if let Some(p) = config_file.parent() {
|
if let Some(p) = config_file.parent() {
|
||||||
_ = std::fs::create_dir_all(p);
|
_ = std::fs::create_dir_all(p);
|
||||||
}
|
}
|
||||||
_ = std::fs::write(&config_file, "font = ''");
|
if std::fs::write(&config_file, include_bytes!("config_gui.toml")).is_ok() {
|
||||||
|
eprintln!("[info] created a default config file.");
|
||||||
|
}
|
||||||
std::process::exit(25);
|
std::process::exit(25);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,9 +147,14 @@ pub fn main(
|
|||||||
scroll_pixels_multiplier,
|
scroll_pixels_multiplier,
|
||||||
scroll_lines_multiplier,
|
scroll_lines_multiplier,
|
||||||
scroll_pages_multiplier,
|
scroll_pages_multiplier,
|
||||||
|
GuiConfig { status_bar_text },
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct GuiConfig {
|
||||||
|
pub status_bar_text: textcfg::TextBuilder,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Gui {
|
pub struct Gui {
|
||||||
pub event_sender: UserEventSender<GuiEvent>,
|
pub event_sender: UserEventSender<GuiEvent>,
|
||||||
pub database: Arc<Mutex<Database>>,
|
pub database: Arc<Mutex<Database>>,
|
||||||
@ -150,6 +176,7 @@ pub struct Gui {
|
|||||||
pub scroll_pixels_multiplier: f64,
|
pub scroll_pixels_multiplier: f64,
|
||||||
pub scroll_lines_multiplier: f64,
|
pub scroll_lines_multiplier: f64,
|
||||||
pub scroll_pages_multiplier: f64,
|
pub scroll_pages_multiplier: f64,
|
||||||
|
pub gui_config: Option<GuiConfig>,
|
||||||
}
|
}
|
||||||
impl Gui {
|
impl Gui {
|
||||||
fn new(
|
fn new(
|
||||||
@ -163,6 +190,7 @@ impl Gui {
|
|||||||
scroll_pixels_multiplier: f64,
|
scroll_pixels_multiplier: f64,
|
||||||
scroll_lines_multiplier: f64,
|
scroll_lines_multiplier: f64,
|
||||||
scroll_pages_multiplier: f64,
|
scroll_pages_multiplier: f64,
|
||||||
|
gui_config: GuiConfig,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
database.lock().unwrap().update_endpoints.push(
|
database.lock().unwrap().update_endpoints.push(
|
||||||
musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd {
|
musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd {
|
||||||
@ -227,6 +255,7 @@ impl Gui {
|
|||||||
scroll_pixels_multiplier,
|
scroll_pixels_multiplier,
|
||||||
scroll_lines_multiplier,
|
scroll_lines_multiplier,
|
||||||
scroll_pages_multiplier,
|
scroll_pages_multiplier,
|
||||||
|
gui_config: Some(gui_config),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -430,6 +459,7 @@ pub struct DrawInfo<'a> {
|
|||||||
Dragging,
|
Dragging,
|
||||||
Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>,
|
Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>,
|
||||||
)>,
|
)>,
|
||||||
|
pub gui_config: &'a GuiConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generic wrapper over anything that implements GuiElemTrait
|
/// Generic wrapper over anything that implements GuiElemTrait
|
||||||
@ -817,6 +847,7 @@ impl WindowHandler<GuiEvent> for Gui {
|
|||||||
);
|
);
|
||||||
let mut dblock = self.database.lock().unwrap();
|
let mut dblock = self.database.lock().unwrap();
|
||||||
let mut covers = self.covers.take().unwrap();
|
let mut covers = self.covers.take().unwrap();
|
||||||
|
let cfg = self.gui_config.take().unwrap();
|
||||||
let mut info = DrawInfo {
|
let mut info = DrawInfo {
|
||||||
actions: Vec::with_capacity(0),
|
actions: Vec::with_capacity(0),
|
||||||
pos: Rectangle::new(Vec2::ZERO, self.size.into_f32()),
|
pos: Rectangle::new(Vec2::ZERO, self.size.into_f32()),
|
||||||
@ -830,6 +861,7 @@ impl WindowHandler<GuiEvent> for Gui {
|
|||||||
child_has_keyboard_focus: true,
|
child_has_keyboard_focus: true,
|
||||||
line_height: self.line_height,
|
line_height: self.line_height,
|
||||||
dragging: self.dragging.take(),
|
dragging: self.dragging.take(),
|
||||||
|
gui_config: &cfg,
|
||||||
};
|
};
|
||||||
self.gui.draw(&mut info, graphics);
|
self.gui.draw(&mut info, graphics);
|
||||||
let actions = std::mem::replace(&mut info.actions, Vec::with_capacity(0));
|
let actions = std::mem::replace(&mut info.actions, Vec::with_capacity(0));
|
||||||
@ -864,6 +896,7 @@ impl WindowHandler<GuiEvent> for Gui {
|
|||||||
}
|
}
|
||||||
// cleanup
|
// cleanup
|
||||||
drop(info);
|
drop(info);
|
||||||
|
self.gui_config = Some(cfg);
|
||||||
self.covers = Some(covers);
|
self.covers = Some(covers);
|
||||||
drop(dblock);
|
drop(dblock);
|
||||||
for a in actions {
|
for a in actions {
|
||||||
|
@ -31,20 +31,11 @@ impl CurrentSong {
|
|||||||
pub fn new(config: GuiElemCfg) -> Self {
|
pub fn new(config: GuiElemCfg) -> Self {
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
children: vec![
|
children: vec![GuiElem::new(AdvancedLabel::new(
|
||||||
GuiElem::new(Label::new(
|
GuiElemCfg::at(Rectangle::from_tuples((0.4, 0.0), (1.0, 1.0))),
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.4, 0.0), (1.0, 0.5))),
|
Vec2::new(0.0, 0.5),
|
||||||
"".to_owned(),
|
|
||||||
Color::from_int_rgb(180, 180, 210),
|
|
||||||
None,
|
|
||||||
Vec2::new(0.0, 1.0),
|
|
||||||
)),
|
|
||||||
GuiElem::new(AdvancedLabel::new(
|
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.4, 0.5), (1.0, 1.0))),
|
|
||||||
Vec2::new(0.0, 0.0),
|
|
||||||
vec![],
|
vec![],
|
||||||
)),
|
))],
|
||||||
],
|
|
||||||
cover_pos: Rectangle::new(Vec2::ZERO, Vec2::ZERO),
|
cover_pos: Rectangle::new(Vec2::ZERO, Vec2::ZERO),
|
||||||
covers: VecDeque::new(),
|
covers: VecDeque::new(),
|
||||||
prev_song: None,
|
prev_song: None,
|
||||||
@ -140,139 +131,34 @@ impl GuiElemTrait for CurrentSong {
|
|||||||
// redraw
|
// redraw
|
||||||
if self.config.redraw {
|
if self.config.redraw {
|
||||||
self.config.redraw = false;
|
self.config.redraw = false;
|
||||||
let (name, subtext) = if let Some(song) = new_song {
|
if let Some(song) = new_song {
|
||||||
if let Some(song) = info.database.get_song(&song) {
|
let status_bar_text = info
|
||||||
let sub = match (
|
.gui_config
|
||||||
info.database.artists().get(&song.artist),
|
.status_bar_text
|
||||||
song.album
|
.gen(&info.database, info.database.get_song(&song));
|
||||||
.as_ref()
|
self.children[0]
|
||||||
.and_then(|id| info.database.albums().get(id)),
|
|
||||||
) {
|
|
||||||
(None, None) => vec![],
|
|
||||||
(Some(artist), None) => vec![
|
|
||||||
(
|
|
||||||
Content::new("by ".to_owned(), Self::color_by(0.0)),
|
|
||||||
1.0,
|
|
||||||
1.0,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Content::new(artist.name.to_owned(), Self::color_artist(0.0)),
|
|
||||||
1.0,
|
|
||||||
1.0,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
(None, Some(album)) => vec![
|
|
||||||
(Content::new(String::new(), Color::TRANSPARENT), 0.0, 1.0),
|
|
||||||
(
|
|
||||||
Content::new("on ".to_owned(), Self::color_on(0.0)),
|
|
||||||
1.0,
|
|
||||||
1.0,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Content::new(album.name.to_owned(), Self::color_album(0.0)),
|
|
||||||
1.0,
|
|
||||||
1.0,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
(Some(artist), Some(album)) => vec![
|
|
||||||
(
|
|
||||||
Content::new("by ".to_owned(), Self::color_by(0.0)),
|
|
||||||
1.0,
|
|
||||||
1.0,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Content::new(
|
|
||||||
format!("{} ", artist.name),
|
|
||||||
Self::color_artist(0.0),
|
|
||||||
),
|
|
||||||
1.0,
|
|
||||||
1.0,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Content::new("on ".to_owned(), Self::color_on(0.0)),
|
|
||||||
1.0,
|
|
||||||
1.0,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Content::new(album.name.to_owned(), Self::color_album(0.0)),
|
|
||||||
1.0,
|
|
||||||
1.0,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
(song.title.clone(), sub)
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
"< song not in db >".to_owned(),
|
|
||||||
vec![(
|
|
||||||
Content::new(
|
|
||||||
"you may need to restart the client to resync the database"
|
|
||||||
.to_owned(),
|
|
||||||
Color::from_rgb(0.8, 0.5, 0.5),
|
|
||||||
),
|
|
||||||
1.0,
|
|
||||||
1.0,
|
|
||||||
)],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(String::new(), vec![])
|
|
||||||
};
|
|
||||||
*self.children[0]
|
|
||||||
.try_as_mut::<Label>()
|
|
||||||
.unwrap()
|
|
||||||
.content
|
|
||||||
.text() = name;
|
|
||||||
self.children[1]
|
|
||||||
.try_as_mut::<AdvancedLabel>()
|
.try_as_mut::<AdvancedLabel>()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.content = subtext;
|
.content = status_bar_text;
|
||||||
self.text_updated = Some(Instant::now());
|
self.text_updated = Some(Instant::now());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if let Some(updated) = &self.text_updated {
|
if let Some(updated) = &self.text_updated {
|
||||||
if let Some(h) = &info.helper {
|
if let Some(h) = &info.helper {
|
||||||
h.request_redraw();
|
h.request_redraw();
|
||||||
}
|
}
|
||||||
let prog = updated.elapsed().as_secs_f32();
|
let mut prog = updated.elapsed().as_secs_f32();
|
||||||
*self.children[0]
|
if prog >= 1.0 {
|
||||||
.try_as_mut::<Label>()
|
prog = 1.0;
|
||||||
|
self.text_updated = None;
|
||||||
|
}
|
||||||
|
self.children[0]
|
||||||
|
.try_as_mut::<AdvancedLabel>()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.content
|
.content
|
||||||
.color() = Self::color_title((prog / 1.5).min(1.0));
|
.iter_mut()
|
||||||
let subtext = self.children[1].try_as_mut::<AdvancedLabel>().unwrap();
|
.count();
|
||||||
match subtext.content.len() {
|
|
||||||
2 => {
|
|
||||||
*subtext.content[0].0.color() = Self::color_by(prog.min(1.0));
|
|
||||||
*subtext.content[1].0.color() =
|
|
||||||
Self::color_artist((prog.max(0.5) - 0.5).min(1.0));
|
|
||||||
if prog >= 1.5 {
|
|
||||||
self.text_updated = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3 => {
|
|
||||||
*subtext.content[0].0.color() = Self::color_on(prog.min(1.0));
|
|
||||||
*subtext.content[1].0.color() =
|
|
||||||
Self::color_album((prog.max(0.5) - 0.5).min(1.0));
|
|
||||||
if prog >= 1.5 {
|
|
||||||
self.text_updated = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4 => {
|
|
||||||
*subtext.content[0].0.color() = Self::color_by(prog.min(1.0));
|
|
||||||
*subtext.content[1].0.color() =
|
|
||||||
Self::color_artist((prog.max(0.5) - 0.5).min(1.0));
|
|
||||||
*subtext.content[2].0.color() = Self::color_on((prog.max(1.0) - 1.0).min(1.0));
|
|
||||||
*subtext.content[3].0.color() =
|
|
||||||
Self::color_album((prog.max(1.5) - 1.5).min(1.0));
|
|
||||||
if prog >= 2.5 {
|
|
||||||
self.text_updated = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.text_updated = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// drawing stuff
|
// drawing stuff
|
||||||
if self.config.pixel_pos.size() != info.pos.size() {
|
if self.config.pixel_pos.size() != info.pos.size() {
|
||||||
|
@ -268,21 +268,19 @@ 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<(Content, f32, f32)>,
|
pub content: Vec<Vec<(Content, 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,
|
||||||
content_height: f32,
|
|
||||||
}
|
}
|
||||||
impl AdvancedLabel {
|
impl AdvancedLabel {
|
||||||
pub fn new(config: GuiElemCfg, align: Vec2, content: Vec<(Content, f32, f32)>) -> Self {
|
pub fn new(config: GuiElemCfg, align: Vec2, content: Vec<Vec<(Content, f32, f32)>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
children: vec![],
|
children: vec![],
|
||||||
align,
|
align,
|
||||||
content,
|
content,
|
||||||
content_pos: Vec2::ZERO,
|
content_pos: Vec2::ZERO,
|
||||||
content_height: 0.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -308,13 +306,19 @@ impl GuiElemTrait for AdvancedLabel {
|
|||||||
fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
|
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.config.pixel_pos.size() != info.pos.size()
|
||||||
|| self.content.iter().any(|(c, _, _)| c.will_redraw())
|
|| self
|
||||||
|
.content
|
||||||
|
.iter()
|
||||||
|
.any(|v| v.iter().any(|(c, _, _)| c.will_redraw()))
|
||||||
{
|
{
|
||||||
self.config.redraw = false;
|
self.config.redraw = false;
|
||||||
|
let mut max_len = 0.0;
|
||||||
|
let mut total_height = 0.0;
|
||||||
|
for line in &self.content {
|
||||||
let mut len = 0.0;
|
let mut len = 0.0;
|
||||||
let mut height = 0.0;
|
let mut height = 0.0;
|
||||||
for (c, scale, _) in &self.content {
|
for (c, scale, _) in line {
|
||||||
let mut size = info
|
let size = info
|
||||||
.font
|
.font
|
||||||
.layout_text(&c.text, 1.0, TextOptions::new())
|
.layout_text(&c.text, 1.0, TextOptions::new())
|
||||||
.size();
|
.size();
|
||||||
@ -323,24 +327,29 @@ impl GuiElemTrait for AdvancedLabel {
|
|||||||
height = size.y * scale;
|
height = size.y * scale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len > 0.0 && height > 0.0 {
|
if len > max_len {
|
||||||
let scale1 = info.pos.width() / len;
|
max_len = len;
|
||||||
let scale2 = info.pos.height() / height;
|
}
|
||||||
|
total_height += height;
|
||||||
|
}
|
||||||
|
if max_len > 0.0 && total_height > 0.0 {
|
||||||
|
let scale1 = info.pos.width() / max_len;
|
||||||
|
let scale2 = info.pos.height() / total_height;
|
||||||
let scale;
|
let scale;
|
||||||
self.content_pos = if scale1 < scale2 {
|
self.content_pos = if scale1 < scale2 {
|
||||||
// use all available width
|
// use all available width
|
||||||
scale = scale1;
|
scale = scale1;
|
||||||
self.content_height = height * scale;
|
Vec2::new(
|
||||||
let pad = info.pos.height() - self.content_height;
|
0.0,
|
||||||
Vec2::new(0.0, pad * self.align.y)
|
(info.pos.height() - (total_height * scale)) * self.align.y,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// use all available height
|
// use all available height
|
||||||
scale = scale2;
|
scale = scale2;
|
||||||
self.content_height = info.pos.height();
|
Vec2::new((info.pos.width() - (max_len * scale)) * self.align.x, 0.0)
|
||||||
let pad = info.pos.width() - len * scale;
|
|
||||||
Vec2::new(pad * self.align.x, 0.0)
|
|
||||||
};
|
};
|
||||||
for (c, s, _) in &mut self.content {
|
for line in &mut self.content {
|
||||||
|
for (c, s, _) in line {
|
||||||
c.formatted = Some(info.font.layout_text(
|
c.formatted = Some(info.font.layout_text(
|
||||||
&c.text,
|
&c.text,
|
||||||
scale * (*s),
|
scale * (*s),
|
||||||
@ -349,14 +358,25 @@ impl GuiElemTrait for AdvancedLabel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let pos_y = info.pos.top_left().y + self.content_pos.y;
|
}
|
||||||
let mut pos_x = info.pos.top_left().x + self.content_pos.x;
|
let pos_x_start = info.pos.top_left().x + self.content_pos.x;
|
||||||
for (c, _, h) in &self.content {
|
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
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.0.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 {
|
if let Some(f) = &c.formatted {
|
||||||
let y = pos_y + (self.content_height - f.height()) * h;
|
let y = pos_y + (height - f.height()) * h;
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,7 @@ mod gui_settings;
|
|||||||
mod gui_text;
|
mod gui_text;
|
||||||
#[cfg(feature = "speedy2d")]
|
#[cfg(feature = "speedy2d")]
|
||||||
mod gui_wrappers;
|
mod gui_wrappers;
|
||||||
|
mod textcfg;
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum Mode {
|
enum Mode {
|
||||||
|
326
musicdb-client/src/textcfg.rs
Normal file
326
musicdb-client/src/textcfg.rs
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
use std::{
|
||||||
|
fmt::Display,
|
||||||
|
iter::Peekable,
|
||||||
|
str::{Chars, FromStr},
|
||||||
|
};
|
||||||
|
|
||||||
|
use musicdb_lib::data::{database::Database, song::Song, GeneralData, SongId};
|
||||||
|
use speedy2d::color::Color;
|
||||||
|
|
||||||
|
use crate::gui_text::Content;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TextBuilder(pub Vec<TextPart>);
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TextPart {
|
||||||
|
LineBreak,
|
||||||
|
SetColor(Color),
|
||||||
|
SetScale(f32),
|
||||||
|
SetHeightAlign(f32),
|
||||||
|
// - - - - -
|
||||||
|
Literal(String),
|
||||||
|
SongTitle,
|
||||||
|
AlbumName,
|
||||||
|
ArtistName,
|
||||||
|
/// Searches for a tag with exactly the provided value.
|
||||||
|
/// Returns nothing or one of the following characters:
|
||||||
|
/// `s` for Song, `a` for Album, and `A` for Artist.
|
||||||
|
TagEq(String),
|
||||||
|
/// Searches for a tag which starts with the provided string, then returns the end of it.
|
||||||
|
/// If the search string is the entire tag, returns an empty string (which is not `nothing` because it is a TextPart::Literal, so it counts as `something` in an `if`).
|
||||||
|
TagEnd(String),
|
||||||
|
/// Searches for a tag which contains the provided string, then returns that tag's value.
|
||||||
|
TagContains(String),
|
||||||
|
/// If `1` is something, uses `2`.
|
||||||
|
/// If `1` is nothing, uses `3`.
|
||||||
|
If(TextBuilder, TextBuilder, TextBuilder),
|
||||||
|
}
|
||||||
|
impl TextBuilder {
|
||||||
|
pub fn gen(&self, db: &Database, current_song: Option<&Song>) -> Vec<Vec<(Content, f32, f32)>> {
|
||||||
|
let mut out = vec![];
|
||||||
|
let mut line = vec![];
|
||||||
|
self.gen_to(db, current_song, &mut out, &mut line, &mut 1.0, &mut 1.0);
|
||||||
|
if !line.is_empty() {
|
||||||
|
out.push(line)
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
pub fn gen_to(
|
||||||
|
&self,
|
||||||
|
db: &Database,
|
||||||
|
current_song: Option<&Song>,
|
||||||
|
out: &mut Vec<Vec<(Content, f32, f32)>>,
|
||||||
|
line: &mut Vec<(Content, f32, f32)>,
|
||||||
|
scale: &mut f32,
|
||||||
|
align: &mut f32,
|
||||||
|
) {
|
||||||
|
let mut color = Color::WHITE;
|
||||||
|
macro_rules! push {
|
||||||
|
($e:expr) => {
|
||||||
|
line.push((Content::new($e, color), *scale, *align))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fn all_general<'a>(
|
||||||
|
db: &'a Database,
|
||||||
|
current_song: &'a Option<&'a Song>,
|
||||||
|
) -> [Option<&'a GeneralData>; 3] {
|
||||||
|
if let Some(s) = current_song {
|
||||||
|
if let Some(al) = s.album.and_then(|id| db.albums().get(&id)) {
|
||||||
|
if let Some(a) = db.artists().get(&s.artist) {
|
||||||
|
[Some(&s.general), Some(&al.general), Some(&a.general)]
|
||||||
|
} else {
|
||||||
|
[Some(&s.general), Some(&al.general), None]
|
||||||
|
}
|
||||||
|
} else if let Some(a) = db.artists().get(&s.artist) {
|
||||||
|
[Some(&s.general), None, Some(&a.general)]
|
||||||
|
} else {
|
||||||
|
[Some(&s.general), None, None]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
[None, None, None]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for part in &self.0 {
|
||||||
|
match part {
|
||||||
|
TextPart::LineBreak => out.push(std::mem::replace(line, vec![])),
|
||||||
|
TextPart::SetColor(c) => color = *c,
|
||||||
|
TextPart::SetScale(v) => *scale = *v,
|
||||||
|
TextPart::SetHeightAlign(v) => *align = *v,
|
||||||
|
TextPart::Literal(s) => push!(s.to_owned()),
|
||||||
|
TextPart::SongTitle => {
|
||||||
|
if let Some(s) = current_song {
|
||||||
|
push!(s.title.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextPart::ArtistName => {
|
||||||
|
if let Some(s) = current_song {
|
||||||
|
if let Some(artist) = db.artists().get(&s.artist) {
|
||||||
|
push!(artist.name.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextPart::TagEq(p) => {
|
||||||
|
for (i, gen) in all_general(db, ¤t_song).into_iter().enumerate() {
|
||||||
|
if let Some(_) = gen.and_then(|gen| gen.tags.iter().find(|t| *t == p)) {
|
||||||
|
push!(match i {
|
||||||
|
0 => 's',
|
||||||
|
1 => 'a',
|
||||||
|
2 => 'A',
|
||||||
|
_ => unreachable!("array length should be 3"),
|
||||||
|
}
|
||||||
|
.to_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextPart::TagEnd(p) => {
|
||||||
|
for gen in all_general(db, ¤t_song) {
|
||||||
|
if let Some(t) =
|
||||||
|
gen.and_then(|gen| gen.tags.iter().find(|t| t.starts_with(p)))
|
||||||
|
{
|
||||||
|
push!(t[p.len()..].to_owned());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextPart::TagContains(p) => {
|
||||||
|
for gen in all_general(db, ¤t_song) {
|
||||||
|
if let Some(t) = gen.and_then(|gen| gen.tags.iter().find(|t| t.contains(p)))
|
||||||
|
{
|
||||||
|
push!(t.to_owned());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextPart::If(condition, yes, no) => {
|
||||||
|
if !condition.gen(db, current_song).is_empty() {
|
||||||
|
yes.gen_to(db, current_song, out, line, scale, align);
|
||||||
|
} else {
|
||||||
|
no.gen_to(db, current_song, out, line, scale, align);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for TextBuilder {
|
||||||
|
type Err = TextBuilderParseError;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::from_chars(&mut s.chars())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TextBuilder {
|
||||||
|
fn from_chars(chars: &mut Chars) -> Result<Self, TextBuilderParseError> {
|
||||||
|
let mut vec = vec![];
|
||||||
|
let mut current = String::new();
|
||||||
|
macro_rules! done {
|
||||||
|
() => {
|
||||||
|
if !current.is_empty() {
|
||||||
|
// if it starts with at least one space, replace the first space with
|
||||||
|
// a No-Break space, as recommended in `https://github.com/QuantumBadger/Speedy2D/issues/45`,
|
||||||
|
// to avoid an issue where leading whitespaces are removed when drawing text.
|
||||||
|
if current.starts_with(' ') {
|
||||||
|
current = current.replacen(' ', "\u{00A0}", 1);
|
||||||
|
}
|
||||||
|
vec.push(TextPart::Literal(std::mem::replace(
|
||||||
|
&mut current,
|
||||||
|
String::new(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
loop {
|
||||||
|
if 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::SongTitle);
|
||||||
|
}
|
||||||
|
Some('a') => {
|
||||||
|
done!();
|
||||||
|
vec.push(TextPart::AlbumName);
|
||||||
|
}
|
||||||
|
Some('A') => {
|
||||||
|
done!();
|
||||||
|
vec.push(TextPart::ArtistName);
|
||||||
|
}
|
||||||
|
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(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::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;
|
||||||
|
}
|
||||||
|
Some(ch) => current.push(ch),
|
||||||
|
None => return Err(TextBuilderParseError::UnclosedPercent),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'?' => {
|
||||||
|
done!();
|
||||||
|
vec.push(TextPart::If(
|
||||||
|
Self::from_chars(chars)?,
|
||||||
|
Self::from_chars(chars)?,
|
||||||
|
Self::from_chars(chars)?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'#' => break,
|
||||||
|
ch => current.push(ch),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
done!();
|
||||||
|
Ok(Self(vec))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TextBuilderParseError {
|
||||||
|
UnclosedPercent,
|
||||||
|
TagModeUnknown(char),
|
||||||
|
TooFewCharsForColor,
|
||||||
|
ColorNotHex,
|
||||||
|
CouldntParse(String, String),
|
||||||
|
}
|
||||||
|
impl Display for TextBuilderParseError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::UnclosedPercent => write!(
|
||||||
|
f,
|
||||||
|
"Unclosed %: Syntax is %<mode><search>%, where <mode> is _, >, or =."
|
||||||
|
),
|
||||||
|
Self::TagModeUnknown(mode) => {
|
||||||
|
write!(f, "Unknown tag mode '{mode}': Allowed are only _, > or =.")
|
||||||
|
}
|
||||||
|
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}'."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
@ -58,6 +58,13 @@ fn main() {
|
|||||||
eprintln!("searching for artists...");
|
eprintln!("searching for artists...");
|
||||||
let mut artists = HashMap::new();
|
let mut artists = HashMap::new();
|
||||||
for song in songs {
|
for song in songs {
|
||||||
|
let mut general = GeneralData::default();
|
||||||
|
if let Some(year) = song.1.year() {
|
||||||
|
general.tags.push(format!("Year={year}"));
|
||||||
|
}
|
||||||
|
if let Some(genre) = song.1.genre_parsed() {
|
||||||
|
general.tags.push(format!("Genre={genre}"));
|
||||||
|
}
|
||||||
let (artist_id, album_id) = if let Some(artist) = song
|
let (artist_id, album_id) = if let Some(artist) = song
|
||||||
.1
|
.1
|
||||||
.album_artist()
|
.album_artist()
|
||||||
@ -135,7 +142,7 @@ fn main() {
|
|||||||
artist: artist_id,
|
artist: artist_id,
|
||||||
more_artists: vec![],
|
more_artists: vec![],
|
||||||
cover: None,
|
cover: None,
|
||||||
general: GeneralData::default(),
|
general,
|
||||||
cached_data: Arc::new(Mutex::new(None)),
|
cached_data: Arc::new(Mutex::new(None)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user