musicdb/musicdb-client/src/gui.rs

2033 lines
73 KiB
Rust
Executable File

use std::{
any::Any,
collections::{BTreeMap, HashMap},
io::Cursor,
net::TcpStream,
sync::{mpsc::Sender, Arc, Mutex},
thread::JoinHandle,
time::{Duration, Instant},
};
use musicdb_lib::{
data::{
database::{ClientIo, Database},
queue::Queue,
song::Song,
AlbumId, ArtistId, CoverId, SongId,
},
load::ToFromBytes,
server::{get, Action},
};
use speedy2d::{
color::Color,
dimen::{UVec2, Vec2},
font::Font,
image::ImageHandle,
shape::Rectangle,
window::{
KeyScancode, ModifiersState, MouseButton, MouseScrollDistance, UserEventSender,
VirtualKeyCode, WindowCreationOptions, WindowHandler, WindowHelper,
},
Graphics2D,
};
#[cfg(feature = "merscfg")]
use crate::merscfg::MersCfg;
use crate::{
gui_base::{Panel, ScrollBox},
gui_edit_song::EditorForSongs,
gui_notif::{NotifInfo, NotifOverlay},
gui_screen::GuiScreen,
gui_song_adder::SongAdder,
gui_text::Label,
textcfg,
};
pub enum GuiEvent {
Refresh,
#[cfg(feature = "merscfg")]
RefreshMers,
UpdatedQueue,
UpdatedLibrary,
Exit,
}
pub fn hotkey_deselect_all(modifiers: &ModifiersState, key: Option<VirtualKeyCode>) -> bool {
!modifiers.logo()
&& !modifiers.alt()
&& modifiers.ctrl()
&& !modifiers.shift()
&& matches!(key, Some(VirtualKeyCode::S))
}
pub fn hotkey_select_all(modifiers: &ModifiersState, key: Option<VirtualKeyCode>) -> bool {
!modifiers.logo()
&& !modifiers.alt()
&& modifiers.ctrl()
&& !modifiers.shift()
&& matches!(key, Some(VirtualKeyCode::A))
}
pub fn hotkey_select_albums(modifiers: &ModifiersState, key: Option<VirtualKeyCode>) -> bool {
!modifiers.logo()
&& !modifiers.alt()
&& modifiers.ctrl()
&& modifiers.shift()
&& matches!(key, Some(VirtualKeyCode::A))
}
pub fn hotkey_select_songs(modifiers: &ModifiersState, key: Option<VirtualKeyCode>) -> bool {
!modifiers.logo()
&& !modifiers.alt()
&& modifiers.ctrl()
&& modifiers.shift()
&& matches!(key, Some(VirtualKeyCode::S))
}
pub fn main(
database: Arc<Mutex<Database>>,
connection: TcpStream,
get_con: Arc<Mutex<get::Client<Box<dyn ClientIo + 'static>>>>,
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
#[cfg(feature = "merscfg")] after_db_cmd: &Arc<
Mutex<Option<Box<dyn FnMut(Command) + Send + Sync + 'static>>>,
>,
) {
let config_dir = super::get_config_file_path();
let config_file = config_dir.join("config_gui.toml");
let mut font = None;
let mut line_height = 32.0;
let mut scroll_pixels_multiplier = 1.0;
let mut scroll_lines_multiplier = 3.0;
let mut scroll_pages_multiplier = 0.75;
let status_bar_text;
let idle_top_text;
let idle_side1_text;
let idle_side2_text;
match std::fs::read_to_string(&config_file) {
Ok(cfg) => {
if let Ok(table) = cfg.parse::<toml::Table>() {
if let Some(path) = table["font"].as_str() {
if let Ok(bytes) = std::fs::read(path) {
if let Ok(f) = Font::new(&bytes) {
font = Some(f);
} else {
eprintln!("[toml] couldn't load font")
}
} else {
eprintln!("[toml] couldn't read font file")
}
}
if let Some(v) = table.get("line_height").and_then(|v| v.as_float()) {
line_height = v as _;
}
if let Some(v) = table
.get("scroll_pixels_multiplier")
.and_then(|v| v.as_float())
{
scroll_pixels_multiplier = v;
}
if let Some(v) = table
.get("scroll_lines_multiplier")
.and_then(|v| v.as_float())
{
scroll_lines_multiplier = v;
}
if let Some(v) = table
.get("scroll_pages_multiplier")
.and_then(|v| v.as_float())
{
scroll_pages_multiplier = v;
}
if let Some(t) = table.get("text").and_then(|v| v.as_table()) {
if let Some(v) = t.get("status_bar").and_then(|v| v.as_str()) {
match v.parse() {
Ok(v) => status_bar_text = v,
Err(e) => {
eprintln!("[toml] `text.status_bar couldn't be parsed: {e}`");
std::process::exit(30);
}
}
} else {
eprintln!("[toml] missing the required `text.status_bar` string value.");
std::process::exit(30);
}
if let Some(v) = t.get("idle_top").and_then(|v| v.as_str()) {
match v.parse() {
Ok(v) => idle_top_text = v,
Err(e) => {
eprintln!("[toml] `text.idle_top couldn't be parsed: {e}`");
std::process::exit(30);
}
}
} else {
eprintln!("[toml] missing the required `text.idle_top` string value.");
std::process::exit(30);
}
if let Some(v) = t.get("idle_side1").and_then(|v| v.as_str()) {
match v.parse() {
Ok(v) => idle_side1_text = v,
Err(e) => {
eprintln!("[toml] `text.idle_side1 couldn't be parsed: {e}`");
std::process::exit(30);
}
}
} else {
eprintln!("[toml] missing the required `text.idle_side1` string value.");
std::process::exit(30);
}
if let Some(v) = t.get("idle_side2").and_then(|v| v.as_str()) {
match v.parse() {
Ok(v) => idle_side2_text = v,
Err(e) => {
eprintln!("[toml] `text.idle_side2 couldn't be parsed: {e}`");
std::process::exit(30);
}
}
} else {
eprintln!("[toml] missing the required `text.idle_side2` string value.");
std::process::exit(30);
}
} else {
eprintln!("[toml] missing the required `[text]` section!");
std::process::exit(30);
}
} else {
eprintln!("Couldn't parse config file {config_file:?} as toml!");
std::process::exit(30);
}
}
Err(e) => {
eprintln!("[exit] no config file found at {config_file:?}: {e}");
if let Some(p) = config_file.parent() {
_ = std::fs::create_dir_all(p);
}
if std::fs::write(&config_file, include_bytes!("config_gui.toml")).is_ok() {
eprintln!("[info] created a default config file.");
}
std::process::exit(25);
}
}
let font = if let Some(v) = font {
v
} else {
eprintln!("[toml] required: font = <string>");
std::process::exit(30);
};
let window = speedy2d::Window::<GuiEvent>::new_with_user_events(
"MusicDB Client",
WindowCreationOptions::new_windowed(
speedy2d::window::WindowSize::MarginPhysicalPixels(0),
None,
),
)
.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(
font,
Arc::clone(&database),
connection,
get_con,
event_sender_arc,
Arc::new(sender),
line_height,
scroll_pixels_multiplier,
scroll_lines_multiplier,
scroll_pages_multiplier,
GuiConfig {
status_bar_text,
idle_top_text,
idle_side1_text,
idle_side2_text,
filter_presets_song: vec![
(
"Fav".to_owned(),
crate::gui_library::FilterType::TagEq("Fav".to_owned()),
),
(
"Year".to_owned(),
crate::gui_library::FilterType::TagWithValueInt("Year=".to_owned(), 1990, 2000),
),
],
filter_presets_album: vec![
(
"Fav".to_owned(),
crate::gui_library::FilterType::TagEq("Fav".to_owned()),
),
(
"Year".to_owned(),
crate::gui_library::FilterType::TagWithValueInt("Year=".to_owned(), 1990, 2000),
),
],
filter_presets_artist: vec![
(
"Fav".to_owned(),
crate::gui_library::FilterType::TagEq("Fav".to_owned()),
),
(
"Year".to_owned(),
crate::gui_library::FilterType::TagWithValueInt("Year=".to_owned(), 1990, 2000),
),
],
#[cfg(feature = "merscfg")]
merscfg: crate::merscfg::MersCfg::new(config_dir.join("dynamic_config.mers"), database),
},
#[cfg(feature = "merscfg")]
after_db_cmd,
));
}
pub struct GuiConfig {
pub status_bar_text: textcfg::TextBuilder,
pub idle_top_text: textcfg::TextBuilder,
pub idle_side1_text: textcfg::TextBuilder,
pub idle_side2_text: textcfg::TextBuilder,
pub filter_presets_song: Vec<(String, crate::gui_library::FilterType)>,
pub filter_presets_album: Vec<(String, crate::gui_library::FilterType)>,
pub filter_presets_artist: Vec<(String, crate::gui_library::FilterType)>,
#[cfg(feature = "merscfg")]
pub merscfg: crate::merscfg::MersCfg,
}
pub struct Gui {
pub event_sender: Arc<UserEventSender<GuiEvent>>,
pub database: Arc<Mutex<Database>>,
pub connection: TcpStream,
pub get_con: Arc<Mutex<get::Client<Box<dyn ClientIo + 'static>>>>,
pub gui: GuiScreen,
pub notif_sender:
Sender<Box<dyn FnOnce(&NotifOverlay) -> (Box<dyn GuiElem>, NotifInfo) + Send>>,
pub size: UVec2,
pub mouse_pos: Vec2,
pub font: Font,
pub keybinds: BTreeMap<KeyBinding, KeyActionRef>,
pub key_actions: KeyActions,
pub covers: Option<HashMap<CoverId, GuiServerImage>>,
pub custom_images: Option<HashMap<String, GuiServerImage>>,
pub modifiers: ModifiersState,
pub dragging: Option<(
Dragging,
Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>,
)>,
pub high_performance: bool,
pub line_height: f32,
pub last_height: f32,
pub scroll_pixels_multiplier: f64,
pub scroll_lines_multiplier: f64,
pub scroll_pages_multiplier: f64,
pub gui_config: Option<GuiConfig>,
last_performance_check: Instant,
average_frame_time_ms: u32,
frames_drawn: u32,
}
impl Gui {
fn new(
font: Font,
database: Arc<Mutex<Database>>,
connection: TcpStream,
get_con: Arc<Mutex<get::Client<Box<dyn ClientIo + 'static>>>>,
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
event_sender: Arc<UserEventSender<GuiEvent>>,
line_height: f32,
scroll_pixels_multiplier: f64,
scroll_lines_multiplier: f64,
scroll_pages_multiplier: f64,
#[cfg(not(feature = "merscfg"))] gui_config: GuiConfig,
#[cfg(feature = "merscfg")] mut gui_config: GuiConfig,
#[cfg(feature = "merscfg")] after_db_cmd: &Arc<
Mutex<Option<Box<dyn FnMut(Command) + Send + Sync + 'static>>>,
>,
) -> Self {
let (notif_overlay, notif_sender) = NotifOverlay::new();
let notif_sender_two = notif_sender.clone();
#[cfg(feature = "merscfg")]
match gui_config.merscfg.load(
Arc::clone(&event_sender),
notif_sender.clone(),
after_db_cmd,
) {
Err(e) => {
if !matches!(e.kind(), std::io::ErrorKind::NotFound) {
eprintln!("Couldn't load merscfg: {e}")
}
}
Ok(Err(e)) => {
eprintln!("Error loading merscfg:\n{}", e.display_term());
}
Ok(Ok(Err((m, e)))) => {
if let Some(e) = e {
eprintln!("Error loading merscfg:\n{m}\n{}", e.display_term());
} else {
eprintln!("Error loading merscfg:\n{m}");
}
}
Ok(Ok(Ok(()))) => eprintln!("Info: using merscfg"),
}
{
let mut db = database.lock().unwrap();
let udepid = db.update_endpoints_id;
db.update_endpoints_id += 1;
db.update_endpoints.push((
udepid,
musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| {
the_impl(&cmd.action, &event_sender_arc, &notif_sender_two);
fn the_impl(
action: &Action,
event_sender_arc: &Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
notif_sender_two: &Sender<
Box<dyn FnOnce(&NotifOverlay) -> (Box<dyn GuiElem>, NotifInfo) + Send>,
>,
) {
match action {
Action::Resume
| Action::Pause
| Action::Stop
| Action::Save
| Action::InitComplete => {}
Action::NextSong
| Action::QueueUpdate(..)
| Action::QueueAdd(..)
| Action::QueueInsert(..)
| Action::QueueRemove(..)
| Action::QueueMove(..)
| Action::QueueMoveInto(..)
| Action::QueueGoto(..)
| Action::QueueShuffle(..)
| Action::QueueSetShuffle(..)
| Action::QueueUnshuffle(..) => {
if let Some(s) = &*event_sender_arc.lock().unwrap() {
_ = s.send_event(GuiEvent::UpdatedQueue);
}
}
Action::SyncDatabase(..)
| Action::AddSong(_, _)
| Action::AddAlbum(_, _)
| Action::AddArtist(_, _)
| Action::AddCover(_, _)
| Action::ModifySong(_, _)
| Action::ModifyAlbum(_, _)
| Action::ModifyArtist(_, _)
| Action::RemoveSong(_)
| Action::RemoveAlbum(_)
| Action::RemoveArtist(_)
| Action::TagSongFlagSet(..)
| Action::TagSongFlagUnset(..)
| Action::TagAlbumFlagSet(..)
| Action::TagAlbumFlagUnset(..)
| Action::TagArtistFlagSet(..)
| Action::TagArtistFlagUnset(..)
| Action::TagSongPropertySet(..)
| Action::TagSongPropertyUnset(..)
| Action::TagAlbumPropertySet(..)
| Action::TagAlbumPropertyUnset(..)
| Action::TagArtistPropertySet(..)
| Action::TagArtistPropertyUnset(..)
| Action::SetSongDuration(..) => {
if let Some(s) = &*event_sender_arc.lock().unwrap() {
_ = s.send_event(GuiEvent::UpdatedLibrary);
}
}
Action::Multiple(actions) => {
for action in actions {
the_impl(action, event_sender_arc, notif_sender_two);
}
}
Action::ErrorInfo(t, d) => {
let (t, d) = (t.clone(), d.clone());
notif_sender_two
.send(Box::new(move |_| {
(
Box::new(Panel::with_background(
GuiElemCfg::default(),
[Label::new(
GuiElemCfg::default(),
if t.is_empty() {
format!("Server message\n{d}")
} else {
format!("Server error ({t})\n{d}")
},
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
)],
Color::from_rgba(0.0, 0.0, 0.0, 0.8),
)),
if t.is_empty() {
NotifInfo::new(Duration::from_secs(2))
} else {
NotifInfo::new(Duration::from_secs(5))
.with_highlight(Color::RED)
},
)
}))
.unwrap();
}
Action::Denied(req) => {
let req = *req;
notif_sender_two
.send(Box::new(move |_| {
(
Box::new(Panel::with_background(
GuiElemCfg::default(),
[Label::new(
GuiElemCfg::default(),
format!(
"server denied {}",
if req.is_some() {
"request, maybe desynced"
} else {
"action, likely desynced"
},
),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
)],
Color::from_rgba(0.0, 0.0, 0.0, 0.8),
)),
NotifInfo::new(Duration::from_secs(1)),
)
}))
.unwrap();
}
}
}
})),
));
}
let no_animations = false;
Gui {
event_sender,
database,
connection,
get_con,
gui: GuiScreen::new(
GuiElemCfg::default(),
notif_overlay,
no_animations,
line_height,
scroll_pixels_multiplier,
scroll_lines_multiplier,
scroll_pages_multiplier,
),
notif_sender,
size: UVec2::ZERO,
mouse_pos: Vec2::ZERO,
font,
keybinds: BTreeMap::new(),
key_actions: KeyActions::default(),
covers: Some(HashMap::new()),
custom_images: Some(HashMap::new()),
// font: Font::new(include_bytes!("/usr/share/fonts/TTF/FiraSans-Regular.ttf")).unwrap(),
modifiers: ModifiersState::default(),
dragging: None,
high_performance: no_animations,
line_height,
last_height: 720.0,
scroll_pixels_multiplier,
scroll_lines_multiplier,
scroll_pages_multiplier,
gui_config: Some(gui_config),
last_performance_check: Instant::now(),
average_frame_time_ms: 0,
frames_drawn: 0,
}
}
fn get_specific_gui_elem_config(&mut self, elem: SpecificGuiElem) -> &mut GuiElemCfg {
match elem {
SpecificGuiElem::SearchArtist => self
.gui
.c_main_view
.children
.library_browser
.c_search_artist
.config_mut(),
SpecificGuiElem::SearchAlbum => self
.gui
.c_main_view
.children
.library_browser
.c_search_album
.config_mut(),
SpecificGuiElem::SearchSong => self
.gui
.c_main_view
.children
.library_browser
.c_search_song
.config_mut(),
}
}
}
/// the trait implemented by all Gui elements.
/// feel free to override the methods you wish to use.
#[allow(unused)]
pub trait GuiElem {
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 and change draw_rev to false.
fn children(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_>;
/// defaults to true.
fn draw_rev(&self) -> bool {
true
}
fn any(&self) -> &dyn Any;
fn any_mut(&mut self) -> &mut dyn Any;
fn elem(&self) -> &dyn GuiElem;
fn elem_mut(&mut self) -> &mut dyn GuiElem;
/// 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, e: &mut EventInfo, 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, e: &mut EventInfo, 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, e: &mut EventInfo, button: MouseButton) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn mouse_wheel(&mut self, e: &mut EventInfo, diff: f32) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn char_watch(
&mut self,
e: &mut EventInfo,
modifiers: ModifiersState,
key: char,
) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn char_focus(
&mut self,
e: &mut EventInfo,
modifiers: ModifiersState,
key: char,
) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn key_watch(
&mut self,
e: &mut EventInfo,
modifiers: ModifiersState,
down: bool,
key: Option<VirtualKeyCode>,
scan: KeyScancode,
) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn key_focus(
&mut self,
e: &mut EventInfo,
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, e: &mut EventInfo, dragged: Dragging) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn updated_library(&mut self) {}
fn updated_queue(&mut self) {}
}
pub struct EventInfo(bool);
impl EventInfo {
pub fn can_take(&self) -> bool {
self.0
}
pub fn take(&mut self) -> bool {
std::mem::replace(&mut self.0, false)
}
fn new() -> Self {
Self(true)
}
}
impl<T: GuiElem + ?Sized> GuiElemInternal for T {}
pub(crate) trait GuiElemInternal: GuiElem {
fn _draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) {
if !self.config_mut().enabled {
return;
}
// adjust info
let npos = adjust_area(&info.pos, &self.config_mut().pos);
let ppos = std::mem::replace(&mut info.pos, npos);
if info.child_has_keyboard_focus {
if self.config().keyboard_focus_index == usize::MAX {
info.has_keyboard_focus = true;
info.child_has_keyboard_focus = false;
}
}
info.mouse_pos_in_bounds = info.pos.contains(info.mouse_pos);
if !info.mouse_pos_in_bounds {
self.config_mut().mouse_down = (false, false, false);
}
// call trait's draw function
self.draw(info, g);
// reset cfg
self.config_mut().init = false;
// 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.config().keyboard_focus_index;
if self.draw_rev() {
for (i, c) in self
.children()
.collect::<Vec<_>>()
.into_iter()
.enumerate()
.rev()
{
info.child_has_keyboard_focus = focus_path && i == kbd_focus_index;
c._draw(info, g);
}
} else {
for (i, c) in self.children().enumerate() {
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.config_mut().pixel_pos = std::mem::replace(&mut info.pos, ppos);
}
/// recursively applies the function to all gui elements below and including this one
fn _recursive_all(&mut self, allow_deactivated: bool, f: &mut dyn FnMut(&mut dyn GuiElem)) {
if self.config().enabled || allow_deactivated {
f(self.elem_mut());
for c in self.children() {
c._recursive_all(allow_deactivated, f);
}
}
}
fn _mouse_event(
&mut self,
e: &mut EventInfo,
allow_deactivated: bool,
condition: &mut dyn FnMut(&mut dyn GuiElem, &mut EventInfo) -> Option<Vec<GuiAction>>,
pos: Vec2,
) -> Option<Vec<GuiAction>> {
if self.config().enabled || allow_deactivated {
for c in &mut self.children() {
if c.config().enabled {
if c.config().pixel_pos.contains(pos) {
if let Some(v) = c._mouse_event(e, allow_deactivated, condition, pos) {
return Some(v);
}
}
}
}
condition(self.elem_mut(), e)
} else {
None
}
}
fn _release_drag(
&mut self,
e: &mut EventInfo,
dragged: &mut Option<Dragging>,
pos: Vec2,
) -> Option<Vec<GuiAction>> {
self._mouse_event(
e,
false,
&mut |v, e| {
if v.config().drag_target {
if let Some(d) = dragged.take() {
return Some(v.dragged(e, d));
}
}
None
},
pos,
)
}
fn _mouse_button(
&mut self,
e: &mut EventInfo,
button: MouseButton,
down: bool,
pos: Vec2,
) -> Option<Vec<GuiAction>> {
if down {
self._mouse_event(
e,
!down,
&mut |v: &mut dyn GuiElem, e| {
if v.config().mouse_events {
match button {
MouseButton::Left => {
v.config_mut().mouse_down.0 = true;
v.config_mut().mouse_pressed.0 = true;
}
MouseButton::Middle => {
v.config_mut().mouse_down.1 = true;
v.config_mut().mouse_pressed.1 = true;
}
MouseButton::Right => {
v.config_mut().mouse_down.2 = true;
v.config_mut().mouse_pressed.2 = true;
}
MouseButton::Other(_) => {}
}
Some(v.mouse_down(e, button))
} else {
None
}
},
pos,
)
} else {
let mut vec = vec![];
if let Some(a) = self._mouse_event(
e,
!down,
&mut |v: &mut dyn GuiElem, e| {
let down = v.config().mouse_down;
if v.config().mouse_events
&& ((button == MouseButton::Left && down.0)
|| (button == MouseButton::Middle && down.1)
|| (button == MouseButton::Right && down.2))
{
Some(v.mouse_pressed(e, button))
} else {
None
}
},
pos,
) {
vec.extend(a);
};
self._recursive_all(!down, &mut |v| {
if v.config().mouse_events {
match button {
MouseButton::Left => {
v.config_mut().mouse_down.0 = false;
v.config_mut().mouse_pressed.0 = false;
}
MouseButton::Middle => {
v.config_mut().mouse_down.1 = false;
v.config_mut().mouse_pressed.1 = false;
}
MouseButton::Right => {
v.config_mut().mouse_down.2 = false;
v.config_mut().mouse_pressed.2 = false;
}
MouseButton::Other(_) => {}
}
vec.extend(v.mouse_up(e, button));
}
});
Some(vec)
}
}
fn _mouse_wheel(&mut self, e: &mut EventInfo, diff: f32, pos: Vec2) -> Option<Vec<GuiAction>> {
self._mouse_event(
e,
false,
&mut |v, e| {
if v.config().scroll_events {
Some(v.mouse_wheel(e, diff))
} else {
None
}
},
pos,
)
}
fn _keyboard_event(
&mut self,
e: &mut EventInfo,
allow_deactivated: bool,
f_focus: &mut dyn FnMut(&mut dyn GuiElem, &mut EventInfo, &mut Vec<GuiAction>),
f_watch: &mut dyn FnMut(&mut dyn GuiElem, &mut EventInfo, &mut Vec<GuiAction>),
) -> Vec<GuiAction> {
let mut o = vec![];
self._keyboard_event_inner_focus(e, allow_deactivated, f_focus, &mut o);
self._keyboard_event_inner_nofocus(e, allow_deactivated, f_watch, &mut o);
o
}
fn _keyboard_event_inner_nofocus(
&mut self,
e: &mut EventInfo,
allow_deactivated: bool,
f_watch: &mut dyn FnMut(&mut dyn GuiElem, &mut EventInfo, &mut Vec<GuiAction>),
events: &mut Vec<GuiAction>,
) {
if self.config().enabled || allow_deactivated {
for child in self.children() {
child._keyboard_event_inner_nofocus(e, allow_deactivated, f_watch, events);
}
f_watch(self.elem_mut(), e, events);
}
}
fn _keyboard_event_inner_focus(
&mut self,
e: &mut EventInfo,
allow_deactivated: bool,
f_focus: &mut dyn FnMut(&mut dyn GuiElem, &mut EventInfo, &mut Vec<GuiAction>),
events: &mut Vec<GuiAction>,
) {
if e.can_take() && (self.config().enabled || allow_deactivated) {
let i = self.config().keyboard_focus_index;
if let Some(child) = self.children().nth(i) {
child._keyboard_event_inner_focus(e, allow_deactivated, f_focus, events);
}
if e.can_take() {
// we have focus and no child has taken the event
f_focus(self.elem_mut(), e, events);
}
}
}
fn _keyboard_move_focus(&mut self, decrement: bool, refocus: bool) -> bool {
if self.config().enabled == false {
return false;
}
let mut focus_index = if refocus {
usize::MAX
} else {
self.config().keyboard_focus_index
};
let allow_focus = self.config().keyboard_events_focus;
let mut children = self.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.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.children().enumerate() {
if c._keyboard_reset_focus() {
index = i;
break;
}
}
let wants = std::mem::replace(&mut self.config_mut().request_keyboard_focus, false);
self.config_mut().keyboard_focus_index = index;
index != usize::MAX || wants
}
}
pub trait GuiElemWrapper {
fn as_elem(&self) -> &dyn GuiElem;
fn as_elem_mut(&mut self) -> &mut dyn GuiElem;
}
impl<T: GuiElem> GuiElemWrapper for Box<T> {
fn as_elem(&self) -> &dyn GuiElem {
self.as_ref()
}
fn as_elem_mut(&mut self) -> &mut dyn GuiElem {
self.as_mut()
}
}
impl GuiElemWrapper for Box<dyn GuiElem> {
fn as_elem(&self) -> &dyn GuiElem {
self.as_ref()
}
fn as_elem_mut(&mut self) -> &mut dyn GuiElem {
self.as_mut()
}
}
impl<T: GuiElemWrapper> GuiElem for T {
fn config(&self) -> &GuiElemCfg {
self.as_elem().config()
}
fn config_mut(&mut self) -> &mut GuiElemCfg {
self.as_elem_mut().config_mut()
}
fn children(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
self.as_elem_mut().children()
}
fn draw_rev(&self) -> bool {
self.as_elem().draw_rev()
}
fn any(&self) -> &dyn Any {
self.as_elem().any()
}
fn any_mut(&mut self) -> &mut dyn Any {
self.as_elem_mut().any_mut()
}
fn elem(&self) -> &dyn GuiElem {
self.as_elem().elem()
}
fn elem_mut(&mut self) -> &mut dyn GuiElem {
self.as_elem_mut().elem_mut()
}
fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) {
self.as_elem_mut().draw(info, g)
}
fn mouse_down(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec<GuiAction> {
self.as_elem_mut().mouse_down(e, button)
}
fn mouse_up(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec<GuiAction> {
self.as_elem_mut().mouse_up(e, button)
}
fn mouse_pressed(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec<GuiAction> {
self.as_elem_mut().mouse_pressed(e, button)
}
fn mouse_wheel(&mut self, e: &mut EventInfo, diff: f32) -> Vec<GuiAction> {
self.as_elem_mut().mouse_wheel(e, diff)
}
fn char_watch(
&mut self,
e: &mut EventInfo,
modifiers: ModifiersState,
key: char,
) -> Vec<GuiAction> {
self.as_elem_mut().char_watch(e, modifiers, key)
}
fn char_focus(
&mut self,
e: &mut EventInfo,
modifiers: ModifiersState,
key: char,
) -> Vec<GuiAction> {
self.as_elem_mut().char_focus(e, modifiers, key)
}
fn key_watch(
&mut self,
e: &mut EventInfo,
modifiers: ModifiersState,
down: bool,
key: Option<VirtualKeyCode>,
scan: KeyScancode,
) -> Vec<GuiAction> {
self.as_elem_mut().key_watch(e, modifiers, down, key, scan)
}
fn key_focus(
&mut self,
e: &mut EventInfo,
modifiers: ModifiersState,
down: bool,
key: Option<VirtualKeyCode>,
scan: KeyScancode,
) -> Vec<GuiAction> {
self.as_elem_mut().key_focus(e, modifiers, down, key, scan)
}
fn dragged(&mut self, e: &mut EventInfo, dragged: Dragging) -> Vec<GuiAction> {
self.as_elem_mut().dragged(e, dragged)
}
fn updated_library(&mut self) {
self.as_elem_mut().updated_library()
}
fn updated_queue(&mut self) {
self.as_elem_mut().updated_queue()
}
}
pub trait GuiElemChildren {
fn iter(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_>;
fn len(&self) -> usize;
}
impl GuiElemChildren for () {
fn iter(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
Box::new([].into_iter())
}
fn len(&self) -> usize {
0
}
}
impl<const N: usize, T: GuiElem> GuiElemChildren for [T; N] {
fn iter(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
Box::new(self.iter_mut().map(|v| v.elem_mut()))
}
fn len(&self) -> usize {
N
}
}
impl<T: GuiElem> GuiElemChildren for Vec<T> {
fn iter(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
Box::new(self.iter_mut().map(|v| v.elem_mut()))
}
fn len(&self) -> usize {
self.len()
}
}
impl<A: GuiElem, B: GuiElem> GuiElemChildren for (A, B) {
fn iter(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
Box::new([self.0.elem_mut(), self.1.elem_mut()].into_iter())
}
fn len(&self) -> usize {
2
}
}
impl<A: GuiElem, B: GuiElem, C: GuiElem> GuiElemChildren for (A, B, C) {
fn iter(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
Box::new([self.0.elem_mut(), self.1.elem_mut(), self.2.elem_mut()].into_iter())
}
fn len(&self) -> usize {
3
}
}
impl<A: GuiElem, B: GuiElem, C: GuiElem, D: GuiElem> GuiElemChildren for (A, B, C, D) {
fn iter(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
Box::new(
[
self.0.elem_mut(),
self.1.elem_mut(),
self.2.elem_mut(),
self.3.elem_mut(),
]
.into_iter(),
)
}
fn len(&self) -> usize {
4
}
}
impl<A: GuiElem, B: GuiElem, C: GuiElem, D: GuiElem, E: GuiElem> GuiElemChildren
for (A, B, C, D, E)
{
fn iter(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
Box::new(
[
self.0.elem_mut(),
self.1.elem_mut(),
self.2.elem_mut(),
self.3.elem_mut(),
self.4.elem_mut(),
]
.into_iter(),
)
}
fn len(&self) -> usize {
5
}
}
#[derive(Debug, Clone)]
/// The config for any gui element.
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,
/// will be set to false after `draw`.
/// can be used to, for example, add the keybinds for your element.
pub init: bool,
/// Position relative to the parent where this element should be drawn.
/// ((0, 0), (1, 1)) is the default and fills all available space.
/// ((0, 0.5), (0.5, 1)) fills the bottom left quarter.
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,
/// which mouse buttons were pressed down while the mouse was on this element and haven't been released OR moved away since? (Left/Middle/Right)
pub mouse_down: (bool, bool, bool),
/// which mouse buttons were pressed down while the mouse was on this element and haven't been released since? (Left/Middle/Right)
pub mouse_pressed: (bool, bool, bool),
/// Set this to true to receive mouse click events when the mouse is within this element's bounds
pub mouse_events: bool,
/// Set this to true to receive scroll events when the mouse is within this element's bounds
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 (Tab skips elements with keyboard_events_focus == false)
pub keyboard_focus_index: usize,
/// if this is true and ResetKeyboardFocus is returned, this element may get the keyboard focus (guaranteed if no other element has this set to true)
pub request_keyboard_focus: bool,
/// if this is true, things can be dragged into this element via drag-n-drop
pub drag_target: bool,
}
#[allow(unused)]
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 force_redraw(mut self) -> Self {
self.redraw = true;
self
}
pub fn disabled(mut self) -> Self {
self.enabled = false;
self
}
}
impl Default for GuiElemCfg {
fn default() -> Self {
Self {
enabled: true,
redraw: false,
init: true,
pos: Rectangle::new(Vec2::ZERO, Vec2::new(1.0, 1.0)),
pixel_pos: Rectangle::ZERO,
mouse_down: (false, false, false),
mouse_pressed: (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,
}
}
}
#[allow(unused)]
pub enum GuiAction {
OpenMain,
/// Add a key action (and optionally bind it) and then call the given function
AddKeybind(
Option<(KeyBinding, bool)>,
KeyAction,
Box<dyn FnOnce(KeyActionId)>,
),
/// Binds the action to the keybinding, or unbinds it entirely
SetKeybind(KeyActionId, Option<(KeyBinding, bool)>),
ForceIdle,
/// false -> prevent idling, true -> end idling even if already idle
EndIdle(bool),
SetHighPerformance(bool),
OpenSettings(bool),
ShowNotification(Box<dyn FnOnce(&NotifOverlay) -> (Box<dyn GuiElem>, NotifInfo) + Send>),
/// Build the GuiAction(s) later, when we have access to the Database (can turn an AlbumId into a QueueContent::Folder, etc)
Build(Box<dyn FnOnce(&mut Database) -> Vec<Self>>),
SendToServer(Action),
ContextMenu(Option<(Vec<Box<dyn GuiElem>>)>),
/// unfocuses all gui elements, then assigns keyboard focus to one with config().request_keyboard_focus == true if there is one.
ResetKeyboardFocus,
SetFocused(SpecificGuiElem),
SetDragging(
Option<(
Dragging,
Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>,
)>,
),
SetLineHeight(f32),
LoadCover(CoverId),
/// Run a custom closure with mutable access to the Gui struct
Do(Box<dyn FnOnce(&mut Gui)>),
Exit,
EditSongs(Vec<Song>),
// EditAlbums(Vec<Album>),
// EditArtists(Vec<Artist>),
OpenAddSongsMenu,
CloseAddSongsMenu,
}
pub enum Dragging {
Artist(ArtistId),
Album(AlbumId),
Song(SongId),
Queue(Result<Queue, Vec<usize>>),
Queues(Vec<Queue>),
}
pub enum SpecificGuiElem {
SearchArtist,
SearchAlbum,
SearchSong,
}
/// GuiElems have access to this within draw.
/// Except for `actions`, they should not change any of these values - GuiElem::draw will handle everything automatically.
pub struct DrawInfo<'a> {
pub time: Instant,
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,
/// true if `info.pos.contains(info.mouse_pos)`.
pub mouse_pos_in_bounds: bool,
pub helper: Option<&'a mut WindowHelper<GuiEvent>>,
pub get_con: Arc<Mutex<get::Client<Box<dyn ClientIo + 'static>>>>,
pub covers: &'a mut HashMap<CoverId, GuiServerImage>,
pub custom_images: &'a mut HashMap<String, GuiServerImage>,
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 dragging: Option<(
Dragging,
Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>,
)>,
pub gui_config: &'a mut GuiConfig,
pub high_performance: bool,
}
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 {
pub 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::AddKeybind(bind, action, func) => {
let id = self.key_actions.add(action);
if let Some((bind, priority)) = bind {
self.keybinds.insert(bind, id.with_priority(priority));
}
func(id);
}
GuiAction::SetKeybind(action, bind) => {
for b in self
.keybinds
.iter()
.filter_map(|(b, a)| {
if a.get_index() == action.get_index() {
Some(b)
} else {
None
}
})
.copied()
.collect::<Vec<_>>()
{
self.keybinds.remove(&b);
}
if let Some((bind, priority)) = bind {
self.keybinds.insert(bind, action.with_priority(priority));
}
}
GuiAction::SendToServer(action) => {
let command = self.database.lock().unwrap().seq.pack(action);
#[cfg(debug_assertions)]
eprintln!("[DEBUG] Sending command to server: {command:?}");
if let Err(e) = command.to_bytes(&mut self.connection) {
eprintln!("Error sending command to server: {e}");
}
}
GuiAction::ShowNotification(func) => _ = self.notif_sender.send(func),
GuiAction::SetFocused(elem) => {
self.get_specific_gui_elem_config(elem)
.request_keyboard_focus = true;
self.gui._keyboard_reset_focus();
}
GuiAction::ResetKeyboardFocus => _ = self.gui._keyboard_reset_focus(),
GuiAction::SetDragging(d) => self.dragging = d,
GuiAction::SetHighPerformance(d) => self.high_performance = d,
GuiAction::ContextMenu(elems) => {
self.gui.c_context_menu = if let Some(elems) = elems {
let elem_height = 32.0;
let w = elem_height * 6.0;
let h = elem_height * elems.len() as f32;
let mut ax = self.mouse_pos.x / self.size.x.max(1) as f32;
let mut ay = self.mouse_pos.y / self.size.y.max(1) as f32;
let mut bx = (self.mouse_pos.x + w) / self.size.x.max(1) as f32;
let mut by = (self.mouse_pos.y + h) / self.size.y.max(1) as f32;
if bx > 1.0 {
ax -= bx - 1.0;
bx = 1.0;
}
if by > 1.0 {
ay -= by - 1.0;
by = 1.0;
}
if ax < 0.0 {
ax = 0.0;
}
if ay < 0.0 {
ay = 0.0;
}
Some(Box::new(ScrollBox::new(
GuiElemCfg::at(Rectangle::from_tuples((ax, ay), (bx, by))),
crate::gui_base::ScrollBoxSizeUnit::Pixels,
elems,
vec![],
elem_height,
)))
} else {
None
};
}
GuiAction::SetLineHeight(h) => {
self.line_height = h;
self.gui
._recursive_all(true, &mut |e| e.config_mut().redraw = true);
}
GuiAction::LoadCover(id) => {
self.covers
.as_mut()
.unwrap()
.insert(id, GuiServerImage::new_cover(id, Arc::clone(&self.get_con)));
}
GuiAction::Do(f) => f(self),
GuiAction::Exit => _ = self.event_sender.send_event(GuiEvent::Exit),
GuiAction::ForceIdle => {
self.gui.force_idle();
}
GuiAction::EndIdle(v) => {
if v {
self.gui.unidle();
} else {
self.gui.not_idle();
}
}
GuiAction::OpenSettings(v) => {
self.gui.unidle();
if self.gui.settings.0 != v {
self.gui.settings = (v, Some(Instant::now()));
}
}
GuiAction::OpenMain => {
self.gui.unidle();
if self.gui.settings.0 {
self.gui.settings = (false, Some(Instant::now()));
}
}
GuiAction::EditSongs(songs) => {
self.gui.c_editing_songs = Some(EditorForSongs::new(songs));
}
GuiAction::OpenAddSongsMenu => {
if self.gui.c_song_adder.is_none() {
self.gui.c_song_adder = Some(SongAdder::new(
GuiElemCfg::default(),
self.high_performance,
self.line_height,
self.scroll_pixels_multiplier,
self.scroll_lines_multiplier,
self.scroll_pages_multiplier,
));
}
}
GuiAction::CloseAddSongsMenu => self.gui.c_song_adder = None,
}
}
}
impl WindowHandler<GuiEvent> for Gui {
fn on_draw(&mut self, helper: &mut WindowHelper<GuiEvent>, graphics: &mut Graphics2D) {
let draw_start_time = Instant::now();
graphics.draw_rectangle(
Rectangle::new(Vec2::ZERO, self.size.into_f32()),
Color::BLACK,
);
let mut cfg = self.gui_config.take().unwrap();
// before the db is locked!
#[cfg(feature = "merscfg")]
MersCfg::run(&mut cfg, self, |m| &m.func_before_draw);
let dblock = Arc::clone(&self.database);
let mut dblock = dblock.lock().unwrap();
let mut covers = self.covers.take().unwrap();
let mut custom_images = self.custom_images.take().unwrap();
let mut info = DrawInfo {
time: draw_start_time,
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,
mouse_pos_in_bounds: false,
get_con: Arc::clone(&self.get_con),
covers: &mut covers,
custom_images: &mut custom_images,
helper: Some(helper),
has_keyboard_focus: false,
child_has_keyboard_focus: true,
line_height: self.line_height,
high_performance: self.high_performance,
dragging: self.dragging.take(),
gui_config: &mut cfg,
};
self.gui._draw(&mut info, graphics);
let actions = std::mem::replace(&mut info.actions, Vec::with_capacity(0));
self.dragging = info.dragging.take();
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),
),
Dragging::Queues(_) => graphics.draw_circle(
self.mouse_pos,
25.0,
Color::from_int_rgba(100, 0, 255, 100),
),
}
}
}
// cleanup
drop(info);
self.gui_config = Some(cfg);
self.covers = Some(covers);
self.custom_images = Some(custom_images);
drop(dblock);
for a in actions {
self.exec_gui_action(a);
}
let ft = draw_start_time.elapsed().as_millis() as u32;
self.average_frame_time_ms = (self.average_frame_time_ms * 7 + ft) / 8;
if !self.high_performance && self.average_frame_time_ms > 50 {
self.high_performance = true;
*self
.gui
.c_settings
.c_scroll_box
.children
.performance_toggle
.children
.1
.children[0]
.content
.text() = "On due to\nbad performance".to_string();
}
#[cfg(debug_assertions)]
{
self.frames_drawn += 1;
if draw_start_time
.duration_since(self.last_performance_check)
.as_secs()
>= 1
{
self.last_performance_check = draw_start_time;
eprintln!(
"[performance] {} fps | {}ms",
self.frames_drawn, self.average_frame_time_ms
);
self.frames_drawn = 0;
}
}
}
fn on_mouse_button_down(&mut self, helper: &mut WindowHelper<GuiEvent>, button: MouseButton) {
if let Some(a) =
self.gui
._mouse_button(&mut EventInfo::new(), 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() {
let (dr, _) = self.dragging.take().unwrap();
let mut opt = Some(dr);
if let Some(a) =
self.gui
._release_drag(&mut EventInfo::new(), &mut opt, self.mouse_pos.clone())
{
for a in a {
self.exec_gui_action(a)
}
}
if let Some(dr) = opt {
match dr {
Dragging::Artist(_)
| Dragging::Album(_)
| Dragging::Song(_)
| Dragging::Queue(Ok(_))
| Dragging::Queues(_) => (),
Dragging::Queue(Err(path)) => {
self.exec_gui_action(GuiAction::SendToServer(Action::QueueRemove(path)))
}
}
}
}
if let Some(a) =
self.gui
._mouse_button(&mut EventInfo::new(), button, false, self.mouse_pos.clone())
{
for a in a {
self.exec_gui_action(a)
}
}
if button != MouseButton::Right {
self.gui.c_context_menu = None;
}
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 * self.scroll_lines_multiplier) as f32
}
MouseScrollDistance::Lines { y, .. } => {
(self.scroll_lines_multiplier * y) as f32 * self.line_height
}
MouseScrollDistance::Pages { y, .. } => {
(self.scroll_pages_multiplier * y * self.scroll_lines_multiplier) as f32
* self.last_height
}
};
if let Some(a) = self
.gui
._mouse_wheel(&mut EventInfo::new(), 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(
&mut EventInfo::new(),
false,
&mut |g, e, a| {
if g.config().keyboard_events_focus {
a.append(&mut g.char_focus(e, self.modifiers.clone(), unicode_codepoint));
}
},
&mut |g, e, a| {
if g.config().keyboard_events_watch {
a.append(&mut g.char_watch(e, 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(
&mut EventInfo::new(),
false,
&mut |g, e, a| {
if g.config().keyboard_events_focus {
a.append(&mut g.key_focus(
e,
self.modifiers.clone(),
true,
virtual_key_code,
scancode,
));
}
},
&mut |g, e, a| {
if g.config().keyboard_events_watch {
a.append(&mut g.key_watch(
e,
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();
// handle keybinds unless settings are open, opening or closing
let mut e = EventInfo::new();
let mut post_action = None;
if self.gui.settings.0 == false && self.gui.settings.1.is_none() {
if let Some(key) = virtual_key_code {
let keybind = KeyBinding::new(&self.modifiers, key);
if let Some(action) = self.keybinds.get(&keybind) {
if action.has_priority() {
e.take();
for a in self.key_actions.get(&action.id()).execute() {
self.exec_gui_action(a);
}
} else {
post_action = Some(action.id());
}
}
}
}
for a in self.gui._keyboard_event(
&mut EventInfo::new(),
true,
&mut |g, e, a| {
if g.config().keyboard_events_focus {
a.append(&mut g.key_focus(
e,
self.modifiers.clone(),
false,
virtual_key_code,
scancode,
));
}
},
&mut |g, e, a| {
if g.config().keyboard_events_watch {
a.append(&mut g.key_watch(
e,
self.modifiers.clone(),
false,
virtual_key_code,
scancode,
));
}
},
) {
self.exec_gui_action(a);
}
if let Some(post_action) = post_action.take() {
if e.take() {
for a in self.key_actions.get(&post_action).execute() {
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(),
#[cfg(feature = "merscfg")]
GuiEvent::RefreshMers => {
if let Some(mut cfg) = self.gui_config.take() {
MersCfg::run(&mut cfg, self, |cfg| &crate::merscfg::OptFunc(None));
self.gui_config = Some(cfg);
}
}
GuiEvent::UpdatedLibrary => {
#[cfg(feature = "merscfg")]
if let Some(mut gc) = self.gui_config.take() {
MersCfg::run(&mut gc, self, |m| &m.func_library_updated);
self.gui_config = Some(gc);
} else {
eprintln!("WARN: Skipping call to merscfg's library_updated because gui_config is not available");
}
self.gui._recursive_all(true, &mut |e| e.updated_library());
helper.request_redraw();
}
GuiEvent::UpdatedQueue => {
#[cfg(feature = "merscfg")]
if let Some(mut gc) = self.gui_config.take() {
MersCfg::run(&mut gc, self, |m| &m.func_queue_updated);
self.gui_config = Some(gc);
} else {
eprintln!("WARN: Skipping call to merscfg's queue_updated because gui_config is not available");
}
self.gui._recursive_all(true, &mut |e| e.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(true, &mut |e| e.config_mut().redraw = true);
}
}
pub enum GuiServerImage {
Loading(JoinHandle<Option<Vec<u8>>>),
Loaded(ImageHandle),
Error,
}
#[allow(unused)]
impl GuiServerImage {
pub fn new_cover<T: ClientIo + 'static>(
id: CoverId,
get_con: Arc<Mutex<get::Client<T>>>,
) -> Self {
Self::Loading(std::thread::spawn(move || {
get_con
.lock()
.unwrap()
.cover_bytes(id)
.ok()
.and_then(|v| v.ok())
}))
}
pub fn new_custom_file<T: ClientIo + 'static>(
file: String,
get_con: Arc<Mutex<get::Client<T>>>,
) -> Self {
Self::Loading(std::thread::spawn(move || {
get_con
.lock()
.unwrap()
.custom_file(&file)
.ok()
.and_then(|v| v.ok())
}))
}
pub fn get(&self) -> Option<ImageHandle> {
match self {
Self::Loaded(handle) => Some(handle.clone()),
Self::Loading(_) | Self::Error => None,
}
}
pub fn is_err(&self) -> bool {
matches!(self, Self::Error)
}
pub fn get_init(&mut self, g: &mut Graphics2D) -> Option<ImageHandle> {
match self {
Self::Loaded(handle) => Some(handle.clone()),
Self::Error => None,
Self::Loading(t) => {
if t.is_finished() {
let s = std::mem::replace(self, Self::Error);
if let Self::Loading(t) = s {
match t.join().unwrap() {
Some(bytes) => match g.create_image_from_file_bytes(
None,
speedy2d::image::ImageSmoothingMode::Linear,
Cursor::new(bytes),
) {
Ok(handle) => {
*self = Self::Loaded(handle.clone());
Some(handle)
}
Err(e) => {
eprintln!("[info] couldn't load cover from bytes: {e}");
None
}
},
None => None,
}
} else {
*self = s;
None
}
} else {
None
}
}
}
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub struct KeyBinding {
pub modifiers: u8,
pub key: VirtualKeyCode,
}
impl KeyBinding {
const CTRL: u8 = 1;
const ALT: u8 = 2;
const SHIFT: u8 = 4;
const META: u8 = 8;
pub fn new(modifiers: &ModifiersState, key: VirtualKeyCode) -> Self {
Self {
modifiers: if modifiers.ctrl() { Self::CTRL } else { 0 }
| if modifiers.alt() { Self::ALT } else { 0 }
| if modifiers.shift() { Self::SHIFT } else { 0 }
| if modifiers.logo() { Self::META } else { 0 },
key,
}
}
pub fn ctrl(key: VirtualKeyCode) -> Self {
Self {
modifiers: Self::CTRL,
key,
}
}
pub fn ctrl_shift(key: VirtualKeyCode) -> Self {
Self {
modifiers: Self::CTRL | Self::SHIFT,
key,
}
}
pub fn get_ctrl(&self) -> bool {
self.modifiers & Self::CTRL != 0
}
pub fn get_alt(&self) -> bool {
self.modifiers & Self::ALT != 0
}
pub fn get_shift(&self) -> bool {
self.modifiers & Self::SHIFT != 0
}
pub fn get_meta(&self) -> bool {
self.modifiers & Self::META != 0
}
}
pub struct KeyAction {
pub category: String,
pub title: String,
pub description: String,
pub action: Box<dyn FnMut() -> Vec<GuiAction>>,
pub enabled: bool,
}
impl KeyAction {
pub fn execute(&mut self) -> Vec<GuiAction> {
if self.enabled {
(self.action)()
} else {
vec![]
}
}
}
#[derive(Default)]
pub struct KeyActions(Vec<KeyAction>);
#[derive(Clone, Copy)]
pub struct KeyActionId(usize);
#[derive(Clone, Copy)]
pub struct KeyActionRef(usize, bool);
impl KeyActionId {
pub fn get_index(&self) -> usize {
self.0
}
pub fn with_priority(&self, priority: bool) -> KeyActionRef {
KeyActionRef(self.0, priority)
}
}
impl KeyActionRef {
pub fn get_index(&self) -> usize {
self.0
}
pub fn id(&self) -> KeyActionId {
KeyActionId(self.0)
}
pub fn has_priority(&self) -> bool {
self.1
}
pub fn set_priority(&mut self, priority: bool) {
self.1 = priority;
}
}
impl KeyActions {
pub fn add(&mut self, action: KeyAction) -> KeyActionId {
let id = KeyActionId(self.0.len());
self.0.push(action);
id
}
pub fn get(&mut self, action: &KeyActionId) -> &mut KeyAction {
&mut self.0[action.0]
}
pub fn view(&self) -> &[KeyAction] {
&self.0
}
pub fn iter(&self) -> impl Iterator<Item = (KeyActionId, &KeyAction)> {
self.0.iter().enumerate().map(|(i, v)| (KeyActionId(i), v))
}
}
pub fn morph_rect(a: &Rectangle, b: &Rectangle, p: f32) -> Rectangle {
let q = 1.0 - p;
Rectangle::from_tuples(
(
a.top_left().x * q + b.top_left().x * p,
a.top_left().y * q + b.top_left().y * p,
),
(
a.bottom_right().x * q + b.bottom_right().x * p,
a.bottom_right().y * q + b.bottom_right().y * p,
),
)
}
pub fn rect_from_rel(v: &Rectangle, outer: &Rectangle) -> Rectangle {
Rectangle::from_tuples(
(
outer.top_left().x + v.top_left().x * outer.width(),
outer.top_left().y + v.top_left().y * outer.height(),
),
(
outer.top_left().x + v.bottom_right().x * outer.width(),
outer.top_left().y + v.bottom_right().y * outer.height(),
),
)
}