This commit is contained in:
Mark
2023-08-13 23:58:53 +02:00
commit 922e4fcc00
43 changed files with 6822 additions and 0 deletions

1
musicdb-client/.gitignore vendored Executable file
View File

@@ -0,0 +1 @@
/target

14
musicdb-client/Cargo.toml Executable file
View File

@@ -0,0 +1,14 @@
[package]
name = "musicdb-client"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" }
regex = "1.9.3"
speedy2d = { version = "1.12.0", optional = true }
[features]
default = ["speedy2d"]

880
musicdb-client/src/gui.rs Executable file
View File

@@ -0,0 +1,880 @@
use std::{
any::Any,
eprintln,
net::TcpStream,
sync::{Arc, Mutex},
time::Instant,
usize,
};
use musicdb_lib::{
data::{database::Database, queue::Queue, AlbumId, ArtistId, SongId},
load::ToFromBytes,
server::Command,
};
use speedy2d::{
color::Color,
dimen::{UVec2, Vec2},
font::Font,
shape::Rectangle,
window::{
KeyScancode, ModifiersState, MouseButton, MouseScrollDistance, UserEventSender,
VirtualKeyCode, WindowCreationOptions, WindowHandler, WindowHelper,
},
Graphics2D,
};
use crate::{gui_screen::GuiScreen, gui_wrappers::WithFocusHotkey};
pub enum GuiEvent {
Refresh,
UpdatedQueue,
UpdatedLibrary,
Exit,
}
pub fn main(
database: Arc<Mutex<Database>>,
connection: TcpStream,
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
) {
let window = speedy2d::Window::<GuiEvent>::new_with_user_events(
"MusicDB Client",
WindowCreationOptions::new_fullscreen_borderless(),
)
.expect("couldn't open window");
*event_sender_arc.lock().unwrap() = Some(window.create_user_event_sender());
let sender = window.create_user_event_sender();
window.run_loop(Gui::new(database, connection, event_sender_arc, sender));
}
pub struct Gui {
pub event_sender: UserEventSender<GuiEvent>,
pub database: Arc<Mutex<Database>>,
pub connection: TcpStream,
pub gui: GuiElem,
pub size: UVec2,
pub mouse_pos: Vec2,
pub font: Font,
pub last_draw: Instant,
pub modifiers: ModifiersState,
pub dragging: Option<(
Dragging,
Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>,
)>,
pub line_height: f32,
pub last_height: f32,
pub scroll_pixels_multiplier: f64,
pub scroll_lines_multiplier: f64,
pub scroll_pages_multiplier: f64,
}
impl Gui {
fn new(
database: Arc<Mutex<Database>>,
connection: TcpStream,
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
event_sender: UserEventSender<GuiEvent>,
) -> Self {
database.lock().unwrap().update_endpoints.push(
musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd {
Command::Resume
| Command::Pause
| Command::Stop
| Command::Save
| Command::SetLibraryDirectory(..) => {}
Command::NextSong
| Command::QueueUpdate(..)
| Command::QueueAdd(..)
| Command::QueueInsert(..)
| Command::QueueRemove(..)
| Command::QueueGoto(..) => {
if let Some(s) = &*event_sender_arc.lock().unwrap() {
_ = s.send_event(GuiEvent::UpdatedQueue);
}
}
Command::SyncDatabase(..)
| Command::AddSong(_)
| Command::AddAlbum(_)
| Command::AddArtist(_)
| Command::ModifySong(_)
| Command::ModifyAlbum(_)
| Command::ModifyArtist(_) => {
if let Some(s) = &*event_sender_arc.lock().unwrap() {
_ = s.send_event(GuiEvent::UpdatedLibrary);
}
}
})),
);
let line_height = 32.0;
let scroll_pixels_multiplier = 1.0;
let scroll_lines_multiplier = 3.0;
let scroll_pages_multiplier = 0.75;
Gui {
event_sender,
database,
connection,
gui: GuiElem::new(WithFocusHotkey::new_noshift(
VirtualKeyCode::Escape,
GuiScreen::new(
GuiElemCfg::default(),
line_height,
scroll_pixels_multiplier,
scroll_lines_multiplier,
scroll_pages_multiplier,
),
)),
size: UVec2::ZERO,
mouse_pos: Vec2::ZERO,
font: Font::new(include_bytes!(
"/usr/share/fonts/mozilla-fira/FiraSans-Regular.otf"
))
.unwrap(),
// font: Font::new(include_bytes!("/usr/share/fonts/TTF/FiraSans-Regular.ttf")).unwrap(),
last_draw: Instant::now(),
modifiers: ModifiersState::default(),
dragging: None,
line_height,
last_height: 720.0,
scroll_pixels_multiplier,
scroll_lines_multiplier,
scroll_pages_multiplier,
}
}
}
/// the trait implemented by all Gui elements.
/// feel free to override the methods you wish to use.
#[allow(unused)]
pub trait GuiElemTrait {
fn config(&self) -> &GuiElemCfg;
fn config_mut(&mut self) -> &mut GuiElemCfg;
/// note: drawing happens from the last to the first element, while priority is from first to last.
/// if you wish to add a "high priority" child to a Vec<GuiElem> using push, .rev() the iterator in this method.
fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_>;
fn any(&self) -> &dyn Any;
fn any_mut(&mut self) -> &mut dyn Any;
fn clone_gui(&self) -> Box<dyn GuiElemTrait>;
/// handles drawing.
fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) {}
/// an event that is invoked whenever a mouse button is pressed on the element.
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
/// an event that is invoked whenever a mouse button that was pressed on the element is released anywhere.
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
/// an event that is invoked after a mouse button was pressed and released on the same GUI element.
fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn mouse_wheel(&mut self, diff: f32) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn char_watch(&mut self, modifiers: ModifiersState, key: char) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn char_focus(&mut self, modifiers: ModifiersState, key: char) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn key_watch(
&mut self,
modifiers: ModifiersState,
down: bool,
key: Option<VirtualKeyCode>,
scan: KeyScancode,
) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn key_focus(
&mut self,
modifiers: ModifiersState,
down: bool,
key: Option<VirtualKeyCode>,
scan: KeyScancode,
) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
/// When something is dragged and released over this element
fn dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn updated_library(&mut self) {}
fn updated_queue(&mut self) {}
}
#[derive(Debug, Clone)]
pub struct GuiElemCfg {
pub enabled: bool,
/// if true, indicates that something (text size, screen size, ...) has changed
/// and you should probably relayout and redraw from scratch.
pub redraw: bool,
pub pos: Rectangle,
/// the pixel position after the last call to draw().
/// in draw, use info.pos instead, as pixel_pos is only updated *after* draw().
/// this can act like a "previous pos" field within draw.
pub pixel_pos: Rectangle,
pub mouse_down: (bool, bool, bool),
pub mouse_events: bool,
pub scroll_events: bool,
/// allows elements to watch all keyboard events, regardless of keyboard focus.
pub keyboard_events_watch: bool,
/// indicates that this element can have the keyboard focus
pub keyboard_events_focus: bool,
/// index of the child that has keyboard focus. if usize::MAX, `self` has focus.
/// will automatically be changed when Tab is pressed. [TODO]
pub keyboard_focus_index: usize,
pub request_keyboard_focus: bool,
pub drag_target: bool,
}
impl GuiElemCfg {
pub fn at(pos: Rectangle) -> Self {
Self {
pos,
..Default::default()
}
}
pub fn w_mouse(mut self) -> Self {
self.mouse_events = true;
self
}
pub fn w_scroll(mut self) -> Self {
self.scroll_events = true;
self
}
pub fn w_keyboard_watch(mut self) -> Self {
self.keyboard_events_watch = true;
self
}
pub fn w_keyboard_focus(mut self) -> Self {
self.keyboard_events_focus = true;
self
}
pub fn w_drag_target(mut self) -> Self {
self.drag_target = true;
self
}
pub fn disabled(mut self) -> Self {
self.enabled = false;
self
}
}
impl Default for GuiElemCfg {
fn default() -> Self {
Self {
enabled: true,
redraw: false,
pos: Rectangle::new(Vec2::ZERO, Vec2::new(1.0, 1.0)),
pixel_pos: Rectangle::ZERO,
mouse_down: (false, false, false),
mouse_events: false,
scroll_events: false,
keyboard_events_watch: false,
keyboard_events_focus: false,
keyboard_focus_index: usize::MAX,
request_keyboard_focus: false,
drag_target: false,
}
}
}
pub enum GuiAction {
OpenMain,
SetIdle(bool),
OpenSettings(bool),
Build(Box<dyn FnOnce(&mut Database) -> Vec<Self>>),
SendToServer(Command),
/// unfocuses all gui elements, then assigns keyboard focus to one with config().request_keyboard_focus == true.
ResetKeyboardFocus,
SetDragging(
Option<(
Dragging,
Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>,
)>,
),
SetLineHeight(f32),
Do(Box<dyn FnMut(&mut Gui)>),
Exit,
}
pub enum Dragging {
Artist(ArtistId),
Album(AlbumId),
Song(SongId),
Queue(Queue),
}
pub struct DrawInfo<'a> {
pub actions: Vec<GuiAction>,
pub pos: Rectangle,
pub database: &'a mut Database,
pub font: &'a Font,
/// absolute position of the mouse on the screen.
/// compare this to `pos` to find the mouse's relative position.
pub mouse_pos: Vec2,
pub helper: Option<&'a mut WindowHelper<GuiEvent>>,
pub has_keyboard_focus: bool,
pub child_has_keyboard_focus: bool,
/// the height of one line of text (in pixels)
pub line_height: f32,
}
pub struct GuiElem {
pub inner: Box<dyn GuiElemTrait>,
}
impl Clone for GuiElem {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone_gui(),
}
}
}
impl GuiElem {
pub fn new<T: GuiElemTrait + 'static>(inner: T) -> Self {
Self {
inner: Box::new(inner),
}
}
pub fn try_as<T: Any>(&self) -> Option<&T> {
self.inner.any().downcast_ref()
}
pub fn try_as_mut<T: Any>(&mut self) -> Option<&mut T> {
self.inner.any_mut().downcast_mut()
}
pub fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) {
if !self.inner.config_mut().enabled {
return;
}
// adjust info
let npos = adjust_area(&info.pos, &self.inner.config_mut().pos);
let ppos = std::mem::replace(&mut info.pos, npos);
if info.child_has_keyboard_focus {
if self.inner.config().keyboard_focus_index == usize::MAX {
info.has_keyboard_focus = true;
info.child_has_keyboard_focus = false;
}
}
// call trait's draw function
self.inner.draw(info, g);
// reset info
info.has_keyboard_focus = false;
let focus_path = info.child_has_keyboard_focus;
// children (in reverse order - first element has the highest priority)
let kbd_focus_index = self.inner.config().keyboard_focus_index;
for (i, c) in self
.inner
.children()
.collect::<Vec<_>>()
.into_iter()
.enumerate()
.rev()
{
info.child_has_keyboard_focus = focus_path && i == kbd_focus_index;
c.draw(info, g);
}
// reset pt. 2
info.child_has_keyboard_focus = focus_path;
self.inner.config_mut().pixel_pos = std::mem::replace(&mut info.pos, ppos);
}
/// recursively applies the function to all gui elements below and including this one
pub fn recursive_all<F: FnMut(&mut GuiElem)>(&mut self, f: &mut F) {
f(self);
for c in self.inner.children() {
c.recursive_all(f);
}
}
fn mouse_event<F: FnMut(&mut GuiElem) -> Option<Vec<GuiAction>>>(
&mut self,
condition: &mut F,
pos: Vec2,
) -> Option<Vec<GuiAction>> {
for c in &mut self.inner.children() {
if c.inner.config().enabled {
if c.inner.config().pixel_pos.contains(pos) {
if let Some(v) = c.mouse_event(condition, pos) {
return Some(v);
}
}
}
}
condition(self)
}
fn release_drag(
&mut self,
dragged: &mut Option<Dragging>,
pos: Vec2,
) -> Option<Vec<GuiAction>> {
self.mouse_event(
&mut |v| {
if v.inner.config().drag_target {
if let Some(d) = dragged.take() {
return Some(v.inner.dragged(d));
}
}
None
},
pos,
)
}
fn mouse_button(
&mut self,
button: MouseButton,
down: bool,
pos: Vec2,
) -> Option<Vec<GuiAction>> {
if down {
self.mouse_event(
&mut |v: &mut GuiElem| {
if v.inner.config().mouse_events {
match button {
MouseButton::Left => v.inner.config_mut().mouse_down.0 = true,
MouseButton::Middle => v.inner.config_mut().mouse_down.1 = true,
MouseButton::Right => v.inner.config_mut().mouse_down.2 = true,
MouseButton::Other(_) => {}
}
Some(v.inner.mouse_down(button))
} else {
None
}
},
pos,
)
} else {
let mut vec = vec![];
if let Some(a) = self.mouse_event(
&mut |v: &mut GuiElem| {
let down = v.inner.config().mouse_down;
if v.inner.config().mouse_events
&& ((button == MouseButton::Left && down.0)
|| (button == MouseButton::Middle && down.1)
|| (button == MouseButton::Right && down.2))
{
Some(v.inner.mouse_pressed(button))
} else {
None
}
},
pos,
) {
vec.extend(a);
};
self.recursive_all(&mut |v| {
if v.inner.config().mouse_events {
match button {
MouseButton::Left => v.inner.config_mut().mouse_down.0 = false,
MouseButton::Middle => v.inner.config_mut().mouse_down.1 = false,
MouseButton::Right => v.inner.config_mut().mouse_down.2 = false,
MouseButton::Other(_) => {}
}
vec.extend(v.inner.mouse_up(button));
}
});
Some(vec)
}
}
fn mouse_wheel(&mut self, diff: f32, pos: Vec2) -> Option<Vec<GuiAction>> {
self.mouse_event(
&mut |v| {
if v.inner.config().scroll_events {
Some(v.inner.mouse_wheel(diff))
} else {
None
}
},
pos,
)
}
fn keyboard_event<
F: FnOnce(&mut Self, &mut Vec<GuiAction>),
G: FnMut(&mut Self, &mut Vec<GuiAction>),
>(
&mut self,
f_focus: F,
mut f_watch: G,
) -> Vec<GuiAction> {
let mut o = vec![];
self.keyboard_event_inner(&mut Some(f_focus), &mut f_watch, &mut o, true);
o
}
fn keyboard_event_inner<
F: FnOnce(&mut Self, &mut Vec<GuiAction>),
G: FnMut(&mut Self, &mut Vec<GuiAction>),
>(
&mut self,
f_focus: &mut Option<F>,
f_watch: &mut G,
events: &mut Vec<GuiAction>,
focus: bool,
) {
f_watch(self, events);
let focus_index = self.inner.config().keyboard_focus_index;
for (i, child) in self.inner.children().enumerate() {
child.keyboard_event_inner(f_focus, f_watch, events, focus && i == focus_index);
}
if focus {
// we have focus and no child has consumed f_focus
if let Some(f) = f_focus.take() {
f(self, events)
}
}
}
fn keyboard_move_focus(&mut self, decrement: bool, refocus: bool) -> bool {
let mut focus_index = if refocus {
usize::MAX
} else {
self.inner.config().keyboard_focus_index
};
let allow_focus = self.inner.config().keyboard_events_focus;
let mut children = self.inner.children().collect::<Vec<_>>();
if focus_index == usize::MAX {
if decrement {
focus_index = children.len().saturating_sub(1);
} else {
focus_index = 0;
}
}
let mut changed = refocus;
let ok = loop {
if let Some(child) = children.get_mut(focus_index) {
if child.keyboard_move_focus(decrement, changed) {
break true;
} else {
changed = true;
if !decrement {
focus_index += 1;
} else {
focus_index = focus_index.wrapping_sub(1);
}
}
} else {
focus_index = usize::MAX;
break allow_focus && refocus;
}
};
self.inner.config_mut().keyboard_focus_index = focus_index;
ok
}
fn keyboard_reset_focus(&mut self) -> bool {
let mut index = usize::MAX;
for (i, c) in self.inner.children().enumerate() {
if c.keyboard_reset_focus() {
index = i;
break;
}
}
let wants = std::mem::replace(&mut self.inner.config_mut().request_keyboard_focus, false);
self.inner.config_mut().keyboard_focus_index = index;
index != usize::MAX || wants
}
}
pub fn adjust_area(outer: &Rectangle, rel_area: &Rectangle) -> Rectangle {
Rectangle::new(
adjust_pos(outer, rel_area.top_left()),
adjust_pos(outer, rel_area.bottom_right()),
)
}
pub fn adjust_pos(outer: &Rectangle, rel_pos: &Vec2) -> Vec2 {
Vec2::new(
outer.top_left().x + outer.width() * rel_pos.x,
outer.top_left().y + outer.height() * rel_pos.y,
)
}
impl Gui {
fn exec_gui_action(&mut self, action: GuiAction) {
match action {
GuiAction::Build(f) => {
let actions = f(&mut *self.database.lock().unwrap());
for action in actions {
self.exec_gui_action(action);
}
}
GuiAction::SendToServer(cmd) => {
if let Err(e) = cmd.to_bytes(&mut self.connection) {
eprintln!("Error sending command to server: {e}");
}
}
GuiAction::ResetKeyboardFocus => _ = self.gui.keyboard_reset_focus(),
GuiAction::SetDragging(d) => self.dragging = d,
GuiAction::SetLineHeight(h) => {
self.line_height = h;
self.gui
.recursive_all(&mut |e| e.inner.config_mut().redraw = true);
}
GuiAction::Do(mut f) => f(self),
GuiAction::Exit => _ = self.event_sender.send_event(GuiEvent::Exit),
GuiAction::SetIdle(v) => {
if let Some(gui) = self
.gui
.inner
.any_mut()
.downcast_mut::<WithFocusHotkey<GuiScreen>>()
{
if gui.inner.idle.0 != v {
gui.inner.idle = (v, Some(Instant::now()));
}
}
}
GuiAction::OpenSettings(v) => {
if let Some(gui) = self
.gui
.inner
.any_mut()
.downcast_mut::<WithFocusHotkey<GuiScreen>>()
{
if gui.inner.idle.0 {
gui.inner.idle = (false, Some(Instant::now()));
}
if gui.inner.settings.0 != v {
gui.inner.settings = (v, Some(Instant::now()));
}
}
}
GuiAction::OpenMain => {
if let Some(gui) = self
.gui
.inner
.any_mut()
.downcast_mut::<WithFocusHotkey<GuiScreen>>()
{
if gui.inner.idle.0 {
gui.inner.idle = (false, Some(Instant::now()));
}
if gui.inner.settings.0 {
gui.inner.settings = (false, Some(Instant::now()));
}
}
}
}
}
}
impl WindowHandler<GuiEvent> for Gui {
fn on_draw(&mut self, helper: &mut WindowHelper<GuiEvent>, graphics: &mut Graphics2D) {
let start = Instant::now();
graphics.draw_rectangle(
Rectangle::new(Vec2::ZERO, self.size.into_f32()),
Color::BLACK,
);
let mut dblock = self.database.lock().unwrap();
let mut info = DrawInfo {
actions: Vec::with_capacity(0),
pos: Rectangle::new(Vec2::ZERO, self.size.into_f32()),
database: &mut *dblock,
font: &self.font,
mouse_pos: self.mouse_pos,
helper: Some(helper),
has_keyboard_focus: false,
child_has_keyboard_focus: true,
line_height: self.line_height,
};
self.gui.draw(&mut info, graphics);
let actions = std::mem::replace(&mut info.actions, Vec::with_capacity(0));
if let Some((d, f)) = &mut self.dragging {
if let Some(f) = f {
f(&mut info, graphics);
} else {
match d {
Dragging::Artist(_) => graphics.draw_circle(
self.mouse_pos,
25.0,
Color::from_int_rgba(0, 100, 255, 100),
),
Dragging::Album(_) => graphics.draw_circle(
self.mouse_pos,
25.0,
Color::from_int_rgba(0, 100, 255, 100),
),
Dragging::Song(_) => graphics.draw_circle(
self.mouse_pos,
25.0,
Color::from_int_rgba(0, 100, 255, 100),
),
Dragging::Queue(_) => graphics.draw_circle(
self.mouse_pos,
25.0,
Color::from_int_rgba(100, 0, 255, 100),
),
}
}
}
drop(info);
drop(dblock);
for a in actions {
self.exec_gui_action(a);
}
// eprintln!(
// "fps <= {}",
// 1000 / self.last_draw.elapsed().as_millis().max(1)
// );
self.last_draw = start;
}
fn on_mouse_button_down(&mut self, helper: &mut WindowHelper<GuiEvent>, button: MouseButton) {
if let Some(a) = self.gui.mouse_button(button, true, self.mouse_pos.clone()) {
for a in a {
self.exec_gui_action(a)
}
}
helper.request_redraw();
}
fn on_mouse_button_up(&mut self, helper: &mut WindowHelper<GuiEvent>, button: MouseButton) {
if self.dragging.is_some() {
if let Some(a) = self.gui.release_drag(
&mut self.dragging.take().map(|v| v.0),
self.mouse_pos.clone(),
) {
for a in a {
self.exec_gui_action(a)
}
}
}
if let Some(a) = self.gui.mouse_button(button, false, self.mouse_pos.clone()) {
for a in a {
self.exec_gui_action(a)
}
}
helper.request_redraw();
}
fn on_mouse_wheel_scroll(
&mut self,
helper: &mut WindowHelper<GuiEvent>,
distance: speedy2d::window::MouseScrollDistance,
) {
let dist = match distance {
MouseScrollDistance::Pixels { y, .. } => (self.scroll_pixels_multiplier * y) as f32,
MouseScrollDistance::Lines { y, .. } => {
(self.scroll_lines_multiplier * y) as f32 * self.line_height
}
MouseScrollDistance::Pages { y, .. } => {
(self.scroll_pages_multiplier * y) as f32 * self.last_height
}
};
if let Some(a) = self.gui.mouse_wheel(dist, self.mouse_pos.clone()) {
for a in a {
self.exec_gui_action(a)
}
}
helper.request_redraw();
}
fn on_keyboard_char(&mut self, helper: &mut WindowHelper<GuiEvent>, unicode_codepoint: char) {
helper.request_redraw();
for a in self.gui.keyboard_event(
|e, a| {
if e.inner.config().keyboard_events_focus {
a.append(
&mut e
.inner
.char_focus(self.modifiers.clone(), unicode_codepoint),
);
}
},
|e, a| {
if e.inner.config().keyboard_events_watch {
a.append(
&mut e
.inner
.char_watch(self.modifiers.clone(), unicode_codepoint),
);
}
},
) {
self.exec_gui_action(a);
}
}
fn on_key_down(
&mut self,
helper: &mut WindowHelper<GuiEvent>,
virtual_key_code: Option<VirtualKeyCode>,
scancode: KeyScancode,
) {
helper.request_redraw();
if let Some(VirtualKeyCode::Tab) = virtual_key_code {
if !(self.modifiers.ctrl() || self.modifiers.alt() || self.modifiers.logo()) {
self.gui.keyboard_move_focus(self.modifiers.shift(), false);
}
}
for a in self.gui.keyboard_event(
|e, a| {
if e.inner.config().keyboard_events_focus {
a.append(&mut e.inner.key_focus(
self.modifiers.clone(),
true,
virtual_key_code,
scancode,
));
}
},
|e, a| {
if e.inner.config().keyboard_events_watch {
a.append(&mut e.inner.key_watch(
self.modifiers.clone(),
true,
virtual_key_code,
scancode,
));
}
},
) {
self.exec_gui_action(a);
}
}
fn on_key_up(
&mut self,
helper: &mut WindowHelper<GuiEvent>,
virtual_key_code: Option<VirtualKeyCode>,
scancode: KeyScancode,
) {
helper.request_redraw();
for a in self.gui.keyboard_event(
|e, a| {
if e.inner.config().keyboard_events_focus {
a.append(&mut e.inner.key_focus(
self.modifiers.clone(),
false,
virtual_key_code,
scancode,
));
}
},
|e, a| {
if e.inner.config().keyboard_events_watch {
a.append(&mut e.inner.key_watch(
self.modifiers.clone(),
false,
virtual_key_code,
scancode,
));
}
},
) {
self.exec_gui_action(a);
}
}
fn on_keyboard_modifiers_changed(
&mut self,
_helper: &mut WindowHelper<GuiEvent>,
state: ModifiersState,
) {
self.modifiers = state;
}
fn on_user_event(&mut self, helper: &mut WindowHelper<GuiEvent>, user_event: GuiEvent) {
match user_event {
GuiEvent::Refresh => helper.request_redraw(),
GuiEvent::UpdatedLibrary => {
self.gui.recursive_all(&mut |e| e.inner.updated_library());
helper.request_redraw();
}
GuiEvent::UpdatedQueue => {
self.gui.recursive_all(&mut |e| e.inner.updated_queue());
helper.request_redraw();
}
GuiEvent::Exit => helper.terminate_loop(),
}
}
fn on_mouse_move(&mut self, helper: &mut WindowHelper<GuiEvent>, position: Vec2) {
self.mouse_pos = position;
helper.request_redraw();
}
fn on_resize(&mut self, _helper: &mut WindowHelper<GuiEvent>, size_pixels: UVec2) {
self.size = size_pixels;
self.gui
.recursive_all(&mut |e| e.inner.config_mut().redraw = true);
}
}

