mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-12-14 11:56:16 +01:00
init
This commit is contained in:
1
musicdb-client/.gitignore
vendored
Executable file
1
musicdb-client/.gitignore
vendored
Executable file
@@ -0,0 +1 @@
|
||||
/target
|
||||
14
musicdb-client/Cargo.toml
Executable file
14
musicdb-client/Cargo.toml
Executable 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
880
musicdb-client/src/gui.rs
Executable 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
459
musicdb-client/src/gui_base.rs
Executable 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
454
musicdb-client/src/gui_library.rs
Executable 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![]
|
||||
}
|
||||
}
|
||||
}
|
||||
238
musicdb-client/src/gui_playback.rs
Executable file
238
musicdb-client/src/gui_playback.rs
Executable 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
689
musicdb-client/src/gui_queue.rs
Executable 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
290
musicdb-client/src/gui_screen.rs
Executable 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
316
musicdb-client/src/gui_settings.rs
Normal file
316
musicdb-client/src/gui_settings.rs
Normal 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
226
musicdb-client/src/gui_text.rs
Executable 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![]
|
||||
}
|
||||
}
|
||||
147
musicdb-client/src/gui_wrappers.rs
Executable file
147
musicdb-client/src/gui_wrappers.rs
Executable 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
436
musicdb-client/src/main.rs
Executable 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
|
||||
}
|
||||
Reference in New Issue
Block a user