various improvements to the client (colored song subtext, search/'more', clear queue)

This commit is contained in:
Mark 2023-09-20 16:02:07 +02:00
parent 68f9747686
commit adaea9cd64
7 changed files with 601 additions and 72 deletions

View File

@ -308,11 +308,11 @@ impl ScrollBoxSizeUnit {
pub struct Button { pub struct Button {
config: GuiElemCfg, config: GuiElemCfg,
pub children: Vec<GuiElem>, pub children: Vec<GuiElem>,
action: Arc<dyn Fn(&Self) -> Vec<GuiAction> + 'static>, action: Arc<dyn Fn(&mut Self) -> Vec<GuiAction> + 'static>,
} }
impl Button { impl Button {
/// automatically adds w_mouse to config /// automatically adds w_mouse to config
pub fn new<F: Fn(&Self) -> Vec<GuiAction> + 'static>( pub fn new<F: Fn(&mut Self) -> Vec<GuiAction> + 'static>(
config: GuiElemCfg, config: GuiElemCfg,
action: F, action: F,
children: Vec<GuiElem>, children: Vec<GuiElem>,
@ -345,7 +345,7 @@ impl GuiElemTrait for Button {
} }
fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> { fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left { if button == MouseButton::Left {
(self.action)(self) (self.action.clone())(self)
} else { } else {
vec![] vec![]
} }

View File

@ -1,3 +1,8 @@
use std::{
rc::Rc,
sync::{atomic::AtomicBool, Arc},
};
use musicdb_lib::data::{database::Database, AlbumId, ArtistId, SongId}; use musicdb_lib::data::{database::Database, AlbumId, ArtistId, SongId};
use regex::{Regex, RegexBuilder}; use regex::{Regex, RegexBuilder};
use speedy2d::{ use speedy2d::{
@ -9,7 +14,7 @@ use speedy2d::{
use crate::{ use crate::{
gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_base::ScrollBox, gui_base::{Button, Panel, ScrollBox},
gui_edit::GuiEdit, gui_edit::GuiEdit,
gui_text::{Label, TextField}, gui_text::{Label, TextField},
gui_wrappers::WithFocusHotkey, gui_wrappers::WithFocusHotkey,
@ -32,13 +37,20 @@ pub struct LibraryBrowser {
search_album_regex: Option<Regex>, search_album_regex: Option<Regex>,
search_song: String, search_song: String,
search_song_regex: Option<Regex>, search_song_regex: Option<Regex>,
filter_target_state: Rc<AtomicBool>,
filter_state: f32,
search_is_case_sensitive: Rc<AtomicBool>,
search_was_case_sensitive: bool,
} }
fn search_regex_new(pat: &str) -> Result<Regex, regex::Error> { fn search_regex_new(pat: &str, case_insensitive: bool) -> Result<Regex, regex::Error> {
RegexBuilder::new(pat) RegexBuilder::new(pat)
.unicode(true) .unicode(true)
.case_insensitive(true) .case_insensitive(case_insensitive)
.build() .build()
} }
const LP_LIB1: f32 = 0.1;
const LP_LIB2: f32 = 1.0;
const LP_LIB1S: f32 = 0.4;
impl LibraryBrowser { impl LibraryBrowser {
pub fn new(config: GuiElemCfg) -> Self { pub fn new(config: GuiElemCfg) -> Self {
let search_artist = TextField::new( let search_artist = TextField::new(
@ -63,10 +75,30 @@ impl LibraryBrowser {
), ),
); );
let library_scroll_box = ScrollBox::new( let library_scroll_box = ScrollBox::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.1), (1.0, 1.0))), GuiElemCfg::at(Rectangle::from_tuples((0.0, LP_LIB1), (1.0, LP_LIB2))),
crate::gui_base::ScrollBoxSizeUnit::Pixels, crate::gui_base::ScrollBoxSizeUnit::Pixels,
vec![], vec![],
); );
let filter_target_state = Rc::new(AtomicBool::new(false));
let fts = Rc::clone(&filter_target_state);
let filter_button = Button::new(
GuiElemCfg::at(Rectangle::from_tuples((0.46, 0.01), (0.54, 0.05))),
move |_| {
fts.store(
!fts.load(std::sync::atomic::Ordering::Relaxed),
std::sync::atomic::Ordering::Relaxed,
);
vec![]
},
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
"more".to_owned(),
Color::GRAY,
None,
Vec2::new(0.5, 0.5),
))],
);
let search_is_case_sensitive = Rc::new(AtomicBool::new(false));
Self { Self {
config, config,
children: vec![ children: vec![
@ -74,6 +106,8 @@ impl LibraryBrowser {
GuiElem::new(search_album), GuiElem::new(search_album),
GuiElem::new(search_song), GuiElem::new(search_song),
GuiElem::new(library_scroll_box), GuiElem::new(library_scroll_box),
GuiElem::new(filter_button),
GuiElem::new(FilterPanel::new(Rc::clone(&search_is_case_sensitive))),
], ],
search_artist: String::new(), search_artist: String::new(),
search_artist_regex: None, search_artist_regex: None,
@ -81,6 +115,10 @@ impl LibraryBrowser {
search_album_regex: None, search_album_regex: None,
search_song: String::new(), search_song: String::new(),
search_song_regex: None, search_song_regex: None,
filter_target_state,
filter_state: 0.0,
search_is_case_sensitive,
search_was_case_sensitive: false,
} }
} }
} }
@ -104,16 +142,26 @@ impl GuiElemTrait for LibraryBrowser {
Box::new(self.clone()) Box::new(self.clone())
} }
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
// search
let mut search_changed = false; let mut search_changed = false;
let mut rebuild_regex = false;
let case_sensitive = self
.search_is_case_sensitive
.load(std::sync::atomic::Ordering::Relaxed);
if self.search_was_case_sensitive != case_sensitive {
self.search_was_case_sensitive = case_sensitive;
rebuild_regex = true;
}
{ {
let v = &mut self.children[0].try_as_mut::<TextField>().unwrap().children[0] let v = &mut self.children[0].try_as_mut::<TextField>().unwrap().children[0]
.try_as_mut::<Label>() .try_as_mut::<Label>()
.unwrap() .unwrap()
.content; .content;
if self.search_artist != *v.get_text() { if rebuild_regex || v.will_redraw() && self.search_artist != *v.get_text() {
search_changed = true; search_changed = true;
self.search_artist = v.get_text().clone(); self.search_artist = v.get_text().clone();
self.search_artist_regex = search_regex_new(&self.search_artist).ok(); self.search_artist_regex =
search_regex_new(&self.search_artist, !case_sensitive).ok();
*v.color() = if self.search_artist_regex.is_some() { *v.color() = if self.search_artist_regex.is_some() {
Color::WHITE Color::WHITE
} else { } else {
@ -126,10 +174,11 @@ impl GuiElemTrait for LibraryBrowser {
.try_as_mut::<Label>() .try_as_mut::<Label>()
.unwrap() .unwrap()
.content; .content;
if self.search_album != *v.get_text() { if rebuild_regex || v.will_redraw() && self.search_album != *v.get_text() {
search_changed = true; search_changed = true;
self.search_album = v.get_text().clone(); self.search_album = v.get_text().clone();
self.search_album_regex = search_regex_new(&self.search_album).ok(); self.search_album_regex =
search_regex_new(&self.search_album, !case_sensitive).ok();
*v.color() = if self.search_album_regex.is_some() { *v.color() = if self.search_album_regex.is_some() {
Color::WHITE Color::WHITE
} else { } else {
@ -146,10 +195,10 @@ impl GuiElemTrait for LibraryBrowser {
.try_as_mut::<Label>() .try_as_mut::<Label>()
.unwrap() .unwrap()
.content; .content;
if self.search_song != *v.get_text() { if rebuild_regex || v.will_redraw() && self.search_song != *v.get_text() {
search_changed = true; search_changed = true;
self.search_song = v.get_text().clone(); self.search_song = v.get_text().clone();
self.search_song_regex = search_regex_new(&self.search_song).ok(); self.search_song_regex = search_regex_new(&self.search_song, !case_sensitive).ok();
*v.color() = if self.search_song_regex.is_some() { *v.color() = if self.search_song_regex.is_some() {
Color::WHITE Color::WHITE
} else { } else {
@ -157,6 +206,44 @@ impl GuiElemTrait for LibraryBrowser {
}; };
} }
} }
// filter
let filter_target_state = self
.filter_target_state
.load(std::sync::atomic::Ordering::Relaxed);
let draw_filter = if filter_target_state && self.filter_state != 1.0 {
if let Some(h) = &info.helper {
h.request_redraw();
}
self.filter_state += (1.0 - self.filter_state) * 0.2;
if self.filter_state > 0.999 {
self.filter_state = 1.0;
}
true
} else if !filter_target_state && self.filter_state != 0.0 {
if let Some(h) = &info.helper {
h.request_redraw();
}
self.filter_state *= 0.8;
if self.filter_state < 0.001 {
self.filter_state = 0.0;
}
true
} else {
false
};
if draw_filter {
let y = LP_LIB1 + (LP_LIB1S - LP_LIB1) * self.filter_state;
self.children[3]
.try_as_mut::<ScrollBox>()
.unwrap()
.config_mut()
.pos = Rectangle::new(Vec2::new(0.0, y), Vec2::new(1.0, LP_LIB2));
let filter_panel = self.children[5].try_as_mut::<FilterPanel>().unwrap();
filter_panel.config_mut().pos =
Rectangle::new(Vec2::new(0.0, LP_LIB1), Vec2::new(1.0, y));
filter_panel.config.enabled = self.filter_state > 0.0;
}
// -
if self.config.redraw || search_changed || info.pos.size() != self.config.pixel_pos.size() { if self.config.redraw || search_changed || info.pos.size() != self.config.pixel_pos.size() {
self.config.redraw = false; self.config.redraw = false;
self.update_list(&info.database, info.line_height); self.update_list(&info.database, info.line_height);
@ -190,6 +277,7 @@ impl LibraryBrowser {
)), )),
artist_height, artist_height,
)); ));
if self.search_album.is_empty() {
for song_id in &artist.singles { for song_id in &artist.singles {
if let Some(song) = db.songs().get(song_id) { if let Some(song) = db.songs().get(song_id) {
if self.search_song.is_empty() if self.search_song.is_empty()
@ -212,6 +300,7 @@ impl LibraryBrowser {
} }
} }
} }
}
for album_id in &artist.albums { for album_id in &artist.albums {
if let Some(album) = db.albums().get(album_id) { if let Some(album) = db.albums().get(album_id) {
if self.search_album.is_empty() if self.search_album.is_empty()
@ -545,3 +634,120 @@ impl GuiElemTrait for ListSong {
} }
} }
} }
#[derive(Clone)]
struct FilterPanel {
config: GuiElemCfg,
children: Vec<GuiElem>,
line_height: f32,
}
const FP_CASESENS_N: &'static str = "Switch to case-sensitive search";
const FP_CASESENS_Y: &'static str = "Switch to case-insensitive search";
impl FilterPanel {
pub fn new(search_is_case_sensitive: Rc<AtomicBool>) -> Self {
let is_case_sensitive = search_is_case_sensitive.load(std::sync::atomic::Ordering::Relaxed);
Self {
config: GuiElemCfg::default().disabled(),
children: vec![GuiElem::new(ScrollBox::new(
GuiElemCfg::default(),
crate::gui_base::ScrollBoxSizeUnit::Pixels,
vec![
(
GuiElem::new(Button::new(
GuiElemCfg::default(),
move |button| {
let is_case_sensitive = !search_is_case_sensitive
.load(std::sync::atomic::Ordering::Relaxed);
search_is_case_sensitive
.store(is_case_sensitive, std::sync::atomic::Ordering::Relaxed);
*button
.children()
.next()
.unwrap()
.try_as_mut::<Label>()
.unwrap()
.content
.text() = if is_case_sensitive {
FP_CASESENS_Y.to_owned()
} else {
FP_CASESENS_N.to_owned()
};
vec![]
},
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
if is_case_sensitive {
FP_CASESENS_Y.to_owned()
} else {
FP_CASESENS_N.to_owned()
},
Color::GRAY,
None,
Vec2::new(0.5, 0.5),
))],
)),
1.0,
),
(
GuiElem::new(Button::new(
GuiElemCfg::default(),
|button| {
let text = button
.children()
.next()
.unwrap()
.try_as_mut::<Label>()
.unwrap()
.content
.text();
*text = if text.len() > 20 {
"Click for RegEx help".to_owned()
} else {
"Click to close RegEx help\ntest\nyay".to_owned()
};
vec![]
},
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
"Click for RegEx help".to_owned(),
Color::GRAY,
None,
Vec2::new(0.5, 0.0),
))],
)),
1.0,
),
],
))],
line_height: 0.0,
}
}
}
impl GuiElemTrait for FilterPanel {
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
if info.line_height != self.line_height {
for (_, h) in &mut self.children[0].try_as_mut::<ScrollBox>().unwrap().children {
*h = info.line_height;
}
self.line_height = info.line_height;
}
}
fn config(&self) -> &GuiElemCfg {
&self.config
}
fn config_mut(&mut self) -> &mut GuiElemCfg {
&mut self.config
}
fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> {
Box::new(self.children.iter_mut())
}
fn any(&self) -> &dyn std::any::Any {
self
}
fn any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn clone_gui(&self) -> Box<dyn GuiElemTrait> {
Box::new(self.clone())
}
}