459
musicdb-client/src/gui_base.rs Executable file
View File

@@ -0,0 +1,459 @@
use std::{sync::Arc, time::Instant};
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::MouseButton};
use crate::{
gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_text::Label,
};
/// A simple container for zero, one, or multiple child GuiElems. Can optionally fill the background with a color.
#[derive(Clone)]
pub struct Panel {
config: GuiElemCfg,
pub children: Vec<GuiElem>,
pub background: Option<Color>,
}
impl Panel {
pub fn new(config: GuiElemCfg, children: Vec<GuiElem>) -> Self {
Self {
config,
children,
background: None,
}
}
pub fn with_background(config: GuiElemCfg, children: Vec<GuiElem>, background: Color) -> Self {
Self {
config,
children,
background: Some(background),
}
}
}
impl GuiElemTrait for Panel {
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 DrawInfo, g: &mut speedy2d::Graphics2D) {
if let Some(c) = self.background {
g.draw_rectangle(info.pos.clone(), c);
}
}
}
#[derive(Clone)]
pub struct ScrollBox {
config: GuiElemCfg,
pub children: Vec<(GuiElem, f32)>,
pub size_unit: ScrollBoxSizeUnit,
pub scroll_target: f32,
pub scroll_display: f32,
height_bottom: f32,
/// 0.max(height_bottom - 1)
max_scroll: f32,
last_height_px: f32,
}
#[derive(Clone)]
pub enum ScrollBoxSizeUnit {
Relative,
Pixels,
}
impl ScrollBox {
pub fn new(
mut config: GuiElemCfg,
size_unit: ScrollBoxSizeUnit,
children: Vec<(GuiElem, f32)>,
) -> Self {
// config.redraw = true;
Self {
config: config.w_scroll(),
children,
size_unit,
scroll_target: 0.0,
scroll_display: 0.0,
/// the y-position of the bottom edge of the last element (i.e. the total height)
height_bottom: 0.0,
max_scroll: 0.0,
last_height_px: 0.0,
}
}
}
impl GuiElemTrait for ScrollBox {
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()
.rev()
.map(|(v, _)| v)
.skip_while(|v| v.inner.config().pos.bottom_right().y < 0.0)
.take_while(|v| v.inner.config().pos.top_left().y < 1.0),
)
}
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 DrawInfo, g: &mut speedy2d::Graphics2D) {
if self.config.pixel_pos.size() != info.pos.size() {
self.config.redraw = true;
}
// smooth scrolling animation
if self.scroll_target > self.max_scroll {
self.scroll_target = self.max_scroll;
} else if self.scroll_target < 0.0 {
self.scroll_target = 0.0;
}
self.scroll_display = 0.2 * self.scroll_target + 0.8 * self.scroll_display;
if self.scroll_display != self.scroll_target {
self.config.redraw = true;
if (self.scroll_display - self.scroll_target).abs() < 1.0 / info.pos.height() {
self.scroll_display = self.scroll_target;
} else if let Some(h) = &info.helper {
h.request_redraw();
}
}
// recalculate positions
if self.config.redraw {
self.config.redraw = false;
let mut y_pos = -self.scroll_display;
for (e, h) in self.children.iter_mut() {
let h_rel = self.size_unit.to_rel(*h, info.pos.height());
let y_rel = self.size_unit.to_rel(y_pos, info.pos.height());
if y_rel + h_rel >= 0.0 && y_rel <= 1.0 {
let cfg = e.inner.config_mut();
cfg.enabled = true;
cfg.pos = Rectangle::new(
Vec2::new(cfg.pos.top_left().x, 0.0f32.max(y_rel)),
Vec2::new(cfg.pos.bottom_right().x, 1.0f32.min(y_rel + h_rel)),
);
} else {
e.inner.config_mut().enabled = false;
}
y_pos += *h;
}
self.height_bottom = y_pos + self.scroll_display;
self.max_scroll =
0.0f32.max(self.height_bottom - self.size_unit.from_rel(0.75, info.pos.height()));
}
}
fn mouse_wheel(&mut self, diff: f32) -> Vec<crate::gui::GuiAction> {
self.scroll_target = (self.scroll_target
- self.size_unit.from_abs(diff as f32, self.last_height_px))
.max(0.0);
Vec::with_capacity(0)
}
}
impl ScrollBoxSizeUnit {
fn to_rel(&self, val: f32, draw_height: f32) -> f32 {
match self {
Self::Relative => val,
Self::Pixels => val / draw_height,
}
}
fn from_rel(&self, val: f32, draw_height: f32) -> f32 {
match self {
Self::Relative => val,
Self::Pixels => val * draw_height,
}
}
fn from_abs(&self, val: f32, draw_height: f32) -> f32 {
match self {
Self::Relative => val / draw_height,
Self::Pixels => val,
}
}
}
#[derive(Clone)]
pub struct Button {
config: GuiElemCfg,
pub children: Vec<GuiElem>,
action: Arc<dyn Fn(&Self) -> Vec<GuiAction> + 'static>,
}
impl Button {
/// automatically adds w_mouse to config
pub fn new<F: Fn(&Self) -> Vec<GuiAction> + 'static>(
config: GuiElemCfg,
action: F,
children: Vec<GuiElem>,
) -> Self {
Self {
config: config.w_mouse(),
children,
action: Arc::new(action),
}
}
}
impl GuiElemTrait for Button {
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 mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left {
(self.action)(self)
} else {
vec![]
}
}
fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
let mouse_down = self.config.mouse_down.0;
let contains = info.pos.contains(info.mouse_pos);
g.draw_rectangle(
info.pos.clone(),
if mouse_down && contains {
Color::from_rgb(0.25, 0.25, 0.25)
} else if contains || mouse_down {
Color::from_rgb(0.15, 0.15, 0.15)
} else {
Color::from_rgb(0.1, 0.1, 0.1)
},
);
}
}
#[derive(Clone)]
pub struct Slider {
pub config: GuiElemCfg,
pub children: Vec<GuiElem>,
pub slider_pos: Rectangle,
pub min: f64,
pub max: f64,
pub val: f64,
val_changed: bool,
pub val_changed_subs: Vec<bool>,
/// if true, the display should be visible.
pub display: bool,
/// if Some, the display is in a transition period.
/// you can set this to None to indicate that the transition has finished, but this is not required.
pub display_since: Option<Instant>,
pub on_update: Arc<dyn Fn(&mut Self, &mut DrawInfo)>,
}
impl Slider {
/// returns true if the value of the slider has changed since the last time this function was called.
/// this is usually used by the closure responsible for directly handling updates. if you wish to check for changes
/// from outside, push a `false` to `val_changed_subs` and remember your index.
/// when the value changes, this will be set to `true`. don't forget to reset it to `false` again if you find it set to `true`,
/// or your code will run every time.
pub fn val_changed(&mut self) -> bool {
if self.val_changed {
self.val_changed = false;
true
} else {
false
}
}
pub fn val_changed_peek(&self) -> bool {
self.val_changed
}
pub fn new<F: Fn(&mut Self, &mut DrawInfo) + 'static>(
config: GuiElemCfg,
slider_pos: Rectangle,
min: f64,
max: f64,
val: f64,
children: Vec<GuiElem>,
on_update: F,
) -> Self {
Self {
config: config.w_mouse().w_scroll(),
children,
slider_pos,
min,
max,
val,
val_changed: true,
val_changed_subs: vec![],
display: false,
display_since: None,
on_update: Arc::new(on_update),
}
}
pub fn new_labeled<F: Fn(&mut Self, &mut Label, &mut DrawInfo) + 'static>(
config: GuiElemCfg,
min: f64,
max: f64,
val: f64,
mktext: F,
) -> Self {
Self::new(
config,
Rectangle::new(Vec2::ZERO, Vec2::new(1.0, 1.0)),
min,
max,
val,
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
String::new(),
Color::WHITE,
// Some(Color::from_int_rgba(0, 0, 0, 150)),
None,
Vec2::new(0.5, 1.0),
))],
move |s, i| {
if s.display || s.display_since.is_some() {
let mut label = s.children.pop().unwrap();
if let Some(l) = label.inner.any_mut().downcast_mut::<Label>() {
let display_state = if let Some(since) =
s.display_since.map(|v| v.elapsed().as_secs_f64() / 0.2)
{
if since >= 1.0 {
s.display_since = None;
if s.display {
1.0
} else {
0.0
}
} else {
if let Some(h) = &i.helper {
h.request_redraw();
}
s.config.redraw = true;
if s.display {
since
} else {
1.0 - since
}
}
} else {
1.0
};
let display_state =
(1.0 - (1.0 - display_state) * (1.0 - display_state)) as _;
if display_state == 0.0 {
l.config_mut().enabled = false;
} else {
l.pos.x = ((s.val - s.min) / (s.max - s.min)) as _;
*l.content.color() = Color::from_rgba(0.8, 0.8, 0.8, display_state);
let cfg = l.config_mut();
cfg.enabled = true;
let label_height = i.line_height / i.pos.height();
cfg.pos = Rectangle::from_tuples(
(0.05, 1.0 - label_height - display_state),
(0.95, 1.0 - display_state),
);
mktext(s, l, i);
}
}
s.children.push(label);
}
},
)
}
}
impl GuiElemTrait for Slider {
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 DrawInfo, g: &mut speedy2d::Graphics2D) {
if self.display != (self.config.mouse_down.0 || info.pos.contains(info.mouse_pos)) {
self.display = !self.display;
self.display_since = Some(Instant::now());
self.config.redraw = true;
}
let dot_size = (info.pos.height() * 0.9).min(info.pos.width() * 0.25);
let y_mid_line = 0.5 * (info.pos.top_left().y + info.pos.bottom_right().y);
let line_radius = dot_size * 0.25;
let line_pos = Rectangle::from_tuples(
(info.pos.top_left().x + dot_size, y_mid_line - line_radius),
(
info.pos.bottom_right().x - dot_size,
y_mid_line + line_radius,
),
);
let line_left = line_pos.top_left().x;
let line_width = line_pos.width();
if self.config.mouse_down.0 {
self.val = self.min
+ (self.max - self.min)
* 1.0f64.min(0.0f64.max(
(info.mouse_pos.x - line_pos.top_left().x) as f64 / line_pos.width() as f64,
));
self.val_changed = true;
for v in &mut self.val_changed_subs {
*v = true;
}
self.config.redraw = true;
}
let line_color = Color::from_int_rgb(50, 50, 100);
g.draw_circle(
Vec2::new(line_pos.top_left().x, y_mid_line),
line_radius,
line_color,
);
g.draw_circle(
Vec2::new(line_pos.bottom_right().x, y_mid_line),
line_radius,
line_color,
);
g.draw_rectangle(line_pos, line_color);
g.draw_circle(
Vec2::new(
line_left
+ (line_width as f64 * (self.val - self.min) / (self.max - self.min)) as f32,
y_mid_line,
),
0.5 * dot_size,
Color::CYAN,
);
if self.config.redraw {
self.config.redraw = false;
(Arc::clone(&self.on_update))(self, info);
}
}
}

