diff --git a/Cargo.toml b/Cargo.toml index c348463..f4e8357 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ canary-script = { path = "crates/script" } lyon = "1" ouroboros = "^0.15" parking_lot = "0.12" +prehash = "0.3.3" slab = "0.4" wasmtime = "0.38" diff --git a/apps/magpie/Cargo.toml b/apps/magpie/Cargo.toml index 86c3ebb..86fd88b 100644 --- a/apps/magpie/Cargo.toml +++ b/apps/magpie/Cargo.toml @@ -2,6 +2,7 @@ name = "canary-magpie" version = "0.1.0" edition = "2021" +license = "AGPL-3.0-or-later" [[bin]] name = "magpie" @@ -13,6 +14,7 @@ anyhow = { version = "1", optional = true } byteorder = "1.4" canary = { path = "../..", optional = true } canary-wgpu = { path = "../../renderers/wgpu", optional = true } +futures-util = { version = "0.3", optional = true, features = ["io"] } mio = { version = "0.8", features = ["net", "os-poll"], optional = true } mio-signals = { version = "0.2", optional = true } parking_lot = { version = "0.12", optional = true } @@ -23,6 +25,7 @@ slab = { version = "0.4", optional = true } winit = { version = "0.27", optional = true } [features] +async = ["dep:futures-util"] service = [ "dep:anyhow", "dep:canary", diff --git a/apps/magpie/src/client.rs b/apps/magpie/src/client.rs deleted file mode 100644 index 5714552..0000000 --- a/apps/magpie/src/client.rs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2022 Marceline Cramer -// SPDX-License-Identifier: AGPL-3.0-or-later - -use serde::Serialize; - -use std::os::unix::net::UnixStream; -use std::path::Path; - -use crate::protocol::{ClientMessenger, MagpieServerMsg, PanelId, SendMessage, MAGPIE_SOCK}; - -/// A client to a Magpie server. -pub struct MagpieClient { - pub messenger: ClientMessenger, -} - -impl MagpieClient { - pub fn new() -> std::io::Result { - let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set"); - let sock_dir = Path::new(&sock_dir); - let sock_path = sock_dir.join(MAGPIE_SOCK); - let socket = UnixStream::connect(sock_path)?; - Ok(Self { - messenger: ClientMessenger::new(socket), - }) - } - - pub fn send(&mut self, msg: &MagpieServerMsg) { - if let Err(err) = self.messenger.send(msg) { - eprintln!("Message send error: {:?}", err); - } - } - - pub fn send_json_message(&mut self, id: PanelId, msg: &T) { - let msg = serde_json::to_string(msg).unwrap(); - eprintln!("Sending message: {}", msg); - - let msg = SendMessage { - id, - msg: msg.into_bytes(), - }; - - self.send(&MagpieServerMsg::SendMessage(msg)); - } -} diff --git a/apps/magpie/src/lib.rs b/apps/magpie/src/lib.rs index c126da0..c17fe70 100644 --- a/apps/magpie/src/lib.rs +++ b/apps/magpie/src/lib.rs @@ -1,7 +1,6 @@ // Copyright (c) 2022 Marceline Cramer // SPDX-License-Identifier: AGPL-3.0-or-later -pub mod client; pub mod protocol; #[cfg(feature = "service")] diff --git a/apps/magpie/src/protocol.rs b/apps/magpie/src/protocol.rs index 6d83b6f..eacf451 100644 --- a/apps/magpie/src/protocol.rs +++ b/apps/magpie/src/protocol.rs @@ -43,71 +43,58 @@ pub enum MagpieServerMsg { SendMessage(SendMessage), } +/// A message sent from a script's panel to a client. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RecvMessage { + pub id: PanelId, + pub msg: Vec, +} + /// A message sent from the Magpie server to a client. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "kind")] -pub enum MagpieClientMsg {} +pub enum MagpieClientMsg { + RecvMessage(RecvMessage), +} /// A [Messenger] specialized for Magpie clients. pub type ClientMessenger = Messenger; +impl ClientMessenger { + pub fn send_panel_json(&mut self, id: PanelId, msg: &O) { + let msg = serde_json::to_string(msg).unwrap(); + eprintln!("Sending message: {:?}", msg); + + let _ = self.send(&MagpieServerMsg::SendMessage(SendMessage { + id, + msg: msg.into_bytes(), + })); + } +} + /// A [Messenger] specialized for Magpie servers. pub type ServerMessenger = Messenger; -/// Bidirectional, transport-agnostic Magpie IO wrapper struct. -pub struct Messenger { - pub transport: T, +/// Piecewise packet assembler for [Messenger]. +pub struct MessageQueue { expected_len: Option, received_buf: VecDeque, received_queue: VecDeque, - closed: bool, - _output: PhantomData, } -impl Messenger { - pub fn new(transport: T) -> Self { +impl Default for MessageQueue { + fn default() -> Self { Self { - transport, expected_len: None, received_buf: Default::default(), received_queue: Default::default(), - closed: false, - _output: PhantomData, } } +} - pub fn is_closed(&self) -> bool { - self.closed - } - - pub fn send(&mut self, msg: &O) -> std::io::Result<()> { - use byteorder::{LittleEndian, WriteBytesExt}; - let payload = serde_json::to_vec(msg).unwrap(); - let len = payload.len() as u32; - self.transport.write_u32::(len)?; - self.transport.write_all(&payload)?; - self.transport.flush()?; - Ok(()) - } - - /// Receives all pending messages and queues them for [recv]. - pub fn flush_recv(&mut self) -> std::io::Result<()> { - let mut buf = [0u8; 1024]; - - loop { - match self.transport.read(&mut buf) { - Ok(0) => { - self.closed = true; - break; - } - Ok(n) => { - self.received_buf.write(&buf[..n])?; - } - Err(ref err) if err.kind() == std::io::ErrorKind::WouldBlock => break, - Err(ref err) if err.kind() == std::io::ErrorKind::Interrupted => continue, - Err(err) => return Err(err), - } - } +impl MessageQueue { + pub fn on_data(&mut self, data: &[u8]) -> std::io::Result<()> { + self.received_buf.write_all(data)?; loop { if let Some(expected_len) = self.expected_len { @@ -139,8 +126,137 @@ impl Messenger { Ok(()) } - /// Tries to receive a single input packet. pub fn recv(&mut self) -> Option { self.received_queue.pop_back() } } + +/// Bidirectional, transport-agnostic Magpie IO wrapper struct. +pub struct Messenger { + transport: T, + queue: MessageQueue, + closed: bool, + _output: PhantomData, +} + +impl Messenger { + pub fn new(transport: T) -> Self { + Self { + transport, + queue: Default::default(), + closed: false, + _output: PhantomData, + } + } + + pub fn is_closed(&self) -> bool { + self.closed + } + + /// Destroys this messenger and returns the inner transport. + pub fn into_transport(self) -> T { + self.transport + } +} + +impl Messenger { + pub fn send(&mut self, msg: &O) -> std::io::Result<()> { + use byteorder::{LittleEndian, WriteBytesExt}; + let payload = serde_json::to_vec(msg).unwrap(); + let len = payload.len() as u32; + self.transport.write_u32::(len)?; + self.transport.write_all(&payload)?; + self.transport.flush()?; + Ok(()) + } +} + +impl Messenger { + /// Synchronously receives all pending messages and queues them for [recv]. + /// + /// This function only works if the transport is in non-blocking mode. + /// Otherwise, this may block while waiting for more data, even if the + /// data it receives does not add up to a full message. + pub fn flush_recv(&mut self) -> std::io::Result<()> { + let mut buf = [0u8; 1024]; + + loop { + match self.transport.read(&mut buf) { + Ok(0) => { + self.closed = true; + break; + } + Err(ref err) if err.kind() == std::io::ErrorKind::ConnectionReset => { + self.closed = true; + break; + } + Ok(n) => { + self.queue.on_data(&buf[..n])?; + } + Err(ref err) if err.kind() == std::io::ErrorKind::WouldBlock => break, + Err(ref err) if err.kind() == std::io::ErrorKind::Interrupted => continue, + Err(err) => return Err(err), + } + } + + Ok(()) + } + + /// Tries to receive a single input packet. + /// + /// For messages to be received here, [flush_recv] must be called to + /// continuously read pending data from the transport. + pub fn try_recv(&mut self) -> Option { + self.queue.recv() + } +} + +#[cfg(feature = "async")] +mod async_messages { + use super::*; + use futures_util::{AsyncReadExt, AsyncWriteExt}; + use std::marker::Unpin; + + impl ClientMessenger { + pub async fn send_panel_json_async(&mut self, id: PanelId, msg: &O) { + let msg = serde_json::to_string(msg).unwrap(); + eprintln!("Sending message: {:?}", msg); + + let _ = self + .send_async(&MagpieServerMsg::SendMessage(SendMessage { + id, + msg: msg.into_bytes(), + })) + .await; + } + } + + impl Messenger { + pub async fn send_async(&mut self, msg: &O) -> std::io::Result<()> { + use byteorder::{LittleEndian, WriteBytesExt}; + let payload = serde_json::to_vec(msg).unwrap(); + let len = payload.len() as u32; + let mut msg = Vec::with_capacity(4 + payload.len()); + msg.write_u32::(len)?; + msg.extend_from_slice(&payload); + self.transport.write_all(&msg).await?; + self.transport.flush().await?; + Ok(()) + } + } + + impl Messenger { + pub async fn recv(&mut self) -> std::io::Result { + let mut buf = [0u8; 1024]; + + loop { + if let Some(msg) = self.queue.recv() { + return Ok(msg); + } + + let num = self.transport.read(&mut buf).await?; + self.queue.on_data(&buf[..num])?; + } + } + } +} diff --git a/apps/magpie/src/service/ipc.rs b/apps/magpie/src/service/ipc.rs index 869e297..ad9dce2 100644 --- a/apps/magpie/src/service/ipc.rs +++ b/apps/magpie/src/service/ipc.rs @@ -14,18 +14,29 @@ use mio_signals::{Signal, Signals}; use parking_lot::RwLock; use slab::Slab; -use crate::protocol::{CreatePanel, MagpieServerMsg, SendMessage, ServerMessenger}; +use crate::protocol::*; use crate::service::window::{WindowMessage, WindowMessageSender}; const SOCK_NAME: &str = "magpie.sock"; -pub enum IpcMessage {} +#[derive(Debug)] +pub enum IpcMessage { + PanelMessage { window: usize, message: Vec }, +} +#[derive(Clone)] pub struct IpcMessageSender { - waker: Waker, + waker: Arc, sender: Sender, } +impl IpcMessageSender { + pub fn send(&self, msg: IpcMessage) { + let _ = self.sender.send(msg); + let _ = self.waker.wake(); + } +} + /// Wraps [mio::net::UnixListener] with automatic file deletion on drop. pub struct Listener { pub uds: UnixListener, @@ -55,8 +66,38 @@ impl DerefMut for Listener { } } +impl Listener { + fn new() -> std::io::Result { + let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set"); + let sock_dir = Path::new(&sock_dir); + let sock_path = sock_dir.join(SOCK_NAME); + + use std::io::{Error, ErrorKind}; + match UnixStream::connect(&sock_path) { + Ok(_) => { + eprintln!("Socket is already in use. Another instance of Magpie may be running."); + let kind = ErrorKind::AddrInUse; + let error = Error::new(kind, "Socket is already in use."); + return Err(error); + } + Err(ref err) if err.kind() == ErrorKind::ConnectionRefused => { + eprintln!("Found leftover socket; removing."); + std::fs::remove_file(&sock_path)?; + } + Err(ref err) if err.kind() == ErrorKind::NotFound => {} + Err(err) => return Err(err), + } + + eprintln!("Making socket at: {:?}", sock_path); + let uds = UnixListener::bind(&sock_path)?; + let path = sock_path.to_path_buf(); + Ok(Self { uds, path }) + } +} + pub struct IpcData { poll: Poll, + window_to_client_panel: HashMap, next_window_id: usize, } @@ -76,27 +117,13 @@ pub struct Client { id_to_window: HashMap, } -impl Drop for Client { - fn drop(&mut self) { - println!("Client #{} disconnected", self.token.0); - let data = self.data.write(); - let _ = data - .poll - .registry() - .deregister(&mut self.messenger.transport); - - for (_id, window) in self.id_to_window.drain() { - let msg = WindowMessage::CloseWindow { id: window }; - let _ = self.window_sender.send_event(msg); - } - } -} - impl Client { pub fn on_readable(&mut self) -> std::io::Result { - self.messenger.flush_recv()?; + if let Err(err) = self.messenger.flush_recv() { + eprintln!("flush_recv() error: {:?}", err); + } - while let Some(msg) = self.messenger.recv() { + while let Some(msg) = self.messenger.try_recv() { println!("Client #{}: {:?}", self.token.0, msg); match msg { MagpieServerMsg::CreatePanel(CreatePanel { @@ -104,7 +131,11 @@ impl Client { protocol, script, }) => { - let window = self.data.write().new_window_id(); + let mut data = self.data.write(); + + let window = data.new_window_id(); + data.window_to_client_panel + .insert(window, (self.token.0, id)); if let Some(old_id) = self.id_to_window.insert(id, window) { let msg = WindowMessage::CloseWindow { id: old_id }; @@ -129,13 +160,26 @@ impl Client { Ok(self.messenger.is_closed()) } + + pub fn disconnect(mut self) { + println!("Client #{} disconnected", self.token.0); + + let mut transport = self.messenger.into_transport(); + let mut data = self.data.write(); + let _ = data.poll.registry().deregister(&mut transport); + + for (_id, window) in self.id_to_window.drain() { + let msg = WindowMessage::CloseWindow { id: window }; + let _ = self.window_sender.send_event(msg); + data.window_to_client_panel.remove(&window); + } + } } pub struct Ipc { pub data: Arc>, pub window_sender: WindowMessageSender, pub message_recv: Receiver, - pub events: Events, pub quit: bool, pub listener: Listener, pub signals: Signals, @@ -147,19 +191,10 @@ pub struct Ipc { impl Ipc { pub fn new(window_sender: WindowMessageSender) -> std::io::Result<(Self, IpcMessageSender)> { - let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set"); - let sock_dir = Path::new(&sock_dir); - let sock_path = sock_dir.join(SOCK_NAME); - eprintln!("Making socket at: {:?}", sock_path); - - let mut listener = Listener { - uds: UnixListener::bind(&sock_path)?, - path: sock_path.to_path_buf(), - }; + let mut listener = Listener::new()?; let mut signals = Signals::new(Signal::Interrupt | Signal::Quit)?; - let events = Events::with_capacity(128); let poll = Poll::new()?; let listener_token = Token(usize::MAX); let signals_token = Token(listener_token.0 - 1); @@ -173,12 +208,13 @@ impl Ipc { let (sender, message_recv) = channel(); let sender = IpcMessageSender { - waker: Waker::new(registry, message_recv_token)?, + waker: Arc::new(Waker::new(registry, message_recv_token)?), sender, }; let data = IpcData { poll, + window_to_client_panel: HashMap::new(), next_window_id: 0, }; @@ -186,7 +222,6 @@ impl Ipc { data: Arc::new(RwLock::new(data)), window_sender, message_recv, - events, quit: false, listener, signals, @@ -199,10 +234,29 @@ impl Ipc { Ok((ipc, sender)) } - pub fn poll(&mut self, timeout: Option) -> std::io::Result<()> { - self.data.write().poll.poll(&mut self.events, timeout)?; + pub fn on_message(&mut self, msg: IpcMessage) -> std::io::Result<()> { + match msg { + IpcMessage::PanelMessage { window, message } => { + let data = self.data.read(); + let (client, panel) = *data.window_to_client_panel.get(&window).unwrap(); + let client = self.clients.get_mut(client).unwrap(); + let reply = RecvMessage { + id: panel, + msg: message, + }; + client + .messenger + .send(&MagpieClientMsg::RecvMessage(reply))?; + } + } - for event in self.events.iter() { + Ok(()) + } + + pub fn poll(&mut self, events: &mut Events, timeout: Option) -> std::io::Result<()> { + self.data.write().poll.poll(events, timeout)?; + + for event in events.iter() { if event.token() == self.listener_token { loop { match self.listener.accept() { @@ -238,13 +292,17 @@ impl Ipc { let _ = self.window_sender.send_event(WindowMessage::Quit); self.quit = true; } + } else if event.token() == self.message_recv_token { + while let Ok(received) = self.message_recv.try_recv() { + self.on_message(received)?; + } } else if let Some(client) = self.clients.get_mut(event.token().0) { let disconnected = client.on_readable()?; if disconnected { - self.clients.remove(event.token().0); + self.clients.remove(event.token().0).disconnect(); } } else { - panic!("Unrecognized event token: {:?}", event); + eprintln!("Unrecognized event token: {:?}", event); } } @@ -252,9 +310,10 @@ impl Ipc { } pub fn run(mut self) { + let mut events = Events::with_capacity(128); while !self.quit { let wait = Duration::from_millis(100); - match self.poll(Some(wait)) { + match self.poll(&mut events, Some(wait)) { Ok(_) => {} Err(e) => { eprintln!("IPC poll error: {:?}", e); diff --git a/apps/magpie/src/service/window.rs b/apps/magpie/src/service/window.rs index 89ae286..717a1d1 100644 --- a/apps/magpie/src/service/window.rs +++ b/apps/magpie/src/service/window.rs @@ -6,7 +6,6 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::Instant; -use anyhow::anyhow; use canary::{CursorEventKind, Panel, Runtime, Vec2, PX_PER_MM}; use canary_wgpu::{wgpu, DrawTarget, Renderer}; use pollster::FutureExt; @@ -37,6 +36,8 @@ pub enum WindowMessage { pub type WindowMessageSender = EventLoopProxy; pub struct Window { + pub ipc_sender: IpcMessageSender, + pub ipc_id: usize, pub window: winit::window::Window, pub surface: wgpu::Surface, pub surface_config: wgpu::SurfaceConfiguration, @@ -51,6 +52,8 @@ pub struct Window { impl Window { pub fn new( + ipc_sender: IpcMessageSender, + ipc_id: usize, panel: Panel, instance: &wgpu::Instance, adapter: &wgpu::Adapter, @@ -78,6 +81,8 @@ impl Window { surface.configure(&device, &surface_config); Ok(Self { + ipc_sender, + ipc_id, window, surface, surface_config, @@ -99,11 +104,22 @@ impl Window { self.window.request_redraw(); } + /// Receives all messages from the script and forwards them to IPC. + pub fn recv_messages(&mut self) { + for message in self.panel.recv_messages() { + self.ipc_sender.send(IpcMessage::PanelMessage { + window: self.ipc_id, + message, + }); + } + } + pub fn update(&mut self) { let now = Instant::now(); let dt = now.duration_since(self.last_update).as_secs_f32(); self.panel.update(dt); self.last_update = now; + self.recv_messages(); } pub fn draw(&mut self) { @@ -124,15 +140,19 @@ impl Window { self.renderer.render(target, &commands); output.present(); + + self.recv_messages(); } pub fn send_message(&mut self, msg: Vec) { self.panel.on_message(msg); + self.recv_messages(); } pub fn resize(&mut self, new_size: PhysicalSize) { let mm = Vec2::new(new_size.width as f32, new_size.height as f32) * PX_PER_MM; self.panel.on_resize(mm); + self.recv_messages(); self.window.request_redraw(); if new_size.width > 0 && new_size.height > 0 { @@ -158,6 +178,7 @@ impl Window { }; self.panel.on_cursor_event(event, self.cursor_pos); + self.recv_messages(); } WindowEvent::MouseInput { state, @@ -176,6 +197,7 @@ impl Window { }; self.panel.on_cursor_event(event, self.cursor_pos); + self.recv_messages(); } _ => {} } @@ -251,12 +273,19 @@ impl WindowStore { message: WindowMessage, ) -> anyhow::Result { match message { - WindowMessage::OpenWindow { id, protocol, script } => { + WindowMessage::OpenWindow { + id, + protocol, + script, + } => { println!("Opening window {} with script {:?}", id, script); let module = std::fs::read(script)?; let mut script = self.runtime.load_module(&module)?; let panel = script.create_panel(&protocol, vec![])?; + let window = Window::new( + self.ipc_sender.to_owned(), + id, panel, &self.instance, &self.adapter, @@ -265,6 +294,7 @@ impl WindowStore { self.renderer.to_owned(), &event_loop, )?; + let window_id = window.get_id(); self.windows.insert(window_id, window); self.ipc_to_window.insert(id, window_id); diff --git a/apps/music-player/Cargo.toml b/apps/music-player/Cargo.toml index 2aed4fe..09ab086 100644 --- a/apps/music-player/Cargo.toml +++ b/apps/music-player/Cargo.toml @@ -10,10 +10,12 @@ path = "src/main.rs" required-features = ["bin"] [dependencies] -canary-magpie = { path = "../magpie", optional = true } -mpris = { version = "2.0.0-rc3", optional = true } +canary-magpie = { path = "../magpie", optional = true, features = ["async"] } +futures-util = { version = "0.3", optional = true } serde = { version = "1", features = ["derive"] } serde_json = "1" +smol = { version = "1.2", optional = true } +zbus = { version = "3.5", optional = true } [features] -bin = ["dep:canary-magpie", "dep:mpris"] +bin = ["dep:canary-magpie", "dep:futures-util", "dep:smol", "dep:zbus"] diff --git a/apps/music-player/src/lib.rs b/apps/music-player/src/lib.rs index d3ecabb..fa0e96d 100644 --- a/apps/music-player/src/lib.rs +++ b/apps/music-player/src/lib.rs @@ -34,9 +34,6 @@ pub enum LoopStatus { pub struct ProgressChanged { /// Current position into the track in seconds. pub position: f32, - - /// Length of the current track in seconds. - pub length: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -48,7 +45,7 @@ pub struct AlbumInfo { pub artists: Vec, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct TrackInfo { /// The title of the current track. pub title: Option, @@ -58,6 +55,9 @@ pub struct TrackInfo { /// The optional track number on the disc the album the track appears on. pub track_number: Option, + + /// Length of the track in seconds. + pub length: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -104,6 +104,6 @@ pub enum OutMsg { /// Sets the volume. Values are clamped to 0.0 to 1.0. SetVolume { volume: f32 }, - /// Set the current track position in seconds. - SetPosition { position: f32 }, + /// Seeks the current track's position in seconds. + Seek { offset: f32 }, } diff --git a/apps/music-player/src/main.rs b/apps/music-player/src/main.rs index 5f07f12..fb647ee 100644 --- a/apps/music-player/src/main.rs +++ b/apps/music-player/src/main.rs @@ -1,81 +1,193 @@ // Copyright (c) 2022 Marceline Cramer // SPDX-License-Identifier: AGPL-3.0-or-later -use canary_music_player::*; -use canary_magpie::client::MagpieClient; -use canary_magpie::protocol::{CreatePanel, MagpieServerMsg}; -use canary_music_player::*; -use mpris::PlayerFinder; +use std::path::Path; -pub struct MetadataTracker { +use canary_magpie::protocol::{ + ClientMessenger, CreatePanel, MagpieClientMsg, MagpieServerMsg, RecvMessage, MAGPIE_SOCK, +}; +use canary_music_player::*; +use smol::net::unix::UnixStream; + +pub type MagpieClient = ClientMessenger; + +pub mod mpris; + +use mpris::*; + +#[derive(Debug)] +pub struct Metadata { pub album: AlbumInfo, pub track: TrackInfo, } -impl From<&mpris::Metadata> for MetadataTracker { - fn from(metadata: &mpris::Metadata) -> Self { +impl<'a> From> for Metadata { + fn from(map: MetadataMap<'a>) -> Self { let album = AlbumInfo { - title: metadata.album_name().map(ToString::to_string), - artists: metadata - .album_artists() - .unwrap_or(Vec::new()) - .iter() - .map(ToString::to_string) - .collect(), + title: map + .get("xesam:album") + .and_then(|v| TryFrom::try_from(v).ok()), + artists: map + .get("xesam:albumArtist") + .cloned() + .and_then(|v| TryFrom::try_from(v).ok()) + .unwrap_or(Vec::new()), }; let track = TrackInfo { - title: metadata.title().map(ToString::to_string), - artists: metadata - .artists() - .unwrap_or(Vec::new()) - .iter() - .map(ToString::to_string) - .collect(), - track_number: metadata.track_number(), + title: map + .get("xesam:title") + .and_then(|v| TryFrom::try_from(v).ok()), + artists: map + .get("xesam:artist") + .cloned() + .and_then(|v| TryFrom::try_from(v).ok()) + .unwrap_or(Vec::new()), + track_number: map + .get("xesam:trackNumber") + .and_then(|v| TryFrom::try_from(v).ok()), + length: map + .get("mpris:length") + .and_then(|v| i64::try_from(v).ok()) + .map(|us| us as f32 / 1_000_000.0), // 1,000,000 microseconds in a second }; Self { album, track } } } -impl MetadataTracker { - pub fn new(magpie: &mut MagpieClient, metadata: &mpris::Metadata) -> Self { +impl Metadata { + pub async fn update_new(magpie: &mut MagpieClient, metadata: MetadataMap<'_>) -> Self { let new: Self = metadata.into(); - magpie.send_json_message(0, &InMsg::AlbumChanged(new.album.clone())); - magpie.send_json_message(0, &InMsg::TrackChanged(new.track.clone())); - magpie.send_json_message( - 0, - &InMsg::ProgressChanged(ProgressChanged { - position: 0.0, - length: metadata.length().map(|l| l.as_secs_f32()), - }), - ); + let msg = InMsg::AlbumChanged(new.album.clone()); + magpie.send_panel_json_async(0, &msg).await; + let msg = InMsg::TrackChanged(new.track.clone()); + magpie.send_panel_json_async(0, &msg).await; new } - pub fn update(&mut self, messenger: &mut MagpieClient, metadata: &mpris::Metadata) { + pub async fn update_diff(&mut self, messenger: &mut MagpieClient, metadata: MetadataMap<'_>) { let new: Self = metadata.into(); if self.album != new.album { - messenger.send_json_message(0, &InMsg::AlbumChanged(new.album.clone())); + let msg = InMsg::AlbumChanged(new.album.clone()); + messenger.send_panel_json_async(0, &msg).await; } if self.track != new.track { - messenger.send_json_message(0, &InMsg::TrackChanged(new.track.clone())); - messenger.send_json_message( - 0, - &InMsg::ProgressChanged(ProgressChanged { - position: 0.0, - length: metadata.length().map(|l| l.as_secs_f32()), - }), - ); + let msg = InMsg::TrackChanged(new.track.clone()); + messenger.send_panel_json_async(0, &msg).await; + let progress = ProgressChanged { position: 0.0 }; + let msg = InMsg::ProgressChanged(progress); + messenger.send_panel_json_async(0, &msg).await; } *self = new; } } +async fn on_message( + player: &PlayerProxy<'_>, + magpie: &mut MagpieClient, + message: MagpieClientMsg, +) -> Result<(), Box> { + let message = match message { + MagpieClientMsg::RecvMessage(RecvMessage { id: 0, msg }) => msg, + _ => return Ok(()), + }; + + let message: OutMsg = match serde_json::from_slice(&message) { + Ok(v) => v, + Err(err) => { + eprintln!("Panel message parse error: {:?}", err); + return Ok(()); + } + }; + + match message { + OutMsg::Pause => player.pause().await?, + OutMsg::Play => player.play().await?, + OutMsg::PlayPause => player.play_pause().await?, + OutMsg::Stop => player.stop().await?, + OutMsg::Previous => player.previous().await?, + OutMsg::Next => player.next().await?, + OutMsg::Seek { offset } => { + let offset = (offset * 1_000_000.0) as i64; // Seconds to microseconds + player.seek(offset).await?; + } + _ => {} + } + + Ok(()) +} + +async fn player_main( + player: &PlayerProxy<'_>, + magpie: &mut MagpieClient, +) -> Result<(), Box> { + use futures_util::StreamExt; + let mut playback_status = player.receive_playback_status_changed().await.fuse(); + let mut metadata_tracker = player.receive_metadata_changed().await.fuse(); + let mut position_tracker = player.receive_position_changed().await.fuse(); + + let mut metadata = Metadata::update_new(magpie, player.metadata().await?).await; + use futures_util::FutureExt; + + loop { + futures_util::select! { + msg = magpie.recv().fuse() => { + match msg { + Ok(msg) => on_message(player, magpie, msg).await?, + Err(err) => eprintln!("Magpie recv error: {:?}", err), + } + } + // TODO also update volume, shuffle status, and loop status + status = playback_status.next() => { + let status = match status { + Some(v) => v, + None => break, + }; + + let status = status.get().await?; + let status = match status.as_str() { + "Playing" => Some(PlaybackStatus::Playing), + "Paused" => Some(PlaybackStatus::Paused), + "Stopped" => Some(PlaybackStatus::Stopped), + _ => None, + }; + + if let Some(status) = status { + let msg = InMsg::PlaybackStatusChanged(status); + magpie.send_panel_json_async(0, &msg).await; + } + } + position = position_tracker.next() => { + let position = match position { + Some(v) => v, + None => break, + }; + + let position = position.get().await?; + let position = position as f32 / 1_000_000.0; // Microseconds to seconds + let progress = ProgressChanged { position }; + let msg = InMsg::ProgressChanged(progress); + magpie.send_panel_json_async(0, &msg).await; + } + new_metadata = metadata_tracker.next() => { + let new_metadata = match new_metadata { + Some(v) => v, + None => break, + }; + + let new_metadata = new_metadata.get().await?; + metadata.update_diff(magpie, new_metadata).await; + } + }; + } + + Ok(()) +} + fn main() { let args: Vec = std::env::args().collect(); let module_path = args @@ -83,105 +195,71 @@ fn main() { .expect("Please pass a path to a Canary script!") .to_owned(); - let player_finder = PlayerFinder::new().expect("Could not connect to D-Bus"); - - let mut magpie = MagpieClient::new().unwrap(); - let protocol = "tebibyte-media.desktop.music-player-controller".to_string(); - let script = std::path::PathBuf::from(&module_path); - let msg = CreatePanel { id: 0, protocol, script }; - let msg = MagpieServerMsg::CreatePanel(msg); - magpie.messenger.send(&msg).unwrap(); - - let mut first_loop = true; - let mut connected = false; - - loop { - if !first_loop { - let wait = std::time::Duration::from_secs(1); - std::thread::sleep(wait); - } - - first_loop = false; - - if connected { - println!("Disconnected from MPRIS"); - let msg = InMsg::Disconnected; - magpie.send_json_message(0, &msg); - connected = false; - } - - println!("Connecting to MPRIS..."); - - let player = match player_finder.find_active() { - Ok(player) => player, - Err(err) => { - eprintln!("Couldn't find player: {:?}", err); - continue; - } + smol::block_on(async { + let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set"); + let sock_dir = Path::new(&sock_dir); + let sock_path = sock_dir.join(MAGPIE_SOCK); + let socket = UnixStream::connect(sock_path).await.unwrap(); + let mut magpie = MagpieClient::new(socket); + let protocol = "tebibyte-media.desktop.music-player-controller".to_string(); + let script = std::path::PathBuf::from(&module_path); + let msg = CreatePanel { + id: 0, + protocol, + script, }; - println!( - "Connected to \"{}\" ({})", - player.identity(), - player.bus_name() - ); - connected = true; - magpie.send_json_message(0, &InMsg::Connected); + let msg = MagpieServerMsg::CreatePanel(msg); + magpie.send_async(&msg).await.unwrap(); - let metadata = player.get_metadata().unwrap(); - let mut metadata_tracker = MetadataTracker::new(&mut magpie, &metadata); + let dbus = zbus::Connection::session().await.unwrap(); - let mut events = match player.events() { - Ok(events) => events, - Err(err) => { - eprintln!("Player events D-Bus error: {:?}", err); - continue; - } - }; + let mut first_loop = true; + let mut connected = false; loop { - let event = match events.next() { - None => break, - Some(Ok(e)) => e, - Some(Err(err)) => { - eprintln!("D-Bus error while reading player events: {:?}", err); + if !first_loop { + let wait = std::time::Duration::from_secs(1); + std::thread::sleep(wait); + } + + first_loop = false; + + if connected { + println!("Disconnected from MPRIS"); + let msg = InMsg::Disconnected; + magpie.send_panel_json_async(0, &msg).await; + connected = false; + } + + println!("Connecting to MPRIS..."); + + let player = match find_player(&dbus).await { + Ok(Some(player)) => player, + Ok(None) => { + eprintln!("Couldn't find player"); continue; } - }; - - use mpris::Event::*; - let in_msg = match event { - Playing => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Playing)), - Paused => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Paused)), - Stopped => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Stopped)), - LoopingChanged(status) => { - use mpris::LoopStatus::*; - let status = match status { - None => LoopStatus::None, - Track => LoopStatus::Track, - Playlist => LoopStatus::Playlist, - }; - - Some(InMsg::LoopingChanged(status)) - } - ShuffleToggled(shuffle) => Some(InMsg::ShuffleChanged { shuffle }), - VolumeChanged(volume) => Some(InMsg::VolumeChanged { - volume: volume as f32, - }), - PlayerShutDown => None, - TrackChanged(ref metadata) => { - metadata_tracker.update(&mut magpie, metadata); - None - } - _ => { - eprintln!("Unhandled MPRIS message: {:?}", event); - None + Err(err) => { + eprintln!("D-Bus error while finding player: {:?}", err); + return; } }; - if let Some(msg) = in_msg { - magpie.send_json_message(0, &msg); + println!( + "Connected to \"{}\" ({})", + player.path().as_str(), + player.destination().as_str() + ); + connected = true; + magpie.send_panel_json_async(0, &InMsg::Connected).await; + + match player_main(&player, &mut magpie).await { + Ok(()) => {} + Err(err) => { + eprintln!("D-Bus error while connected to player: {:?}", err); + } } } - } + }); } diff --git a/apps/music-player/src/mpris.rs b/apps/music-player/src/mpris.rs new file mode 100644 index 0000000..fb06bf0 --- /dev/null +++ b/apps/music-player/src/mpris.rs @@ -0,0 +1,51 @@ +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::collections::HashMap; + +use zbus::fdo::DBusProxy; +use zbus::zvariant::Value; +use zbus::{dbus_proxy, Connection, Result}; + +pub type MetadataMap<'a> = HashMap>; + +#[dbus_proxy( + interface = "org.mpris.MediaPlayer2.Player", + default_path = "/org/mpris/MediaPlayer2" +)] +trait Player { + fn next(&self) -> Result<()>; + fn previous(&self) -> Result<()>; + fn pause(&self) -> Result<()>; + fn play_pause(&self) -> Result<()>; + fn stop(&self) -> Result<()>; + fn play(&self) -> Result<()>; + fn seek(&self, offset: i64) -> Result<()>; + + #[dbus_proxy(property)] + fn playback_status(&self) -> Result; + + #[dbus_proxy(property)] + fn position(&self) -> Result; + + #[dbus_proxy(property)] + fn metadata(&self) -> Result; +} + +pub async fn find_player(connection: &Connection) -> Result> { + let dbus = DBusProxy::new(connection).await?; + let names = dbus.list_names().await?; + + for name in names { + let name = name.as_str().to_string(); + if name.starts_with("org.mpris.MediaPlayer2") { + let player = PlayerProxy::builder(connection) + .destination(name)? + .build() + .await?; + return Ok(Some(player)); + } + } + + Ok(None) +} diff --git a/apps/sandbox/src/main.rs b/apps/sandbox/src/main.rs index 00cc1f2..2ffe6e8 100644 --- a/apps/sandbox/src/main.rs +++ b/apps/sandbox/src/main.rs @@ -33,6 +33,7 @@ struct App { last_update: Instant, protocol_buf: String, bind_message_buf: String, + panel_bg: egui::Color32, } impl App { @@ -59,6 +60,7 @@ impl App { last_update: Instant::now(), protocol_buf: String::new(), bind_message_buf: String::new(), + panel_bg: egui::Color32::TRANSPARENT, } } } @@ -68,6 +70,8 @@ impl eframe::App for App { ctx.request_repaint(); egui::SidePanel::left("left_panel").show(ctx, |ui| { + ui.heading("New Panel"); + ui.label("Protocol name:"); ui.text_edit_singleline(&mut self.protocol_buf); @@ -91,6 +95,14 @@ impl eframe::App for App { self.panels.push(panel); } + + ui.separator(); + ui.heading("Global Settings"); + + ui.horizontal(|ui| { + ui.label("Panel background color: "); + ui.color_edit_button_srgba(&mut self.panel_bg); + }); }); let dt = self.last_update.elapsed().as_secs_f32(); @@ -98,7 +110,7 @@ impl eframe::App for App { for panel in self.panels.iter_mut() { panel.panel.update(dt); - panel.show(ctx); + panel.show(self.panel_bg, ctx); } } } @@ -112,83 +124,87 @@ pub struct PanelWindow { } impl PanelWindow { - pub fn show(&mut self, ctx: &egui::Context) { + pub fn show(&mut self, bg: egui::Color32, ctx: &egui::Context) { + let frame = egui::Frame::window(&ctx.style()).fill(bg); let window_id = egui::Id::new(format!("panel_{}", self.index)); - egui::Window::new("Panel").id(window_id).show(ctx, |ui| { - egui::menu::bar(ui, |ui| { - ui.checkbox(&mut self.show_msg, "Show Message Editor"); - }); + egui::Window::new("Panel") + .frame(frame) + .id(window_id) + .show(ctx, |ui| { + egui::menu::bar(ui, |ui| { + ui.checkbox(&mut self.show_msg, "Show Message Editor"); + }); - let sense = egui::Sense { - click: true, - drag: true, - focusable: true, - }; - - let desired_size = ui.available_size(); - let response = ui.allocate_response(desired_size, sense); - let rect = response.rect; - - if rect.size() != self.current_size { - let size = rect.size(); - self.current_size = size; - - let size = canary::Vec2::new(size.x, size.y); - self.panel.on_resize(size * PX_PER_MM); - } - - if let Some(hover_pos) = response.hover_pos() { - let local = (hover_pos - rect.left_top()) * PX_PER_MM; - let pos = canary::Vec2::new(local.x, local.y); - - let kind = if response.drag_started() { - CursorEventKind::Select - } else if response.drag_released() { - CursorEventKind::Deselect - } else if response.dragged() { - CursorEventKind::Drag - } else { - CursorEventKind::Hover + let sense = egui::Sense { + click: true, + drag: true, + focusable: true, }; - self.panel.on_cursor_event(kind, pos); - } + let desired_size = ui.available_size(); + let response = ui.allocate_response(desired_size, sense); + let rect = response.rect; - let texture = egui::TextureId::Managed(0); - let uv = egui::pos2(0.0, 0.0); - let mut mesh = egui::Mesh::with_texture(texture); + if rect.size() != self.current_size { + let size = rect.size(); + self.current_size = size; - let commands = self.panel.draw(); - for command in commands.into_iter() { - let voff = mesh.vertices.len() as u32; - - match command { - canary::DrawCommand::Mesh { vertices, indices } => { - for v in vertices.iter() { - use egui::epaint::Vertex; - let pos = v.position / PX_PER_MM; - let pos = egui::pos2(pos.x, pos.y); - let pos = pos + rect.left_top().to_vec2(); - let (r, g, b, a) = v.color.to_rgba_unmultiplied(); - let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a); - let v = Vertex { pos, uv, color }; - mesh.vertices.push(v); - } - - for i in indices.iter() { - mesh.indices.push(i + voff); - } - } - _ => unimplemented!(), + let size = canary::Vec2::new(size.x, size.y); + self.panel.on_resize(size * PX_PER_MM); } - } - let painter = ui.painter_at(rect); - let shape = egui::Shape::mesh(mesh); - painter.add(shape); + if let Some(hover_pos) = response.hover_pos() { + let local = (hover_pos - rect.left_top()) * PX_PER_MM; + let pos = canary::Vec2::new(local.x, local.y); - response - }); + let kind = if response.drag_started() { + CursorEventKind::Select + } else if response.drag_released() { + CursorEventKind::Deselect + } else if response.dragged() { + CursorEventKind::Drag + } else { + CursorEventKind::Hover + }; + + self.panel.on_cursor_event(kind, pos); + } + + let texture = egui::TextureId::Managed(0); + let uv = egui::pos2(0.0, 0.0); + let mut mesh = egui::Mesh::with_texture(texture); + + let commands = self.panel.draw(); + for command in commands.into_iter() { + let voff = mesh.vertices.len() as u32; + + match command { + canary::DrawCommand::Mesh { vertices, indices } => { + for v in vertices.iter() { + use egui::epaint::Vertex; + let pos = v.position / PX_PER_MM; + let pos = egui::pos2(pos.x, pos.y); + let pos = pos + rect.left_top().to_vec2(); + let (r, g, b, a) = v.color.to_rgba_unmultiplied(); + let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a); + let v = Vertex { pos, uv, color }; + mesh.vertices.push(v); + } + + for i in indices.iter() { + mesh.indices.push(i + voff); + } + } + _ => unimplemented!(), + } + } + + let painter = ui.painter_at(rect); + let shape = egui::Shape::mesh(mesh); + painter.add(shape); + + response + }); let msg_edit_id = egui::Id::new(format!("msg_edit_{}", self.index)); egui::Window::new("Message Editor") diff --git a/check_licenses.sh b/check_licenses.sh index aad8cd3..19da131 100755 --- a/check_licenses.sh +++ b/check_licenses.sh @@ -12,16 +12,16 @@ dir="$(pwd | sed 's/\//\n/g' | tail -n 1)" for toml in $(find "$PWD" -name "Cargo.toml"); do printf "Project: %s\n" "$(tomcat package.name "$toml")" + toml_lic="$(tomcat package.license "$toml")" + if ! test -n "$toml_lic"; then + printf "%s: Missing license information\n" "$(printf "%s\n" "$toml" |\ + sed "s/^.\+$dir\///g")" + continue 2 + fi for file in $(find "$(printf "%s\n" "$toml" |\ sed 's/Cargo\.toml/src/g')" -name "*.rs") do info="$(head -n 2 "$file")" - toml_lic="$(tomcat package.license "$toml")" - if ! test -n "$toml_lic"; then - printf "%s: Missing license information\n" "$(printf "%s\n" "$toml" |\ - sed "s/^.\+$dir\///g")" - continue 2 - fi if ! [ "$toml_lic" = "$(printf "%s\n" "$info" | tail -n 1 |\ sed -n 's/\/\/ SPDX-License-Identifier: //p')" ] then diff --git a/crates/script/src/api/mod.rs b/crates/script/src/api/mod.rs index 56484e1..0df721e 100644 --- a/crates/script/src/api/mod.rs +++ b/crates/script/src/api/mod.rs @@ -94,6 +94,10 @@ impl Panel { self.draw_indexed(&vertices, &indices); } + + pub fn send_message(&self, message: &[u8]) { + unsafe { panel_send_message(self.0, message.as_ptr() as u32, message.len() as u32) } + } } #[repr(transparent)] @@ -443,4 +447,6 @@ extern "C" { fn message_get_len(id: u32) -> u32; fn message_get_data(id: u32, ptr: u32); + + fn panel_send_message(id: u32, message_ptr: u32, message_len: u32); } diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index c2ada0e..58c0266 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -173,13 +173,13 @@ impl Color { ) } - pub fn alpha_multiply(&self, mul: u8) -> Self { + pub const fn alpha_multiply(&self, mul: u8) -> Self { let a = self.0 as u8 as u16; let multiplied = ((a * (mul as u16)) >> 8) as u8; self.with_alpha(multiplied) } - pub fn with_alpha(&self, alpha: u8) -> Self { + pub const fn with_alpha(&self, alpha: u8) -> Self { Self(self.0 & 0xffffff00 | alpha as u32) } diff --git a/scripts/sao-ui/src/lib.rs b/scripts/sao-ui/src/lib.rs index b558d3f..948c2e9 100644 --- a/scripts/sao-ui/src/lib.rs +++ b/scripts/sao-ui/src/lib.rs @@ -7,6 +7,7 @@ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; pub mod anim; pub mod main_menu; pub mod music_player; +pub mod style; pub mod widgets; use api::*; @@ -23,6 +24,7 @@ fn bind_panel_impl(panel: Panel, protocol: Message, msg: Message) -> Box MusicPlayerPanel::bind(panel, msg), + "wip-dialog" => ConfirmationDialogPanel::bind(panel, msg), _ => MainMenuPanel::bind(panel, msg), } } @@ -50,15 +52,23 @@ impl PanelImpl for ConfirmationDialogPanel { self.dialog.on_cursor_event(kind, at.into()); } - fn on_resize(&mut self, _size: Vec2) {} + fn on_resize(&mut self, size: Vec2) { + self.dialog.resize(size); + } fn on_message(&mut self, _msg: Message) {} } impl ConfirmationDialogPanel { pub fn bind(panel: Panel, msg: Message) -> Box { - let msg = msg.to_vec(); - let info: DialogInfo = serde_json::from_slice(&msg).unwrap(); + // let msg = msg.to_vec(); + // let info: DialogInfo = serde_json::from_slice(&msg).unwrap(); + + let info = DialogInfo { + title: "Hello world!".to_string(), + content: "Testing, testing...".to_string(), + responses: vec![DialogResponse::Yes, DialogResponse::No], + }; use widgets::dialog::*; let style = DialogStyle::default(); diff --git a/scripts/sao-ui/src/main_menu.rs b/scripts/sao-ui/src/main_menu.rs index 87bbbb0..b921a46 100644 --- a/scripts/sao-ui/src/main_menu.rs +++ b/scripts/sao-ui/src/main_menu.rs @@ -7,6 +7,7 @@ use crate::{DrawContext, Rect}; use button::{RectButton, RoundButton, RoundButtonStyle}; use dialog::{Dialog, DialogInfo, DialogResponse, DialogStyle}; use menu::{SlotMenu, SlotMenuEvent, TabMenu}; +use palette::Palette; use shell::{Offset, OffsetAlignment, Popup, Reveal}; use text::LabelText; @@ -47,6 +48,7 @@ pub struct MainMenu { pub menu: Offset>, pub player_info: Reveal>, pub inventory: Reveal>, + pub palette: Reveal>, pub settings: Reveal>, } @@ -64,9 +66,9 @@ impl Default for MainMenu { radius: 7.5, spacing: 1.5, thickness: 0.4, - body_color: Color::WHITE, - ring_color: Color::WHITE, - icon_color: Color::BLACK, + body_color: THEME.palette.surface, + ring_color: THEME.palette.surface, + icon_color: THEME.palette.text, }; let mut buttons = Vec::new(); @@ -102,6 +104,15 @@ impl Default for MainMenu { let inventory = Offset::new(inventory, submenu_spacing_right); let inventory = Reveal::new(inventory, reveal_slide, reveal_duration); + let palette = Palette::new(Default::default()); + let palette = Offset::new_aligned( + palette, + submenu_spacing_left, + OffsetAlignment::End, + OffsetAlignment::Center, + ); + let palette = Reveal::new(palette, -reveal_slide, reveal_duration); + let settings = SettingsMenu::new(); let settings = Offset::new(settings, submenu_spacing_right); let settings = Reveal::new(settings, reveal_slide, reveal_duration); @@ -110,6 +121,7 @@ impl Default for MainMenu { menu, player_info, inventory, + palette, settings, } } @@ -120,6 +132,7 @@ impl Container for MainMenu { f(&mut self.menu); f(&mut self.player_info); f(&mut self.inventory); + f(&mut self.palette); f(&mut self.settings); } @@ -139,8 +152,14 @@ impl Container for MainMenu { self.player_info.hide(); self.inventory.hide(); } - SlotMenuEvent::SubmenuOpen(4) => self.settings.show(), - SlotMenuEvent::SubmenuClose(4) => self.settings.hide(), + SlotMenuEvent::SubmenuOpen(4) => { + self.palette.show(); + self.settings.show(); + } + SlotMenuEvent::SubmenuClose(4) => { + self.palette.hide(); + self.settings.hide(); + } _ => {} }; } @@ -150,6 +169,7 @@ pub struct PlayerInfo { width: f32, height: f32, rounding: f32, + color: Color, } impl PlayerInfo { @@ -158,6 +178,7 @@ impl PlayerInfo { width: 70.0, height: 120.0, rounding: 5.0, + color: THEME.palette.surface, } } } @@ -170,7 +191,7 @@ impl RectBounds for PlayerInfo { impl Widget for PlayerInfo { fn draw(&mut self, ctx: &DrawContext) { - ctx.draw_rounded_rect(self.get_bounds(), self.rounding, Color::WHITE); + ctx.draw_rounded_rect(self.get_bounds(), self.rounding, self.color); } } diff --git a/scripts/sao-ui/src/music_player.rs b/scripts/sao-ui/src/music_player.rs index 9eab50e..9be32f0 100644 --- a/scripts/sao-ui/src/music_player.rs +++ b/scripts/sao-ui/src/music_player.rs @@ -1,12 +1,16 @@ +// Copyright (c) 2022 Marceline Crmaer +// SPDX-License-Identifier: AGPL-3.0-or-later + use api::*; use canary_script::*; -use canary_music_player::{AlbumInfo, PlaybackStatus, ProgressChanged, TrackInfo}; +use canary_music_player::{AlbumInfo, OutMsg, PlaybackStatus, ProgressChanged, TrackInfo}; use crate::widgets::prelude::*; use button::{RoundButton, RoundButtonStyle}; use dialog::{DialogBodyStyle, DialogFooterStyle}; use shell::Offset; +use slider::Slider; use text::{HorizontalAlignment, Label, LabelText}; pub struct MusicPlayerPanel { @@ -57,7 +61,7 @@ impl PanelImpl for MusicPlayerPanel { use InMsg::*; match (self.widget.as_mut(), msg) { (Some(_), Disconnected) => self.widget = None, - (None, Connected) => self.widget = Some(MusicPlayerWidget::new()), + (None, Connected) => self.widget = Some(MusicPlayerWidget::new(self.panel)), (Some(widget), AlbumChanged(info)) => widget.update_album(info), (Some(widget), TrackChanged(info)) => widget.update_track(info), (Some(widget), PlaybackStatusChanged(status)) => widget.update_playback_status(status), @@ -91,6 +95,7 @@ pub struct MusicPlayerStyle { pub rounding: f32, pub art_margin: f32, pub button_spacing: f32, + pub slider_height: f32, } impl Default for MusicPlayerStyle { @@ -101,11 +106,13 @@ impl Default for MusicPlayerStyle { rounding: 5.0, art_margin: 5.0, button_spacing: 15.0, + slider_height: 7.5, } } } pub struct MusicPlayerWidget { + panel: Panel, artist: Offset