View File

@ -8,7 +8,7 @@ use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::MouseButton}
use crate::{ use crate::{
gui::{adjust_area, adjust_pos, GuiAction, GuiCover, GuiElem, GuiElemCfg, GuiElemTrait}, gui::{adjust_area, adjust_pos, GuiAction, GuiCover, GuiElem, GuiElemCfg, GuiElemTrait},
gui_text::Label, gui_text::{AdvancedLabel, Content, Label},
}; };
/* /*
@ -25,6 +25,7 @@ pub struct CurrentSong {
prev_song: Option<SongId>, prev_song: Option<SongId>,
cover_pos: Rectangle, cover_pos: Rectangle,
covers: VecDeque<(CoverId, Option<(bool, Instant)>)>, covers: VecDeque<(CoverId, Option<(bool, Instant)>)>,
text_updated: Option<Instant>,
} }
impl CurrentSong { impl CurrentSong {
pub fn new(config: GuiElemCfg) -> Self { pub fn new(config: GuiElemCfg) -> Self {
@ -38,19 +39,39 @@ impl CurrentSong {
None, None,
Vec2::new(0.0, 1.0), Vec2::new(0.0, 1.0),
)), )),
GuiElem::new(Label::new( GuiElem::new(AdvancedLabel::new(
GuiElemCfg::at(Rectangle::from_tuples((0.4, 0.5), (1.0, 1.0))), GuiElemCfg::at(Rectangle::from_tuples((0.4, 0.5), (1.0, 1.0))),
"".to_owned(),
Color::from_int_rgb(120, 120, 120),
None,
Vec2::new(0.0, 0.0), Vec2::new(0.0, 0.0),
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,
text_updated: None,
} }
} }
pub fn set_idle_mode(&mut self, idle_mode: f32) {
// ...
}
fn color_title(a: f32) -> Color {
Self::color_with_alpha(&Color::WHITE, a)
}
fn color_artist(a: f32) -> Color {
Color::from_rgba(0.32, 0.20, 0.49, a)
}
fn color_album(a: f32) -> Color {
Color::from_rgba(0.03, 0.24, 0.18, a)
}
fn color_by(a: f32) -> Color {
Self::color_with_alpha(&Color::DARK_GRAY, a)
}
fn color_on(a: f32) -> Color {
Self::color_with_alpha(&Color::DARK_GRAY, a)
}
fn color_with_alpha(c: &Color, a: f32) -> Color {
Color::from_rgba(c.r(), c.g(), c.b(), a)
}
} }
impl GuiElemTrait for CurrentSong { impl GuiElemTrait for CurrentSong {
fn config(&self) -> &GuiElemCfg { fn config(&self) -> &GuiElemCfg {
@ -127,39 +148,130 @@ impl GuiElemTrait for CurrentSong {
.as_ref() .as_ref()
.and_then(|id| info.database.albums().get(id)), .and_then(|id| info.database.albums().get(id)),
) { ) {
(None, None) => String::new(), (None, None) => vec![],
(Some(artist), None) => format!("by {}", artist.name), (Some(artist), None) => vec![
(None, Some(album)) => { (
if let Some(artist) = info.database.artists().get(&album.artist) { Content::new("by ".to_owned(), Self::color_by(0.0)),
format!("on {} by {}", album.name, artist.name) 1.0,
} else { 1.0,
format!("on {}", album.name) ),
} (
} Content::new(artist.name.to_owned(), Self::color_artist(0.0)),
(Some(artist), Some(album)) => { 1.0,
format!("by {} on {}", artist.name, album.name) 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) (song.title.clone(), sub)
} else { } else {
( (
"< song not in db >".to_owned(), "< song not in db >".to_owned(),
"maybe restart the client to resync the database?".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 { } else {
(String::new(), String::new()) (String::new(), vec![])
}; };
*self.children[0] *self.children[0]
.try_as_mut::<Label>() .try_as_mut::<Label>()
.unwrap() .unwrap()
.content .content
.text() = name; .text() = name;
*self.children[1] self.children[1]
.try_as_mut::<AdvancedLabel>()
.unwrap()
.content = subtext;
self.text_updated = Some(Instant::now());
}
}
if let Some(updated) = &self.text_updated {
if let Some(h) = &info.helper {
h.request_redraw();
}
let prog = updated.elapsed().as_secs_f32();
*self.children[0]
.try_as_mut::<Label>() .try_as_mut::<Label>()
.unwrap() .unwrap()
.content .content
.text() = subtext; .color() = Self::color_title((prog / 1.5).min(1.0));
let subtext = self.children[1].try_as_mut::<AdvancedLabel>().unwrap();
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

View File

@ -36,13 +36,17 @@ pub struct QueueViewer {
config: GuiElemCfg, config: GuiElemCfg,
children: Vec<GuiElem>, children: Vec<GuiElem>,
} }
const QP_QUEUE1: f32 = 0.0;
const QP_QUEUE2: f32 = 0.95;
const QP_INV1: f32 = QP_QUEUE2;
const QP_INV2: f32 = 1.0;
impl QueueViewer { impl QueueViewer {
pub fn new(config: GuiElemCfg) -> Self { pub fn new(config: GuiElemCfg) -> Self {
Self { Self {
config, config,
children: vec![ children: vec![
GuiElem::new(ScrollBox::new( GuiElem::new(ScrollBox::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0665), (1.0, 1.0))), GuiElemCfg::at(Rectangle::from_tuples((0.0, QP_QUEUE1), (1.0, QP_QUEUE2))),
crate::gui_base::ScrollBoxSizeUnit::Pixels, crate::gui_base::ScrollBoxSizeUnit::Pixels,
vec![( vec![(
GuiElem::new(Label::new( GuiElem::new(Label::new(
@ -56,10 +60,10 @@ impl QueueViewer {
)], )],
)), )),
GuiElem::new(QueueEmptySpaceDragHandler::new(GuiElemCfg::at( GuiElem::new(QueueEmptySpaceDragHandler::new(GuiElemCfg::at(
Rectangle::from_tuples((0.0, 0.0665), (1.0, 1.0)), Rectangle::from_tuples((0.0, QP_QUEUE1), (1.0, QP_QUEUE2)),
))), ))),
GuiElem::new(Panel::new( GuiElem::new(Panel::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.0665))), GuiElemCfg::at(Rectangle::from_tuples((0.0, QP_INV1), (1.0, QP_INV2))),
vec![ vec![
GuiElem::new( GuiElem::new(
QueueLoop::new( QueueLoop::new(

View File

@ -78,7 +78,7 @@ impl GuiScreen {
scroll_sensitivity_pages: f64, scroll_sensitivity_pages: f64,
) -> Self { ) -> Self {
Self { Self {
config: config.w_keyboard_watch(), config: config.w_keyboard_watch().w_mouse(),
children: vec![ children: vec![
GuiElem::new(StatusBar::new( GuiElem::new(StatusBar::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.9), (1.0, 1.0))), GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.9), (1.0, 1.0))),
@ -95,7 +95,7 @@ impl GuiScreen {
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.9))), GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.9))),
vec![ vec![
GuiElem::new(Button::new( GuiElem::new(Button::new(
GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.03))), GuiElemCfg::at(Rectangle::from_tuples((0.75, 0.0), (0.875, 0.03))),
|_| vec![GuiAction::OpenSettings(true)], |_| vec![GuiAction::OpenSettings(true)],
vec![GuiElem::new(Label::new( vec![GuiElem::new(Label::new(
GuiElemCfg::default(), GuiElemCfg::default(),
@ -106,7 +106,7 @@ impl GuiScreen {
))], ))],
)), )),
GuiElem::new(Button::new( GuiElem::new(Button::new(
GuiElemCfg::at(Rectangle::from_tuples((0.75, 0.0), (1.0, 0.03))), GuiElemCfg::at(Rectangle::from_tuples((0.875, 0.0), (1.0, 0.03))),
|_| vec![GuiAction::Exit], |_| vec![GuiAction::Exit],
vec![GuiElem::new(Label::new( vec![GuiElem::new(Label::new(
GuiElemCfg::default(), GuiElemCfg::default(),
@ -124,6 +124,29 @@ impl GuiScreen {
(0.5, 0.03), (0.5, 0.03),
(1.0, 1.0), (1.0, 1.0),
)))), )))),
GuiElem::new(Button::new(
GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.03))),
|_| {
vec![GuiAction::SendToServer(
musicdb_lib::server::Command::QueueUpdate(
vec![],
musicdb_lib::data::queue::QueueContent::Folder(
0,
vec![],
String::new(),
)
.into(),
),
)]
},
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
"Clear Queue".to_string(),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
))],
)),
], ],
)), )),
], ],
@ -203,7 +226,11 @@ impl GuiElemTrait for GuiScreen {
self.not_idle(); self.not_idle();
vec![] vec![]
} }
fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) { fn mouse_down(&mut self, _button: speedy2d::window::MouseButton) -> Vec<GuiAction> {
self.not_idle();
vec![]
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut Graphics2D) {
// idle stuff // idle stuff
if self.prev_mouse_pos != info.mouse_pos { if self.prev_mouse_pos != info.mouse_pos {
self.prev_mouse_pos = info.mouse_pos; self.prev_mouse_pos = info.mouse_pos;
@ -289,6 +316,7 @@ pub struct StatusBar {
config: GuiElemCfg, config: GuiElemCfg,
children: Vec<GuiElem>, children: Vec<GuiElem>,
idle_mode: f32, idle_mode: f32,
idle_prev: f32,
} }
impl StatusBar { impl StatusBar {
pub fn new(config: GuiElemCfg, playing: bool) -> Self { pub fn new(config: GuiElemCfg, playing: bool) -> Self {
@ -301,17 +329,31 @@ impl StatusBar {
)))), )))),
GuiElem::new(PlayPauseToggle::new( GuiElem::new(PlayPauseToggle::new(
GuiElemCfg::at(Rectangle::from_tuples((0.85, 0.0), (0.95, 1.0))), GuiElemCfg::at(Rectangle::from_tuples((0.85, 0.0), (0.95, 1.0))),
false, playing,
)),
GuiElem::new(Panel::with_background(
GuiElemCfg::default(),
vec![],
Color::BLACK,
)), )),
GuiElem::new(Panel::new(GuiElemCfg::default(), vec![])),
], ],
idle_mode: 0.0, idle_mode: 0.0,
idle_prev: 0.0,
} }
} }
const fn index_current_song() -> usize {
0
}
const fn index_play_pause_toggle() -> usize {
1
}
const fn index_bgpanel() -> usize {
2
}
pub fn set_background(&mut self, bg: Option<Color>) {
self.children[Self::index_bgpanel()]
.inner
.any_mut()
.downcast_mut::<Panel>()
.unwrap()
.background = bg;
}
} }
impl GuiElemTrait for StatusBar { impl GuiElemTrait for StatusBar {
fn config(&self) -> &GuiElemCfg { fn config(&self) -> &GuiElemCfg {
@ -333,6 +375,8 @@ impl GuiElemTrait for StatusBar {
Box::new(self.clone()) Box::new(self.clone())
} }
fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) { fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) {
// the line that separates this section from the rest of the ui.
// fades away when idle_mode approaches 1.0
if self.idle_mode < 1.0 { if self.idle_mode < 1.0 {
g.draw_line( g.draw_line(
info.pos.top_left(), info.pos.top_left(),
@ -341,5 +385,28 @@ impl GuiElemTrait for StatusBar {
Color::from_rgba(1.0, 1.0, 1.0, 1.0 - self.idle_mode), Color::from_rgba(1.0, 1.0, 1.0, 1.0 - self.idle_mode),
); );
} }
if self.idle_mode != self.idle_prev {
// if exiting the moving stage, set background to transparent.
// if entering the moving stage, set background to black.
if self.idle_mode == 1.0 || self.idle_mode == 0.0 {
self.set_background(None);
} else if self.idle_prev == 1.0 || self.idle_prev == 0.0 {
self.set_background(Some(Color::BLACK));
}
// position the text
let current_song = self.children[Self::index_current_song()]
.inner
.any_mut()
.downcast_mut::<CurrentSong>()
.unwrap();
current_song.set_idle_mode(self.idle_mode);
let play_pause = self.children[Self::index_play_pause_toggle()]
.inner
.any_mut()
.downcast_mut::<PlayPauseToggle>()
.unwrap();
// - - - - -
self.idle_prev = self.idle_mode;
}
} }
} }

View File

@ -32,6 +32,14 @@ pub struct Content {
formatted: Option<Rc<FormattedTextBlock>>, formatted: Option<Rc<FormattedTextBlock>>,
} }
impl Content { impl Content {
pub fn new(text: String, color: Color) -> Self {
Self {
text,
color,
background: None,
formatted: None,
}
}
pub fn get_text(&self) -> &String { pub fn get_text(&self) -> &String {
&self.text &self.text
} }
@ -129,7 +137,7 @@ impl GuiElemTrait for Label {
// TODO! this, but requires keyboard events first // TODO! this, but requires keyboard events first
/// a single-line text fields for users to type text into. /// a single-line text field for users to type text into.
#[derive(Clone)] #[derive(Clone)]
pub struct TextField { pub struct TextField {
config: GuiElemCfg, config: GuiElemCfg,
@ -247,3 +255,108 @@ impl GuiElemTrait for TextField {
vec![] vec![]
} }
} }
/// More advanced version of `Label`.
/// Allows stringing together multiple `Content`s in one line.
#[derive(Clone)]
pub struct AdvancedLabel {
config: GuiElemCfg,
children: Vec<GuiElem>,
/// 0.0 => align to top/left
/// 0.5 => center
/// 1.0 => align to bottom/right
pub align: Vec2,
/// (Content, Size-Scale, Height)
/// Size-Scale and Height should default to 1.0.
pub content: Vec<(Content, f32, f32)>,
/// the position from where content drawing starts.
/// recalculated when layouting is performed.
content_pos: Vec2,
content_height: f32,
}
impl AdvancedLabel {
pub fn new(config: GuiElemCfg, align: Vec2, content: Vec<(Content, f32, f32)>) -> Self {
Self {
config,
children: vec![],
align,
content,
content_pos: Vec2::ZERO,
content_height: 0.0,
}
}
}
impl GuiElemTrait for AdvancedLabel {
fn config(&self) -> &GuiElemCfg {
&self.config
}
fn config_mut(&mut self) -> &mut GuiElemCfg {
&mut self.config
}
fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> {
Box::new(self.children.iter_mut())
}
fn any(&self) -> &dyn std::any::Any {
self
}
fn any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn clone_gui(&self) -> Box<dyn GuiElemTrait> {
Box::new(self.clone())
}
fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
if self.config.redraw
|| self.config.pixel_pos.size() != info.pos.size()
|| self.content.iter().any(|(c, _, _)| c.will_redraw())
{
self.config.redraw = false;
let mut len = 0.0;
let mut height = 0.0;
for (c, scale, _) in &self.content {
let mut 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;
}
}
if len > 0.0 && height > 0.0 {
let scale1 = info.pos.width() / len;
let scale2 = info.pos.height() / height;
let scale;
self.content_pos = if scale1 < scale2 {
// use all available width
scale = scale1;
self.content_height = height * scale;
let pad = info.pos.height() - self.content_height;
Vec2::new(0.0, pad * self.align.y)
} else {
// use all available height
scale = scale2;
self.content_height = info.pos.height();
let pad = info.pos.width() - len * scale;
Vec2::new(pad * self.align.x, 0.0)
};
for (c, s, _) in &mut self.content {
c.formatted = Some(info.font.layout_text(
&c.text,
scale * (*s),
TextOptions::new(),
));
}
}
}
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;
for (c, _, h) in &self.content {
if let Some(f) = &c.formatted {
let y = pos_y + (self.content_height - f.height()) * h;
g.draw_text(Vec2::new(pos_x, y), c.color, f);
pos_x += f.width();
}
}
}
}

View File

@ -140,12 +140,18 @@ fn main() {
}); });
} }
eprintln!("searching for covers..."); eprintln!("searching for covers...");
let mut multiple_cover_options = vec![];
let mut single_images = HashMap::new(); let mut single_images = HashMap::new();
for (i1, (_artist, (artist_id, albums))) in artists.iter().enumerate() { for (i1, (_artist, (artist_id, albums))) in artists.iter().enumerate() {
eprint!("\rartist {}/{}", i1 + 1, artists.len()); eprint!("\rartist {}/{}", i1 + 1, artists.len());
for (_album, (album_id, album_dir)) in albums { for (_album, (album_id, album_dir)) in albums {
if let Some(album_dir) = album_dir { if let Some(album_dir) = album_dir {
if let Some(cover_id) = get_cover(&mut database, &lib_dir, album_dir) { if let Some(cover_id) = get_cover(
&mut database,
&lib_dir,
album_dir,
&mut multiple_cover_options,
) {
database.albums_mut().get_mut(album_id).unwrap().cover = Some(cover_id); database.albums_mut().get_mut(album_id).unwrap().cover = Some(cover_id);
} }
} }
@ -158,7 +164,9 @@ fn main() {
{ {
let cover_id = if let Some(cover_id) = single_images.get(dir) { let cover_id = if let Some(cover_id) = single_images.get(dir) {
Some(*cover_id) Some(*cover_id)
} else if let Some(cover_id) = get_cover(&mut database, &lib_dir, dir) { } else if let Some(cover_id) =
get_cover(&mut database, &lib_dir, dir, &mut multiple_cover_options)
{
single_images.insert(dir.to_owned(), cover_id); single_images.insert(dir.to_owned(), cover_id);
Some(cover_id) Some(cover_id)
} else { } else {
@ -171,6 +179,13 @@ fn main() {
} }
} }
eprintln!(); eprintln!();
if !multiple_cover_options.is_empty() {
eprintln!("> Found more than one cover in the following directories: ");
for dir in multiple_cover_options {
eprintln!(">> {}", dir.to_string_lossy());
}
eprintln!("> Default behavior is using the largest image file found.");
}
if let Some(uka) = database.artists().get(&unknown_artist) { if let Some(uka) = database.artists().get(&unknown_artist) {
if uka.albums.is_empty() && uka.singles.is_empty() { if uka.albums.is_empty() && uka.singles.is_empty() {
database.artists_mut().remove(&unknown_artist); database.artists_mut().remove(&unknown_artist);
@ -212,9 +227,15 @@ impl OnceNewline {
} }
} }
fn get_cover(database: &mut Database, lib_dir: &str, abs_dir: impl AsRef<Path>) -> Option<CoverId> { fn get_cover(
database: &mut Database,
lib_dir: &str,
abs_dir: impl AsRef<Path>,
multiple_options_list: &mut Vec<PathBuf>,
) -> Option<CoverId> {
let mut multiple = false;
let mut cover = None; let mut cover = None;
if let Ok(files) = fs::read_dir(abs_dir) { if let Ok(files) = fs::read_dir(&abs_dir) {
for file in files { for file in files {
if let Ok(file) = file { if let Ok(file) = file {
if let Ok(metadata) = file.metadata() { if let Ok(metadata) = file.metadata() {
@ -228,6 +249,9 @@ fn get_cover(database: &mut Database, lib_dir: &str, abs_dir: impl AsRef<Path>)
.as_ref() .as_ref()
.is_some_and(|(_, size)| *size < metadata.len()) .is_some_and(|(_, size)| *size < metadata.len())
{ {
if cover.is_some() {
multiple = true;
}
cover = Some((path, metadata.len())); cover = Some((path, metadata.len()));
} }
} }
@ -236,6 +260,9 @@ fn get_cover(database: &mut Database, lib_dir: &str, abs_dir: impl AsRef<Path>)
} }
} }
} }
if multiple {
multiple_options_list.push(abs_dir.as_ref().to_path_buf());
}
if let Some((path, _)) = cover { if let Some((path, _)) = cover {
let rel_path = path.strip_prefix(&lib_dir).unwrap().to_path_buf(); let rel_path = path.strip_prefix(&lib_dir).unwrap().to_path_buf();
Some(database.add_cover_new(Cover { Some(database.add_cover_new(Cover {