454
musicdb-client/src/gui_library.rs Executable file
View File

@@ -0,0 +1,454 @@
use musicdb_lib::data::{database::Database, AlbumId, ArtistId, SongId};
use regex::{Regex, RegexBuilder};
use speedy2d::{
color::Color,
dimen::Vec2,
shape::Rectangle,
window::{MouseButton, VirtualKeyCode},
};
use crate::{
gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_base::ScrollBox,
gui_text::{Label, TextField},
gui_wrappers::WithFocusHotkey,
};
#[derive(Clone)]
pub struct LibraryBrowser {
config: GuiElemCfg,
pub children: Vec<GuiElem>,
search_artist: String,
search_artist_regex: Option<Regex>,
search_album: String,
search_album_regex: Option<Regex>,
search_song: String,
search_song_regex: Option<Regex>,
}
fn search_regex_new(pat: &str) -> Result<Regex, regex::Error> {
RegexBuilder::new(pat)
.unicode(true)
.case_insensitive(true)
.build()
}
impl LibraryBrowser {
pub fn new(config: GuiElemCfg) -> Self {
let search_artist = TextField::new(
GuiElemCfg::at(Rectangle::from_tuples((0.01, 0.01), (0.45, 0.05))),
"artist".to_string(),
Color::GRAY,
Color::WHITE,
);
let search_album = TextField::new(
GuiElemCfg::at(Rectangle::from_tuples((0.55, 0.01), (0.99, 0.05))),
"album".to_string(),
Color::GRAY,
Color::WHITE,
);
let search_song = WithFocusHotkey::new_ctrl(
VirtualKeyCode::F,
TextField::new(
GuiElemCfg::at(Rectangle::from_tuples((0.01, 0.06), (0.99, 0.1))),
"song".to_string(),
Color::GRAY,
Color::WHITE,
),
);
let library_scroll_box = ScrollBox::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.1), (1.0, 1.0))),
crate::gui_base::ScrollBoxSizeUnit::Pixels,
vec![],
);
Self {
config,
children: vec![
GuiElem::new(search_artist),
GuiElem::new(search_album),
GuiElem::new(search_song),
GuiElem::new(library_scroll_box),
],
search_artist: String::new(),
search_artist_regex: None,
search_album: String::new(),
search_album_regex: None,
search_song: String::new(),
search_song_regex: None,
}
}
}
impl GuiElemTrait for LibraryBrowser {
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 DrawInfo, _g: &mut speedy2d::Graphics2D) {
let mut search_changed = false;
{
let v = &mut self.children[0].try_as_mut::<TextField>().unwrap().children[0]
.try_as_mut::<Label>()
.unwrap()
.content;
if self.search_artist != *v.get_text() {
search_changed = true;
self.search_artist = v.get_text().clone();
self.search_artist_regex = search_regex_new(&self.search_artist).ok();
*v.color() = if self.search_artist_regex.is_some() {
Color::WHITE
} else {
Color::RED
};
}
}
{
let v = &mut self.children[1].try_as_mut::<TextField>().unwrap().children[0]
.try_as_mut::<Label>()
.unwrap()
.content;
if self.search_album != *v.get_text() {
search_changed = true;
self.search_album = v.get_text().clone();
self.search_album_regex = search_regex_new(&self.search_album).ok();
*v.color() = if self.search_album_regex.is_some() {
Color::WHITE
} else {
Color::RED
};
}
}
{
let v = &mut self.children[2]
.try_as_mut::<WithFocusHotkey<TextField>>()
.unwrap()
.inner
.children[0]
.try_as_mut::<Label>()
.unwrap()
.content;
if self.search_song != *v.get_text() {
search_changed = true;
self.search_song = v.get_text().clone();
self.search_song_regex = search_regex_new(&self.search_song).ok();
*v.color() = if self.search_song_regex.is_some() {
Color::WHITE
} else {
Color::RED
};
}
}
if self.config.redraw || search_changed || info.pos.size() != self.config.pixel_pos.size() {
self.config.redraw = false;
self.update_list(&info.database, info.line_height);
}
}
fn updated_library(&mut self) {
self.config.redraw = true;
}
}
impl LibraryBrowser {
fn update_list(&mut self, db: &Database, line_height: f32) {
let song_height = line_height;
let artist_height = song_height * 3.0;
let album_height = song_height * 2.0;
// sort artists by name
let mut artists = db.artists().iter().collect::<Vec<_>>();
artists.sort_by_key(|v| &v.1.name);
let mut gui_elements = vec![];
for (artist_id, artist) in artists {
if self.search_artist.is_empty()
|| self
.search_artist_regex
.as_ref()
.is_some_and(|regex| regex.is_match(&artist.name))
{
let mut artist_gui = Some((
GuiElem::new(ListArtist::new(
GuiElemCfg::default(),
*artist_id,
artist.name.clone(),
)),
artist_height,
));
for album_id in &artist.albums {
if let Some(album) = db.albums().get(album_id) {
if self.search_album.is_empty()
|| self
.search_album_regex
.as_ref()
.is_some_and(|regex| regex.is_match(&album.name))
{
let mut album_gui = Some((
GuiElem::new(ListAlbum::new(
GuiElemCfg::default(),
*album_id,
album.name.clone(),
)),
album_height,
));
for song_id in &album.songs {
if let Some(song) = db.songs().get(song_id) {
if self.search_song.is_empty()
|| self
.search_song_regex
.as_ref()
.is_some_and(|regex| regex.is_match(&song.title))
{
if let Some(g) = artist_gui.take() {
gui_elements.push(g);
}
if let Some(g) = album_gui.take() {
gui_elements.push(g);
}
gui_elements.push((
GuiElem::new(ListSong::new(
GuiElemCfg::default(),
*song_id,
song.title.clone(),
)),
song_height,
));
}
}
}
}
}
}
}
}
let scroll_box = self.children[3].try_as_mut::<ScrollBox>().unwrap();
scroll_box.children = gui_elements;
scroll_box.config_mut().redraw = true;
}
}
#[derive(Clone)]
struct ListArtist {
config: GuiElemCfg,
id: ArtistId,
children: Vec<GuiElem>,
mouse_pos: Vec2,
}
impl ListArtist {
pub fn new(config: GuiElemCfg, id: ArtistId, name: String) -> Self {
let label = Label::new(
GuiElemCfg::default(),
name,
Color::from_int_rgb(81, 24, 125),
None,
Vec2::new(0.0, 0.5),
);
Self {
config: config.w_mouse(),
id,
children: vec![GuiElem::new(label)],
mouse_pos: Vec2::ZERO,
}
}
}
impl GuiElemTrait for ListArtist {
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 DrawInfo, _g: &mut speedy2d::Graphics2D) {
self.mouse_pos = Vec2::new(
info.mouse_pos.x - self.config.pixel_pos.top_left().x,
info.mouse_pos.y - self.config.pixel_pos.top_left().y,
);
}
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left {
let mouse_pos = self.mouse_pos;
let w = self.config.pixel_pos.width();
let h = self.config.pixel_pos.height();
let mut el = GuiElem::new(self.clone());
vec![GuiAction::SetDragging(Some((
Dragging::Artist(self.id),
Some(Box::new(move |i, g| {
let sw = i.pos.width();
let sh = i.pos.height();
let x = (i.mouse_pos.x - mouse_pos.x) / sw;
let y = (i.mouse_pos.y - mouse_pos.y) / sh;
el.inner.config_mut().pos =
Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh));
el.draw(i, g)
})),
)))]
} else {
vec![]
}
}
}
#[derive(Clone)]
struct ListAlbum {
config: GuiElemCfg,
id: AlbumId,
children: Vec<GuiElem>,
mouse_pos: Vec2,
}
impl ListAlbum {
pub fn new(config: GuiElemCfg, id: AlbumId, name: String) -> Self {
let label = Label::new(
GuiElemCfg::default(),
name,
Color::from_int_rgb(8, 61, 47),
None,
Vec2::new(0.0, 0.5),
);
Self {
config: config.w_mouse(),
id,
children: vec![GuiElem::new(label)],
mouse_pos: Vec2::ZERO,
}
}
}
impl GuiElemTrait for ListAlbum {
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 DrawInfo, _g: &mut speedy2d::Graphics2D) {
self.mouse_pos = Vec2::new(
info.mouse_pos.x - self.config.pixel_pos.top_left().x,
info.mouse_pos.y - self.config.pixel_pos.top_left().y,
);
}
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left {
let mouse_pos = self.mouse_pos;
let w = self.config.pixel_pos.width();
let h = self.config.pixel_pos.height();
let mut el = GuiElem::new(self.clone());
vec![GuiAction::SetDragging(Some((
Dragging::Album(self.id),
Some(Box::new(move |i, g| {
let sw = i.pos.width();
let sh = i.pos.height();
let x = (i.mouse_pos.x - mouse_pos.x) / sw;
let y = (i.mouse_pos.y - mouse_pos.y) / sh;
el.inner.config_mut().pos =
Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh));
el.draw(i, g)
})),
)))]
} else {
vec![]
}
}
}
#[derive(Clone)]
struct ListSong {
config: GuiElemCfg,
id: SongId,
children: Vec<GuiElem>,
mouse_pos: Vec2,
}
impl ListSong {
pub fn new(config: GuiElemCfg, id: SongId, name: String) -> Self {
let label = Label::new(
GuiElemCfg::default(),
name,
Color::from_int_rgb(175, 175, 175),
None,
Vec2::new(0.0, 0.5),
);
Self {
config: config.w_mouse(),
id,
children: vec![GuiElem::new(label)],
mouse_pos: Vec2::ZERO,
}
}
}
impl GuiElemTrait for ListSong {
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 DrawInfo, _g: &mut speedy2d::Graphics2D) {
self.mouse_pos = Vec2::new(
info.mouse_pos.x - self.config.pixel_pos.top_left().x,
info.mouse_pos.y - self.config.pixel_pos.top_left().y,
);
}
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left {
let mouse_pos = self.mouse_pos;
let w = self.config.pixel_pos.width();
let h = self.config.pixel_pos.height();
let mut el = GuiElem::new(self.clone());
vec![GuiAction::SetDragging(Some((
Dragging::Song(self.id),
Some(Box::new(move |i, g| {
let sw = i.pos.width();
let sh = i.pos.height();
let x = (i.mouse_pos.x - mouse_pos.x) / sw;
let y = (i.mouse_pos.y - mouse_pos.y) / sh;
el.inner.config_mut().pos =
Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh));
el.draw(i, g)
})),
)))]
} else {
vec![]
}
}
}

View File

@@ -0,0 +1,238 @@
use musicdb_lib::{
data::{queue::QueueContent, SongId},
server::Command,
};
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::MouseButton};
use crate::{
gui::{adjust_area, adjust_pos, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_text::Label,
};
#[derive(Clone)]
pub struct CurrentSong {
config: GuiElemCfg,
children: Vec<GuiElem>,
prev_song: Option<SongId>,
}
impl CurrentSong {
pub fn new(config: GuiElemCfg) -> Self {
Self {
config,
children: vec![
GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.5))),
"".to_owned(),
Color::from_int_rgb(180, 180, 210),
None,
Vec2::new(0.1, 1.0),
)),
GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.5), (0.5, 1.0))),
"".to_owned(),
Color::from_int_rgb(120, 120, 120),
None,
Vec2::new(0.3, 0.0),
)),
],
prev_song: None,
}
}
}
impl GuiElemTrait for CurrentSong {
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) {
let song = if let Some(v) = info.database.queue.get_current() {
if let QueueContent::Song(song) = v.content() {
if Some(*song) == self.prev_song {
// same song as before
return;
} else {
Some(*song)
}
} else if self.prev_song.is_none() {
// no song, nothing in queue
return;
} else {
None
}
} else if self.prev_song.is_none() {
// no song, nothing in queue
return;
} else {
None
};
if self.prev_song != song {
self.config.redraw = true;
self.prev_song = song;
}
if self.config.redraw {
self.config.redraw = false;
let (name, subtext) = if let Some(song) = song {
if let Some(song) = info.database.get_song(&song) {
let sub = match (
song.artist
.as_ref()
.and_then(|id| info.database.artists().get(id)),
song.album
.as_ref()
.and_then(|id| info.database.albums().get(id)),
) {
(None, None) => String::new(),
(Some(artist), None) => format!("by {}", artist.name),
(None, Some(album)) => {
if let Some(artist) = album
.artist
.as_ref()
.and_then(|id| info.database.artists().get(id))
{
format!("on {} by {}", album.name, artist.name)
} else {
format!("on {}", album.name)
}
}
(Some(artist), Some(album)) => {
format!("by {} on {}", artist.name, album.name)
}
};
(song.title.clone(), sub)
} else {
(
"< song not in db >".to_owned(),
"maybe restart the client to resync the database?".to_owned(),
)
}
} else {
(String::new(), String::new())
};
*self.children[0]
.try_as_mut::<Label>()
.unwrap()
.content
.text() = name;
*self.children[1]
.try_as_mut::<Label>()
.unwrap()
.content
.text() = subtext;
}
}
}
#[derive(Clone)]
pub struct PlayPauseToggle {
config: GuiElemCfg,
children: Vec<GuiElem>,
playing_target: bool,
playing_waiting_for_change: bool,
}
impl PlayPauseToggle {
/// automatically adds w_mouse to config
pub fn new(config: GuiElemCfg, playing: bool) -> Self {
Self {
config: config.w_mouse(),
children: vec![],
playing_target: playing,
playing_waiting_for_change: false,
}
}
}
impl GuiElemTrait for PlayPauseToggle {
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.playing_waiting_for_change {
if info.database.playing == self.playing_target {
self.playing_waiting_for_change = false;
}
} else {
// not waiting for change, update if the value changes
self.playing_target = info.database.playing;
}
let pos = if info.pos.width() > info.pos.height() {
let a = 0.5 * info.pos.height();
let m = 0.5 * (info.pos.top_left().x + info.pos.bottom_right().x);
Rectangle::new(
Vec2::new(m - a, info.pos.top_left().y),
Vec2::new(m + a, info.pos.bottom_right().y),
)
} else {
let a = 0.5 * info.pos.width();
let m = 0.5 * (info.pos.top_left().y + info.pos.bottom_right().y);
Rectangle::new(
Vec2::new(info.pos.top_left().x, m - a),
Vec2::new(info.pos.bottom_right().x, m + a),
)
};
if self.playing_target {
g.draw_triangle(
[
adjust_pos(&pos, &Vec2::new(0.25, 0.25)),
adjust_pos(&pos, &Vec2::new(0.75, 0.5)),
adjust_pos(&pos, &Vec2::new(0.25, 0.75)),
],
if self.playing_waiting_for_change {
Color::GRAY
} else {
Color::GREEN
},
)
} else {
g.draw_rectangle(
adjust_area(&pos, &Rectangle::from_tuples((0.25, 0.25), (0.75, 0.75))),
if self.playing_waiting_for_change {
Color::RED
} else {
Color::GRAY
},
);
}
}
fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
if !self.playing_waiting_for_change {
self.playing_target = !self.playing_target;
self.playing_waiting_for_change = true;
vec![GuiAction::SendToServer(if self.playing_target {
Command::Resume
} else {
Command::Pause
})]
} else {
vec![]
}
}
}

689
musicdb-client/src/gui_queue.rs Executable file
View File

@@ -0,0 +1,689 @@
use musicdb_lib::{
data::{
database::Database,
queue::{Queue, QueueContent},
song::Song,
AlbumId, ArtistId,
},
server::Command,
};
use speedy2d::{
color::Color,
dimen::Vec2,
shape::Rectangle,
window::{ModifiersState, MouseButton, VirtualKeyCode},
};
use crate::{
gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_base::ScrollBox,
gui_text::Label,
};
#[derive(Clone)]
pub struct QueueViewer {
config: GuiElemCfg,
children: Vec<GuiElem>,
}
impl QueueViewer {
pub fn new(config: GuiElemCfg) -> Self {
let queue_scroll_box = ScrollBox::new(
GuiElemCfg::default(),
crate::gui_base::ScrollBoxSizeUnit::Pixels,
vec![(
GuiElem::new(Label::new(
GuiElemCfg::default(),
"loading...".to_string(),
Color::DARK_GRAY,
None,
Vec2::new(0.5, 0.5),
)),
1.0,
)],
);
Self {
config,
children: vec![
GuiElem::new(queue_scroll_box),
GuiElem::new(QueueEmptySpaceDragHandler::new(GuiElemCfg::default())),
],
}
}
}
impl GuiElemTrait for QueueViewer {
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 DrawInfo, _g: &mut speedy2d::Graphics2D) {
if self.config.redraw || info.pos.size() != self.config.pixel_pos.size() {
self.config.redraw = false;
let mut c = vec![];
queue_gui(
&info.database.queue,
&info.database,
0.0,
0.02,
info.line_height,
&mut c,
vec![],
true,
);
let mut scroll_box = self.children[0].try_as_mut::<ScrollBox>().unwrap();
scroll_box.children = c;
scroll_box.config_mut().redraw = true;
}
}
fn updated_queue(&mut self) {
self.config.redraw = true;
}
}
fn queue_gui(
queue: &Queue,
db: &Database,
depth: f32,
depth_inc_by: f32,
line_height: f32,
target: &mut Vec<(GuiElem, f32)>,
path: Vec<usize>,
current: bool,
) {
let cfg = GuiElemCfg::at(Rectangle::from_tuples((depth, 0.0), (1.0, 1.0)));
match queue.content() {
QueueContent::Song(id) => {
if let Some(s) = db.songs().get(id) {
target.push((
GuiElem::new(QueueSong::new(cfg, path, s.clone(), current)),
line_height,
));
}
}
QueueContent::Folder(ia, q, _) => {
target.push((
GuiElem::new(QueueFolder::new(cfg, path.clone(), queue.clone(), current)),
line_height * 0.67,
));
for (i, q) in q.iter().enumerate() {
let mut p = path.clone();
p.push(i);
queue_gui(
q,
db,
depth + depth_inc_by,
depth_inc_by,
line_height,
target,
p,
current && *ia == i,
);
}
}
}
}
#[derive(Clone)]
struct QueueEmptySpaceDragHandler {
config: GuiElemCfg,
children: Vec<GuiElem>,
}
impl QueueEmptySpaceDragHandler {
pub fn new(config: GuiElemCfg) -> Self {
Self {
config: config.w_drag_target(),
children: vec![],
}
}
}
impl GuiElemTrait for QueueEmptySpaceDragHandler {
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 dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> {
dragged_add_to_queue(dragged, |q| Command::QueueAdd(vec![], q))
}
}
fn generic_queue_draw(
info: &mut DrawInfo,
path: &Vec<usize>,
mouse: &mut bool,
copy_on_mouse_down: bool,
) -> bool {
if *mouse && !info.pos.contains(info.mouse_pos) {
*mouse = false;
if !copy_on_mouse_down {
info.actions
.push(GuiAction::SendToServer(Command::QueueRemove(path.clone())));
}
true
} else {
false
}
}
#[derive(Clone)]
struct QueueSong {
config: GuiElemCfg,
children: Vec<GuiElem>,
path: Vec<usize>,
song: Song,
current: bool,
mouse: bool,
mouse_pos: Vec2,
copy: bool,
copy_on_mouse_down: bool,
}
impl QueueSong {
pub fn new(config: GuiElemCfg, path: Vec<usize>, song: Song, current: bool) -> Self {
Self {
config: config.w_mouse().w_keyboard_watch().w_drag_target(),
children: vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
song.title.clone(),
if current {
Color::from_int_rgb(194, 76, 178)
} else {
Color::from_int_rgb(120, 76, 194)
},
None,
Vec2::new(0.0, 0.5),
))],
path,
song,
current,
mouse: false,
mouse_pos: Vec2::ZERO,
copy: false,
copy_on_mouse_down: false,
}
}
}
impl GuiElemTrait for QueueSong {
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 mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left {
self.mouse = true;
self.copy_on_mouse_down = self.copy;
}
vec![]
}
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left {
self.mouse = false;
vec![GuiAction::SendToServer(Command::QueueGoto(
self.path.clone(),
))]
} else {
vec![]
}
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
if !self.mouse {
self.mouse_pos = Vec2::new(
info.mouse_pos.x - self.config.pixel_pos.top_left().x,
info.mouse_pos.y - self.config.pixel_pos.top_left().y,
);
}
if generic_queue_draw(info, &self.path, &mut self.mouse, self.copy_on_mouse_down) {
let mouse_pos = self.mouse_pos;
let w = self.config.pixel_pos.width();
let h = self.config.pixel_pos.height();
let mut el = GuiElem::new(self.clone());
info.actions.push(GuiAction::SetDragging(Some((
Dragging::Queue(QueueContent::Song(self.song.id).into()),
Some(Box::new(move |i, g| {
let sw = i.pos.width();
let sh = i.pos.height();
let x = (i.mouse_pos.x - mouse_pos.x) / sw;
let y = (i.mouse_pos.y - mouse_pos.y) / sh;
el.inner.config_mut().pos =
Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh));
el.draw(i, g)
})),
))));
}
}
fn key_watch(
&mut self,
modifiers: ModifiersState,
_down: bool,
_key: Option<VirtualKeyCode>,
_scan: speedy2d::window::KeyScancode,
) -> Vec<GuiAction> {
self.copy = modifiers.ctrl();
vec![]
}
fn dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> {
let mut p = self.path.clone();
dragged_add_to_queue(dragged, move |q| {
if let Some(i) = p.pop() {
Command::QueueInsert(p, i, q)
} else {
Command::QueueAdd(p, q)
}
})
}
}
#[derive(Clone)]
struct QueueFolder {
config: GuiElemCfg,
children: Vec<GuiElem>,
path: Vec<usize>,
queue: Queue,
current: bool,
mouse: bool,
mouse_pos: Vec2,
copy: bool,
copy_on_mouse_down: bool,
}
impl QueueFolder {
pub fn new(config: GuiElemCfg, path: Vec<usize>, queue: Queue, current: bool) -> Self {
Self {
config: if path.is_empty() {
config
} else {
config.w_mouse().w_keyboard_watch()
}
.w_drag_target(),
children: vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
match queue.content() {
QueueContent::Folder(_, q, n) => format!(
"{} ({})",
if path.is_empty() && n.is_empty() {
"Queue"
} else {
n
},
q.len()
),
_ => "[???]".to_string(),
},
Color::from_int_rgb(52, 132, 50),
None,
Vec2::new(0.0, 0.5),
))],
path,
queue,
current,
mouse: false,
mouse_pos: Vec2::ZERO,
copy: false,
copy_on_mouse_down: false,
}
}
}
impl GuiElemTrait for QueueFolder {
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 DrawInfo, _g: &mut speedy2d::Graphics2D) {
if !self.mouse {
self.mouse_pos = Vec2::new(
info.mouse_pos.x - self.config.pixel_pos.top_left().x,
info.mouse_pos.y - self.config.pixel_pos.top_left().y,
);
}
if generic_queue_draw(info, &self.path, &mut self.mouse, self.copy_on_mouse_down) {
let mouse_pos = self.mouse_pos;
let w = self.config.pixel_pos.width();
let h = self.config.pixel_pos.height();
let mut el = GuiElem::new(self.clone());
info.actions.push(GuiAction::SetDragging(Some((
Dragging::Queue(self.queue.clone()),
Some(Box::new(move |i, g| {
let sw = i.pos.width();
let sh = i.pos.height();
let x = (i.mouse_pos.x - mouse_pos.x) / sw;
let y = (i.mouse_pos.y - mouse_pos.y) / sh;
el.inner.config_mut().pos =
Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh));
el.draw(i, g)
})),
))));
}
}
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left {
self.mouse = true;
self.copy_on_mouse_down = self.copy;
}
vec![]
}
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left {
self.mouse = false;
vec![GuiAction::SendToServer(Command::QueueGoto(
self.path.clone(),
))]
} else {
vec![]
}
}
fn key_watch(
&mut self,
modifiers: ModifiersState,
_down: bool,
_key: Option<VirtualKeyCode>,
_scan: speedy2d::window::KeyScancode,
) -> Vec<GuiAction> {
self.copy = modifiers.ctrl();
vec![]
}
fn dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> {
let p = self.path.clone();
dragged_add_to_queue(dragged, move |q| Command::QueueAdd(p, q))
}
}
fn dragged_add_to_queue<F: FnOnce(Queue) -> Command + 'static>(
dragged: Dragging,
f: F,
) -> Vec<GuiAction> {
match dragged {
Dragging::Artist(id) => {
vec![GuiAction::Build(Box::new(move |db| {
if let Some(q) = add_to_queue_artist_by_id(id, db) {
vec![GuiAction::SendToServer(f(q))]
} else {
vec![]
}
}))]
}
Dragging::Album(id) => {
vec![GuiAction::Build(Box::new(move |db| {
if let Some(q) = add_to_queue_album_by_id(id, db) {
vec![GuiAction::SendToServer(f(q))]
} else {
vec![]
}
}))]
}
Dragging::Song(id) => {
let q = QueueContent::Song(id).into();
vec![GuiAction::SendToServer(f(q))]
}
Dragging::Queue(q) => {
vec![GuiAction::SendToServer(f(q))]
}
}
}
fn add_to_queue_album_by_id(id: AlbumId, db: &Database) -> Option<Queue> {
if let Some(album) = db.albums().get(&id) {
Some(
QueueContent::Folder(
0,
album
.songs
.iter()
.map(|id| QueueContent::Song(*id).into())
.collect(),
album.name.clone(),
)
.into(),
)
} else {
None
}
}
fn add_to_queue_artist_by_id(id: ArtistId, db: &Database) -> Option<Queue> {
if let Some(artist) = db.artists().get(&id) {
Some(
QueueContent::Folder(
0,
artist
.albums
.iter()
.filter_map(|id| add_to_queue_album_by_id(*id, db))
.collect(),
artist.name.clone(),
)
.into(),
)
} else {
None
}
}
// use musicdb_lib::{
// data::{
// database::Database,
// queue::{Queue, QueueContent},
// AlbumId, ArtistId,
// },
// server::Command,
// };
// use speedy2d::{
// color::Color,
// dimen::Vec2,
// font::{TextLayout, TextOptions},
// };
// use crate::gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait};
// pub struct QueueViewer {
// config: GuiElemCfg,
// children: Vec<GuiElem>,
// /// 0.0 = bottom
// scroll: f32,
// }
// impl QueueViewer {
// pub fn new(config: GuiElemCfg) -> Self {
// Self {
// config: config.w_drag_target(),
// children: vec![],
// scroll: 0.0,
// }
// }
// }
// impl GuiElemTrait for QueueViewer {
// 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 draw(&mut self, info: &DrawInfo, g: &mut speedy2d::Graphics2D) {
// g.draw_rectangle(info.pos.clone(), Color::from_rgb(0.0, 0.1, 0.0));
// let queue_height = info.database.queue.len();
// let start_y_pos = info.pos.bottom_right().y
// + (self.scroll - queue_height as f32) * info.queue_song_height;
// let mut skip = 0;
// let limit = queue_height.saturating_sub(self.scroll.floor() as usize + skip);
// self.draw_queue(
// &info.database.queue,
// &mut skip,
// &mut 0,
// limit,
// &mut Vec2::new(info.pos.top_left().x, start_y_pos),
// info.pos.width(),
// info,
// g,
// );
// }
// fn dragged(&mut self, dragged: Dragging) -> Vec<crate::gui::GuiAction> {
// match dragged {
// Dragging::Song(id) => vec![GuiAction::SendToServer(Command::QueueAdd(
// vec![],
// QueueContent::Song(id).into(),
// ))],
// Dragging::Album(id) => vec![GuiAction::Build(Box::new(move |db| {
// if let Some(q) = Self::add_to_queue_album_by_id(id, db) {
// vec![GuiAction::SendToServer(Command::QueueAdd(vec![], q))]
// } else {
// vec![]
// }
// }))],
// Dragging::Artist(id) => vec![GuiAction::Build(Box::new(move |db| {
// if let Some(q) = Self::add_to_queue_artist_by_id(id, db) {
// vec![GuiAction::SendToServer(Command::QueueAdd(vec![], q))]
// } else {
// vec![]
// }
// }))],
// _ => vec![],
// }
// }
// }
// impl QueueViewer {
// fn add_to_queue_album_by_id(id: AlbumId, db: &Database) -> Option<Queue> {
// if let Some(album) = db.albums().get(&id) {
// Some(
// QueueContent::Folder(
// 0,
// album
// .songs
// .iter()
// .map(|id| QueueContent::Song(*id).into())
// .collect(),
// album.name.clone(),
// )
// .into(),
// )
// } else {
// None
// }
// }
// fn add_to_queue_artist_by_id(id: ArtistId, db: &Database) -> Option<Queue> {
// if let Some(artist) = db.artists().get(&id) {
// Some(
// QueueContent::Folder(
// 0,
// artist
// .albums
// .iter()
// .filter_map(|id| Self::add_to_queue_album_by_id(*id, db))
// .collect(),
// artist.name.clone(),
// )
// .into(),
// )
// } else {
// None
// }
// }
// }
// const INDENT_PX: f32 = 8.0;
// impl QueueViewer {
// fn draw_queue(
// &mut self,
// queue: &Queue,
// skip: &mut usize,
// drawn: &mut usize,
// limit: usize,
// top_left: &mut Vec2,
// width: f32,
// info: &DrawInfo,
// g: &mut speedy2d::Graphics2D,
// ) {
// // eprintln!("[queue: {} : {}/{}]", *skip, *drawn, limit);
// match queue.content() {
// QueueContent::Song(id) => {
// if *skip == 0 {
// if *drawn < limit {
// *drawn += 1;
// let text = if let Some(song) = info.database.get_song(id) {
// song.title.clone()
// } else {
// format!("< {id} >")
// };
// let height = info
// .font
// .layout_text(&text, 1.0, TextOptions::new())
// .height();
// g.draw_text_cropped(
// top_left.clone(),
// info.pos.clone(),
// Color::from_int_rgb(112, 41, 99),
// &info.font.layout_text(
// &text,
// 0.75 * info.queue_song_height / height,
// TextOptions::new(),
// ),
// );
// top_left.y += info.queue_song_height;
// }
// } else {
// *skip -= 1;
// }
// }
// QueueContent::Folder(index, vec, _name) => {
// top_left.x += INDENT_PX;
// for v in vec {
// self.draw_queue(v, skip, drawn, limit, top_left, width - INDENT_PX, info, g);
// }
// top_left.x -= INDENT_PX;
// }
// }
// }
// }

290
musicdb-client/src/gui_screen.rs Executable file
View File

@@ -0,0 +1,290 @@
use std::time::{Duration, Instant};
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D};
use crate::{
gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_base::{Button, Panel},
gui_library::LibraryBrowser,
gui_playback::{CurrentSong, PlayPauseToggle},
gui_queue::QueueViewer,
gui_settings::Settings,
gui_text::Label,
};
/// calculates f(p) (f(x) = 3x^2 - 2x^3)):
/// f(0) = 0
/// f(0.5) = 0.5
/// f(1) = 1
/// f'(0) = f'(1) = 0
/// -> smooth animation, fast to calculate
pub fn transition(p: f32) -> f32 {
3.0 * p * p - 2.0 * p * p * p
}
#[derive(Clone)]
pub struct GuiScreen {
config: GuiElemCfg,
/// 0: StatusBar / Idle display
/// 1: Settings
/// 2: Panel for Main view
children: Vec<GuiElem>,
pub idle: (bool, Option<Instant>),
pub settings: (bool, Option<Instant>),
pub last_interaction: Instant,
idle_timeout: Option<f64>,
pub prev_mouse_pos: Vec2,
}
impl GuiScreen {
fn i_statusbar() -> usize {
0
}
pub fn new(
config: GuiElemCfg,
line_height: f32,
scroll_sensitivity_pixels: f64,
scroll_sensitivity_lines: f64,
scroll_sensitivity_pages: f64,
) -> Self {
Self {
config: config.w_keyboard_watch(),
children: vec![
GuiElem::new(StatusBar::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.9), (1.0, 1.0))),
true,
)),
GuiElem::new(Settings::new(
GuiElemCfg::default().disabled(),
line_height,
scroll_sensitivity_pixels,
scroll_sensitivity_lines,
scroll_sensitivity_pages,
)),
GuiElem::new(Panel::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.9))),
vec![
GuiElem::new(Button::new(
GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.1))),
|_| vec![GuiAction::OpenSettings(true)],
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
"Settings".to_string(),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
))],
)),
GuiElem::new(Button::new(
GuiElemCfg::at(Rectangle::from_tuples((0.75, 0.0), (1.0, 0.1))),
|_| vec![GuiAction::Exit],
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
"Exit".to_string(),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
))],
)),
GuiElem::new(LibraryBrowser::new(GuiElemCfg::at(Rectangle::from_tuples(
(0.0, 0.0),
(0.5, 1.0),
)))),
GuiElem::new(QueueViewer::new(GuiElemCfg::at(Rectangle::from_tuples(
(0.5, 0.1),
(1.0, 1.0),
)))),
],
)),
],
idle: (false, None),
settings: (false, None),
last_interaction: Instant::now(),
idle_timeout: Some(60.0),
prev_mouse_pos: Vec2::ZERO,
}
}
fn get_prog(v: &mut (bool, Option<Instant>), seconds: f32) -> f32 {
if let Some(since) = &mut v.1 {
let prog = since.elapsed().as_secs_f32() / seconds;
if prog >= 1.0 {
v.1 = None;
if v.0 {
1.0
} else {
0.0
}
} else {
if v.0 {
prog
} else {
1.0 - prog
}
}
} else if v.0 {
1.0
} else {
0.0
}
}
fn not_idle(&mut self) {
self.last_interaction = Instant::now();
if self.idle.0 {
self.idle = (false, Some(Instant::now()));
}
}
fn idle_check(&mut self) {
if !self.idle.0 {
if let Some(dur) = &self.idle_timeout {
if self.last_interaction.elapsed().as_secs_f64() > *dur {
self.idle = (true, Some(Instant::now()));
}
}
}
}
}
impl GuiElemTrait for GuiScreen {
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 key_watch(
&mut self,
_modifiers: speedy2d::window::ModifiersState,
_down: bool,
_key: Option<speedy2d::window::VirtualKeyCode>,
_scan: speedy2d::window::KeyScancode,
) -> Vec<GuiAction> {
self.not_idle();
vec![]
}
fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) {
// idle stuff
if self.prev_mouse_pos != info.mouse_pos {
self.prev_mouse_pos = info.mouse_pos;
self.not_idle();
} else if !self.idle.0 && self.config.pixel_pos.size() != info.pos.size() {
// resizing prevents idle, but doesn't un-idle
self.not_idle();
}
self.idle_check();
// request_redraw for animations
if self.idle.1.is_some() | self.settings.1.is_some() {
if let Some(h) = &info.helper {
h.request_redraw()
}
}
// animations: idle
if self.idle.1.is_some() {
let seconds = if self.idle.0 { 2.0 } else { 0.5 };
let p1 = Self::get_prog(&mut self.idle, seconds);
if !self.idle.0 || self.idle.1.is_none() {
if let Some(h) = &info.helper {
h.set_cursor_visible(!self.idle.0);
for el in self.children.iter_mut().skip(1) {
el.inner.config_mut().enabled = !self.idle.0;
}
}
}
let p = transition(p1);
self.children[0].inner.config_mut().pos =
Rectangle::from_tuples((0.0, 0.9 - 0.9 * p), (1.0, 1.0));
self.children[0]
.inner
.any_mut()
.downcast_mut::<StatusBar>()
.unwrap()
.idle_mode = p1;
}
// animations: settings
if self.settings.1.is_some() {
let p1 = Self::get_prog(&mut self.settings, 0.3);
let p = transition(p1);
let cfg = self.children[1].inner.config_mut();
cfg.enabled = p > 0.0;
cfg.pos = Rectangle::from_tuples((0.0, 0.9 - 0.9 * p), (1.0, 0.9));
}
// set idle timeout (only when settings are open)
if self.settings.0 || self.settings.1.is_some() {
self.idle_timeout = self.children[1]
.inner
.any()
.downcast_ref::<Settings>()
.unwrap()
.get_timeout_val();
}
}
}
#[derive(Clone)]
pub struct StatusBar {
config: GuiElemCfg,
children: Vec<GuiElem>,
idle_mode: f32,
}
impl StatusBar {
pub fn new(config: GuiElemCfg, playing: bool) -> Self {
Self {
config,
children: vec![
GuiElem::new(CurrentSong::new(GuiElemCfg::at(Rectangle::new(
Vec2::ZERO,
Vec2::new(0.8, 1.0),
)))),
GuiElem::new(PlayPauseToggle::new(
GuiElemCfg::at(Rectangle::from_tuples((0.85, 0.0), (0.95, 1.0))),
false,
)),
GuiElem::new(Panel::with_background(
GuiElemCfg::default(),
vec![],
Color::BLACK,
)),
],
idle_mode: 0.0,
}
}
}
impl GuiElemTrait for StatusBar {
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 DrawInfo, g: &mut Graphics2D) {
if self.idle_mode < 1.0 {
g.draw_line(
info.pos.top_left(),
info.pos.top_right(),
2.0,
Color::from_rgba(1.0, 1.0, 1.0, 1.0 - self.idle_mode),
);
}
}
}

View File

@@ -0,0 +1,316 @@
use std::{ops::DerefMut, time::Duration};
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D};
use crate::{
gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_base::{Button, Panel, ScrollBox, Slider},
gui_text::Label,
};
#[derive(Clone)]
pub struct Settings {
pub config: GuiElemCfg,
pub children: Vec<GuiElem>,
}
impl Settings {
pub fn new(
mut config: GuiElemCfg,
line_height: f32,
scroll_sensitivity_pixels: f64,
scroll_sensitivity_lines: f64,
scroll_sensitivity_pages: f64,
) -> Self {
config.redraw = true;
Self {
config,
children: vec![
GuiElem::new(ScrollBox::new(
GuiElemCfg::default(),
crate::gui_base::ScrollBoxSizeUnit::Pixels,
vec![
(
GuiElem::new(Button::new(
GuiElemCfg::at(Rectangle::from_tuples((0.75, 0.0), (1.0, 1.0))),
|btn| vec![GuiAction::OpenSettings(false)],
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
"Back".to_string(),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
))],
)),
0.0,
),
(
GuiElem::new(Panel::new(
GuiElemCfg::default(),
vec![
GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples(
(0.0, 0.0),
(0.33, 1.0),
)),
"Settings panel opacity".to_string(),
Color::WHITE,
None,
Vec2::new(0.9, 0.5),
)),
GuiElem::new({
let mut s = Slider::new_labeled(
GuiElemCfg::at(Rectangle::from_tuples(
(0.33, 0.0),
(1.0, 1.0),
)),
0.0,
1.0,
1.0,
|slider, label, _info| {
if slider.val_changed() {
*label.content.text() =
format!("{:.0}%", slider.val * 100.0);
}
},
);
s.val_changed_subs.push(false);
s
}),
],
)),
0.0,
),
(
GuiElem::new(Panel::new(
GuiElemCfg::default(),
vec![
GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples(
(0.0, 0.0),
(0.33, 1.0),
)),
"Line Height / Text Size".to_string(),
Color::WHITE,
None,
Vec2::new(0.9, 0.5),
)),
GuiElem::new(Slider::new_labeled(
GuiElemCfg::at(Rectangle::from_tuples(
(0.33, 0.0),
(1.0, 1.0),
)),
16.0,
80.0,
line_height as _,
|slider, label, info| {
if slider.val_changed() {
*label.content.text() =
format!("line height: {:.0}", slider.val);
let h = slider.val as _;
info.actions.push(GuiAction::SetLineHeight(h));
}
},
)),
],
)),
0.0,
),
(
GuiElem::new(Panel::new(
GuiElemCfg::default(),
vec![
GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples(
(0.0, 0.0),
(0.33, 1.0),
)),
"Scroll Sensitivity (lines)".to_string(),
Color::WHITE,
None,
Vec2::new(0.9, 0.5),
)),
GuiElem::new(Slider::new_labeled(
GuiElemCfg::at(Rectangle::from_tuples(
(0.33, 0.0),
(1.0, 1.0),
)),
0.0,
12.0,
scroll_sensitivity_lines,
|slider, label, info| {
if slider.val_changed() {
*label.content.text() =
format!("{:.1}", slider.val);
let h = slider.val as _;
info.actions.push(GuiAction::Do(Box::new(
move |gui| gui.scroll_lines_multiplier = h,
)));
}
},
)),
],
)),
0.0,
),
(
GuiElem::new(Panel::new(
GuiElemCfg::default(),
vec![
GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples(
(0.0, 0.0),
(0.33, 1.0),
)),
"Idle time".to_string(),
Color::WHITE,
None,
Vec2::new(0.9, 0.5),
)),
GuiElem::new(Slider::new_labeled(
GuiElemCfg::at(Rectangle::from_tuples(
(0.33, 0.0),
(1.0, 1.0),
)),
0.0,
(60.0f64 * 60.0 * 6.0).sqrt(),
60.0f64.sqrt(),
|slider, label, info| {
if slider.val_changed() {
*label.content.text() = if slider.val > 0.0 {
let mut s = String::new();
let seconds = (slider.val * slider.val) as u64;
let hours = seconds / 3600;
let seconds = seconds % 3600;
let minutes = seconds / 60;
let seconds = seconds % 60;
if hours > 0 {
s = hours.to_string();
s.push_str("h ");
}
if minutes > 0 || hours > 0 && seconds > 0 {
s.push_str(&minutes.to_string());
s.push_str("m ");
}
if hours == 0
&& minutes < 10
&& (seconds > 0 || minutes == 0)
{
s.push_str(&seconds.to_string());
s.push_str("s");
} else if s.ends_with(" ") {
s.pop();
}
s
} else {
"no timeout".to_string()
}
};
let h = slider.val as _;
if slider.val_changed() {
info.actions.push(GuiAction::Do(Box::new(
move |gui| gui.scroll_lines_multiplier = h,
)));
}
},
)),
],
)),
0.0,
),
],
)),
GuiElem::new(Panel::with_background(
GuiElemCfg::default().w_mouse(),
vec![],
Color::BLACK,
)),
],
}
}
pub fn get_timeout_val(&self) -> Option<f64> {
let v = self.children[0]
.inner
.any()
.downcast_ref::<ScrollBox>()
.unwrap()
.children[4]
.0
.inner
.any()
.downcast_ref::<Panel>()
.unwrap()
.children[1]
.inner
.any()
.downcast_ref::<Slider>()
.unwrap()
.val;
if v > 0.0 {
Some(v * v)
} else {
None
}
}
}
impl GuiElemTrait for Settings {
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 clone_gui(&self) -> Box<dyn GuiElemTrait> {
Box::new(self.clone())
}
fn any(&self) -> &dyn std::any::Any {
self
}
fn any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut Graphics2D) {
let (rest, background) = self.children.split_at_mut(1);
let scrollbox = rest[0].inner.any_mut().downcast_mut::<ScrollBox>().unwrap();
let settings_opacity_slider = scrollbox.children[1]
.0
.inner
.any_mut()
.downcast_mut::<Panel>()
.unwrap()
.children[1]
.inner
.any_mut()
.downcast_mut::<Slider>()
.unwrap();
if settings_opacity_slider.val_changed_subs[0] {
settings_opacity_slider.val_changed_subs[0] = false;
let color = background[0]
.inner
.any_mut()
.downcast_mut::<Panel>()
.unwrap()
.background
.as_mut()
.unwrap();
*color = Color::from_rgba(
color.r(),
color.g(),
color.b(),
settings_opacity_slider.val as _,
);
}
if self.config.redraw {
self.config.redraw = false;
for (i, (_, h)) in scrollbox.children.iter_mut().enumerate() {
*h = if i == 0 {
info.line_height * 2.0
} else {
info.line_height
};
}
}
}
}

226
musicdb-client/src/gui_text.rs Executable file
View File

@@ -0,0 +1,226 @@
use std::rc::Rc;
use speedy2d::{
color::Color,
dimen::Vec2,
font::{FormattedTextBlock, TextLayout, TextOptions},
shape::Rectangle,
window::{ModifiersState, MouseButton},
};
use crate::gui::{GuiAction, GuiElem, GuiElemCfg, GuiElemTrait};
#[derive(Clone)]
pub struct Label {
config: GuiElemCfg,
children: Vec<GuiElem>,
pub content: Content,
pub pos: Vec2,
}
#[derive(Clone)]
pub struct Content {
text: String,
color: Color,
background: Option<Color>,
formatted: Option<Rc<FormattedTextBlock>>,
}
impl Content {
pub fn get_text(&self) -> &String {
&self.text
}
pub fn get_color(&self) -> &Color {
&self.color
}
/// causes text layout reset
pub fn text(&mut self) -> &mut String {
self.formatted = None;
&mut self.text
}
pub fn color(&mut self) -> &mut Color {
&mut self.color
}
}
impl Label {
pub fn new(
config: GuiElemCfg,
text: String,
color: Color,
background: Option<Color>,
pos: Vec2,
) -> Self {
Self {
config,
children: vec![],
content: Content {
text,
color,
background,
formatted: None,
},
pos,
}
}
}
impl GuiElemTrait for Label {
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.pixel_pos.size() != info.pos.size() {
// resize
self.content.formatted = None;
}
let text = if let Some(text) = &self.content.formatted {
text
} else {
let l = info
.font
.layout_text(&self.content.text, 1.0, TextOptions::new());
let l = info.font.layout_text(
&self.content.text,
(info.pos.width() / l.width()).min(info.pos.height() / l.height()),
TextOptions::new(),
);
self.content.formatted = Some(l);
self.content.formatted.as_ref().unwrap()
};
let top_left = Vec2::new(
info.pos.top_left().x + self.pos.x * (info.pos.width() - text.width()),
info.pos.top_left().y + self.pos.y * (info.pos.height() - text.height()),
);
if let Some(bg) = self.content.background {
g.draw_rectangle(
Rectangle::new(
top_left,
Vec2::new(top_left.x + text.width(), top_left.y + text.height()),
),
bg,
);
}
g.draw_text(top_left, self.content.color, text);
}
}
// TODO! this, but requires keyboard events first
/// a single-line text fields for users to type text into.
#[derive(Clone)]
pub struct TextField {
config: GuiElemCfg,
pub children: Vec<GuiElem>,
}
impl TextField {
pub fn new(config: GuiElemCfg, hint: String, color_hint: Color, color_input: Color) -> Self {
Self {
config: config.w_mouse().w_keyboard_focus(),
children: vec![
GuiElem::new(Label::new(
GuiElemCfg::default(),
String::new(),
color_input,
None,
Vec2::new(0.0, 0.5),
)),
GuiElem::new(Label::new(
GuiElemCfg::default(),
hint,
color_hint,
None,
Vec2::new(0.0, 0.5),
)),
],
}
}
}
impl GuiElemTrait for TextField {
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) {
let (t, c) = if info.has_keyboard_focus {
(3.0, Color::WHITE)
} else {
(1.0, Color::GRAY)
};
g.draw_line(info.pos.top_left(), info.pos.top_right(), t, c);
g.draw_line(info.pos.bottom_left(), info.pos.bottom_right(), t, c);
g.draw_line(info.pos.top_left(), info.pos.bottom_left(), t, c);
g.draw_line(info.pos.top_right(), info.pos.bottom_right(), t, c);
}
fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
self.config.request_keyboard_focus = true;
vec![GuiAction::ResetKeyboardFocus]
}
fn char_focus(&mut self, modifiers: ModifiersState, key: char) -> Vec<GuiAction> {
if !(modifiers.ctrl() || modifiers.alt() || modifiers.logo()) && !key.is_control() {
let content = &mut self.children[0].try_as_mut::<Label>().unwrap().content;
let was_empty = content.get_text().is_empty();
content.text().push(key);
if was_empty {
self.children[1].inner.config_mut().enabled = false;
}
}
vec![]
}
fn key_focus(
&mut self,
modifiers: ModifiersState,
down: bool,
key: Option<speedy2d::window::VirtualKeyCode>,
_scan: speedy2d::window::KeyScancode,
) -> Vec<GuiAction> {
if down
&& !(modifiers.alt() || modifiers.logo())
&& key == Some(speedy2d::window::VirtualKeyCode::Backspace)
{
let content = &mut self.children[0].try_as_mut::<Label>().unwrap().content;
if !content.get_text().is_empty() {
if modifiers.ctrl() {
for s in [true, false, true] {
while !content.get_text().is_empty()
&& content.get_text().ends_with(' ') == s
{
content.text().pop();
}
}
} else {
content.text().pop();
}
if content.get_text().is_empty() {
self.children[1].inner.config_mut().enabled = true;
}
}
}
vec![]
}
}

View File

@@ -0,0 +1,147 @@
use speedy2d::{
window::{MouseButton, VirtualKeyCode},
Graphics2D,
};
use crate::gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait};
#[derive(Clone)]
pub struct WithFocusHotkey<T: GuiElemTrait + Clone> {
pub inner: T,
/// 4 * (ignore, pressed): 10 or 11 -> doesn't matter, 01 -> must be pressed, 00 -> must not be pressed
/// logo alt shift ctrl
pub modifiers: u8,
pub key: VirtualKeyCode,
}
impl<T: GuiElemTrait + Clone> WithFocusHotkey<T> {
/// unlike noshift, this ignores the shift modifier
pub fn new_key(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b1000, key, inner)
}
/// requires the key to be pressed without any modifiers
pub fn new_noshift(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0, key, inner)
}
pub fn new_shift(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b0100, key, inner)
}
pub fn new_ctrl(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b01, key, inner)
}
pub fn new_ctrl_shift(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b0101, key, inner)
}
pub fn new_alt(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b010000, key, inner)
}
pub fn new_alt_shift(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b010100, key, inner)
}
pub fn new_ctrl_alt(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b010001, key, inner)
}
pub fn new_ctrl_alt_shift(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b010101, key, inner)
}
pub fn new(modifiers: u8, key: VirtualKeyCode, mut inner: T) -> WithFocusHotkey<T> {
inner.config_mut().keyboard_events_watch = true;
WithFocusHotkey {
inner,
modifiers,
key,
}
}
}
impl<T: Clone + 'static> GuiElemTrait for WithFocusHotkey<T>
where
T: GuiElemTrait,
{
fn config(&self) -> &GuiElemCfg {
self.inner.config()
}
fn config_mut(&mut self) -> &mut GuiElemCfg {
self.inner.config_mut()
}
fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> {
self.inner.children()
}
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 DrawInfo, g: &mut Graphics2D) {
self.inner.draw(info, g)
}
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
self.inner.mouse_down(button)
}
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
self.inner.mouse_up(button)
}
fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
self.inner.mouse_pressed(button)
}
fn mouse_wheel(&mut self, diff: f32) -> Vec<GuiAction> {
self.inner.mouse_wheel(diff)
}
fn char_watch(
&mut self,
modifiers: speedy2d::window::ModifiersState,
key: char,
) -> Vec<GuiAction> {
self.inner.char_watch(modifiers, key)
}
fn char_focus(
&mut self,
modifiers: speedy2d::window::ModifiersState,
key: char,
) -> Vec<GuiAction> {
self.inner.char_focus(modifiers, key)
}
fn key_watch(
&mut self,
modifiers: speedy2d::window::ModifiersState,
down: bool,
key: Option<speedy2d::window::VirtualKeyCode>,
scan: speedy2d::window::KeyScancode,
) -> Vec<GuiAction> {
let hotkey = down == false
&& key.is_some_and(|v| v == self.key)
&& (self.modifiers & 0b10 == 1 || (self.modifiers & 0b01 == 1) == modifiers.ctrl())
&& (self.modifiers & 0b1000 == 1
|| (self.modifiers & 0b0100 == 1) == modifiers.shift())
&& (self.modifiers & 0b100000 == 1
|| (self.modifiers & 0b010000 == 1) == modifiers.alt())
&& (self.modifiers & 0b10000000 == 1
|| (self.modifiers & 0b01000000 == 1) == modifiers.logo());
let mut o = self.inner.key_watch(modifiers, down, key, scan);
if hotkey {
self.config_mut().request_keyboard_focus = true;
o.push(GuiAction::ResetKeyboardFocus);
}
o
}
fn key_focus(
&mut self,
modifiers: speedy2d::window::ModifiersState,
down: bool,
key: Option<speedy2d::window::VirtualKeyCode>,
scan: speedy2d::window::KeyScancode,
) -> Vec<GuiAction> {
self.inner.key_focus(modifiers, down, key, scan)
}
fn dragged(&mut self, dragged: crate::gui::Dragging) -> Vec<GuiAction> {
self.inner.dragged(dragged)
}
fn updated_library(&mut self) {
self.inner.updated_library()
}
fn updated_queue(&mut self) {
self.inner.updated_queue()
}
}

436
musicdb-client/src/main.rs Executable file
View File

@@ -0,0 +1,436 @@
use std::{
eprintln, fs,
net::{SocketAddr, TcpStream},
path::PathBuf,
sync::{Arc, Mutex},
thread,
time::Duration,
};
use gui::GuiEvent;
use musicdb_lib::{
data::{
album::Album, artist::Artist, database::Database, queue::QueueContent, song::Song,
DatabaseLocation, GeneralData,
},
load::ToFromBytes,
player::Player,
server::Command,
};
#[cfg(feature = "speedy2d")]
mod gui;
#[cfg(feature = "speedy2d")]
mod gui_base;
#[cfg(feature = "speedy2d")]
mod gui_library;
#[cfg(feature = "speedy2d")]
mod gui_playback;
#[cfg(feature = "speedy2d")]
mod gui_queue;
#[cfg(feature = "speedy2d")]
mod gui_screen;
#[cfg(feature = "speedy2d")]
mod gui_settings;
#[cfg(feature = "speedy2d")]
mod gui_text;
#[cfg(feature = "speedy2d")]
mod gui_wrappers;
enum Mode {
Cli,
Gui,
SyncPlayer,
FillDb,
}
fn main() {
let mut args = std::env::args().skip(1);
let mode = match args.next().as_ref().map(|v| v.trim()) {
Some("cli") => Mode::Cli,
Some("gui") => Mode::Gui,
Some("syncplayer") => Mode::SyncPlayer,
Some("filldb") => Mode::FillDb,
_ => {
println!("Run with argument <cli/gui/syncplayer/filldb>!");
return;
}
};
let addr = args.next().unwrap_or("127.0.0.1:26314".to_string());
let mut con = TcpStream::connect(addr.parse::<SocketAddr>().unwrap()).unwrap();
let database = Arc::new(Mutex::new(Database::new_clientside()));
#[cfg(feature = "speedy2d")]
let update_gui_sender: Arc<Mutex<Option<speedy2d::window::UserEventSender<GuiEvent>>>> =
Arc::new(Mutex::new(None));
#[cfg(feature = "speedy2d")]
let sender = Arc::clone(&update_gui_sender);
let wants_player = matches!(mode, Mode::SyncPlayer);
let con_thread = {
let database = Arc::clone(&database);
let mut con = con.try_clone().unwrap();
// this is all you need to keep the db in sync
thread::spawn(move || {
let mut player = if wants_player {
Some(Player::new().unwrap())
} else {
None
};
loop {
if let Some(player) = &mut player {
let mut db = database.lock().unwrap();
if !db.lib_directory.as_os_str().is_empty() {
player.update(&mut db);
}
}
let update = Command::from_bytes(&mut con).unwrap();
if let Some(player) = &mut player {
player.handle_command(&update);
}
database.lock().unwrap().apply_command(update);
#[cfg(feature = "speedy2d")]
if let Some(v) = &*update_gui_sender.lock().unwrap() {
v.send_event(GuiEvent::Refresh).unwrap();
}
}
})
};
match mode {
Mode::Cli => {
Looper {
con: &mut con,
database: &database,
}
.cmd_loop();
}
Mode::Gui => {
#[cfg(feature = "speedy2d")]
{
let occasional_refresh_sender = Arc::clone(&sender);
thread::spawn(move || loop {
std::thread::sleep(Duration::from_secs(1));
if let Some(v) = &*occasional_refresh_sender.lock().unwrap() {
v.send_event(GuiEvent::Refresh).unwrap();
}
});
gui::main(database, con, sender)
};
}
Mode::SyncPlayer => {
con_thread.join().unwrap();
}
Mode::FillDb => {
// wait for init
let dir = loop {
let db = database.lock().unwrap();
if !db.lib_directory.as_os_str().is_empty() {
break db.lib_directory.clone();
}
drop(db);
std::thread::sleep(Duration::from_millis(300));
};
eprintln!("
WARN: This will add all audio files in the lib-dir to the library, even if they were already added!
lib-dir: {:?}
If you really want to continue, type Yes.", dir);
let mut line = String::new();
std::io::stdin().read_line(&mut line).unwrap();
if line.trim().to_lowercase() == "yes" {
for artist in fs::read_dir(&dir)
.expect("reading lib-dir")
.filter_map(|v| v.ok())
{
if let Ok(albums) = fs::read_dir(artist.path()) {
let artist_name = artist.file_name().to_string_lossy().into_owned();
let mut artist_id = None;
for album in albums.filter_map(|v| v.ok()) {
if let Ok(songs) = fs::read_dir(album.path()) {
let album_name = album.file_name().to_string_lossy().into_owned();
let mut album_id = None;
let mut songs: Vec<_> = songs.filter_map(|v| v.ok()).collect();
songs.sort_unstable_by_key(|v| v.file_name());
for song in songs {
match song.path().extension().map(|v| v.to_str()) {
Some(Some(
"mp3" | "wav" | "wma" | "aac" | "flac" | "m4a" | "m4p"
| "ogg" | "oga" | "mogg" | "opus" | "tta",
)) => {
println!("> {:?}", song.path());
let song_name =
song.file_name().to_string_lossy().into_owned();
println!(
" {} - {} - {}",
song_name, artist_name, album_name
);
// get artist id
let artist_id = if let Some(v) = artist_id {
v
} else {
let mut adding_artist = false;
loop {
let db = database.lock().unwrap();
let artists = db
.artists()
.iter()
.filter(|(_, v)| v.name == artist_name)
.collect::<Vec<_>>();
if artists.len() > 1 {
eprintln!("Choosing the first of {} artists named {}.", artists.len(), artist_name);
}
if let Some((id, _)) = artists.first() {
artist_id = Some(**id);
break **id;
} else {
drop(db);
if !adding_artist {
adding_artist = true;
Command::AddArtist(Artist {
id: 0,
name: artist_name.clone(),
cover: None,
albums: vec![],
singles: vec![],
general: GeneralData::default(),
})
.to_bytes(&mut con)
.expect(
"sending AddArtist to db failed",
);
}
std::thread::sleep(Duration::from_millis(
300,
));
};
}
};
// get album id
let album_id = if let Some(v) = album_id {
v
} else {
let mut adding_album = false;
loop {
let db = database.lock().unwrap();
let albums = db
.artists()
.get(&artist_id)
.expect("artist_id not valid (bug)")
.albums
.iter()
.filter_map(|v| {
Some((v, db.albums().get(&v)?))
})
.filter(|(_, v)| v.name == album_name)
.collect::<Vec<_>>();
if albums.len() > 1 {
eprintln!("Choosing the first of {} albums named {} by the artist {}.", albums.len(), album_name, artist_name);
}
if let Some((id, _)) = albums.first() {
album_id = Some(**id);
break **id;
} else {
drop(db);
if !adding_album {
adding_album = true;
Command::AddAlbum(Album {
id: 0,
name: album_name.clone(),
artist: Some(artist_id),
cover: None,
songs: vec![],
general: GeneralData::default(),
})
.to_bytes(&mut con)
.expect("sending AddAlbum to db failed");
}
std::thread::sleep(Duration::from_millis(
300,
));
};
}
};
Command::AddSong(Song::new(
DatabaseLocation {
rel_path: PathBuf::from(artist.file_name())
.join(album.file_name())
.join(song.file_name()),
},
song_name,
Some(album_id),
Some(artist_id),
vec![],
None,
))
.to_bytes(&mut con)
.expect("sending AddSong to db failed");
}
_ => {}
}
}
}
}
}
}
}
}
}
}
struct Looper<'a> {
pub con: &'a mut TcpStream,
pub database: &'a Arc<Mutex<Database>>,
}
impl<'a> Looper<'a> {
pub fn cmd_loop(&mut self) {
loop {
println!();
let line = self.read_line(" > enter a command (help for help)");
let line = line.trim();
match line {
"resume" => Command::Resume,
"pause" => Command::Pause,
"stop" => Command::Stop,
"next" => Command::NextSong,
"set-lib-dir" => {
let line = self.read_line("Enter the new (absolute) library directory, or leave empty to abort");
if !line.is_empty() {
Command::SetLibraryDirectory(line.into())
} else {
continue;
}
},
"add-song" => {
let song = Song {
id: 0,
location: self.read_line("The songs file is located, relative to the library root, at...").into(),
title: self.read_line("The songs title is..."),
album: self.read_line_ido("The song is part of the album with the id... (empty for None)"),
artist: self.read_line_ido("The song is made by the artist with the id... (empty for None)"),
more_artists: accumulate(|| self.read_line_ido("The song is made with support by other artist, one of which has the id... (will ask repeatedly; leave empty once done)")),
cover: self.read_line_ido("The song should use the cover with the id... (empty for None - will default to album or artist cover, if available)"),
general: GeneralData::default(),
cached_data: Arc::new(Mutex::new(None)),
};
println!("You are about to add the following song to the database:");
println!(" + {song}");
if self.read_line("Are you sure? (type 'yes' to continue)").to_lowercase().trim() == "yes" {
Command::AddSong(song)
} else {
println!("[-] Aborted - no event will be sent to the database.");
continue;
}
},
"update-song" => {
let song_id = self.read_line_id("The ID of the song is...");
if let Some(mut song) = self.database.lock().unwrap().get_song(&song_id).cloned() {
println!("You are now editing the song {song}.");
loop {
match self.read_line("What do you want to edit? (title/album/artist/location or done)").to_lowercase().trim() {
"done" => break,
"title" => {
println!("prev: '{}'", song.title);
song.title = self.read_line("");
}
"album" => {
println!("prev: '{}'", song.album.map_or(String::new(), |v| v.to_string()));
song.album = self.read_line_ido("");
}
"artist" => {
println!("prev: '{}'", song.artist.map_or(String::new(), |v| v.to_string()));
song.artist = self.read_line_ido("");
}
"location" => {
println!("prev: '{:?}'", song.location);
song.location = self.read_line("").into();
}
_ => println!("[-] must be title/album/artist/location or done"),
}
}
println!("You are about to update the song:");
println!(" + {song}");
if self.read_line("Are you sure? (type 'yes' to continue)").to_lowercase().trim() == "yes" {
Command::ModifySong(song)
} else {
println!("[-] Aborted - no event will be sent to the database.");
continue;
}
} else {
println!("[-] No song with that ID found, aborting.");
continue;
}
}
"queue-clear" => Command::QueueUpdate(vec![], QueueContent::Folder(0, vec![], String::new()).into()),
"queue-add-to-end" => Command::QueueAdd(vec![], QueueContent::Song(self.read_line_id("The ID of the song that should be added to the end of the queue is...")).into()),
"save" => Command::Save,
"status" => {
let db = self.database.lock().unwrap();
println!("DB contains {} songs:", db.songs().len());
for song in db.songs().values() {
println!("> [{}]: {}", song.id, song);
}
println!("Queue: {:?}, then {:?}", db.queue.get_current(), db.queue.get_next());
continue;
}
"exit" => {
println!("<< goodbye");
break;
}
_ => {
println!("Type 'exit' to exit, 'status' to see the db, 'resume', 'pause', 'stop', 'next', 'queue-clear', 'queue-add-to-end', 'add-song', 'add-album', 'add-artist', 'update-song', 'update-album', 'update-artist', 'set-lib-dir', or 'save' to control playback or update the db.");
continue;
}
}
.to_bytes(self.con)
.unwrap();
}
}
pub fn read_line(&mut self, q: &str) -> String {
loop {
if !q.is_empty() {
println!("{q}");
}
let mut line = String::new();
std::io::stdin().read_line(&mut line).unwrap();
while line.ends_with('\n') || line.ends_with('\r') {
line.pop();
}
if line.trim() == "#" {
self.cmd_loop();
} else {
return line;
}
}
}
pub fn read_line_id(&mut self, q: &str) -> u64 {
loop {
if let Ok(v) = self.read_line(q).trim().parse() {
return v;
} else {
println!("[-] Must be a positive integer.");
}
}
}
pub fn read_line_ido(&mut self, q: &str) -> Option<u64> {
loop {
let line = self.read_line(q);
let line = line.trim();
if line.is_empty() {
return None;
}
if let Ok(v) = line.parse() {
return Some(v);
} else {
println!("[-] Must be a positive integer or nothing for None.");
}
}
}
}
pub fn accumulate<F: FnMut() -> Option<T>, T>(mut f: F) -> Vec<T> {
let mut o = vec![];
loop {
if let Some(v) = f() {
o.push(v);
} else {
break;
}
}
o
}