diff --git a/apps/magpie/Cargo.toml b/apps/magpie/Cargo.toml index 6d09612..1dcb110 100644 --- a/apps/magpie/Cargo.toml +++ b/apps/magpie/Cargo.toml @@ -9,8 +9,10 @@ path = "src/main.rs" required-features = ["service"] [dependencies] +anyhow = { version = "1", optional = true } byteorder = "1.4" canary = { path = "../..", optional = true } +futures-util = { version = "0.3", optional = true, features = ["io"] } glium = { version = "0.32", optional = true} mio = { version = "0.8", features = ["net", "os-poll"], optional = true } mio-signals = { version = "0.2", optional = true } @@ -20,4 +22,5 @@ serde_json = "1" slab = { version = "0.4", optional = true} [features] -service = ["dep:canary", "dep:glium", "dep:mio", "dep:mio-signals", "dep:parking_lot", "dep:slab"] +async = ["dep:futures-util"] +service = ["dep:anyhow", "dep:canary", "dep:glium", "dep:mio", "dep:mio-signals", "dep:parking_lot", "dep:slab"] diff --git a/apps/magpie/src/client.rs b/apps/magpie/src/client.rs deleted file mode 100644 index 652c7f2..0000000 --- a/apps/magpie/src/client.rs +++ /dev/null @@ -1,41 +0,0 @@ -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 233623e..c17fe70 100644 --- a/apps/magpie/src/lib.rs +++ b/apps/magpie/src/lib.rs @@ -1,4 +1,6 @@ -pub mod client; +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: AGPL-3.0-or-later + pub mod protocol; #[cfg(feature = "service")] diff --git a/apps/magpie/src/main.rs b/apps/magpie/src/main.rs index d53eb7a..372dd9c 100644 --- a/apps/magpie/src/main.rs +++ b/apps/magpie/src/main.rs @@ -1,3 +1,6 @@ +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: AGPL-3.0-or-later + use glium::glutin::event_loop::EventLoopBuilder; use canary_magpie::service::*; diff --git a/apps/magpie/src/protocol.rs b/apps/magpie/src/protocol.rs index a833bc3..eacf451 100644 --- a/apps/magpie/src/protocol.rs +++ b/apps/magpie/src/protocol.rs @@ -1,3 +1,6 @@ +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: AGPL-3.0-or-later + use std::collections::VecDeque; use std::io::{Read, Write}; use std::marker::PhantomData; @@ -21,6 +24,7 @@ pub type PanelId = u32; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct CreatePanel { pub id: PanelId, + pub protocol: String, pub script: PathBuf, } @@ -39,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 { @@ -135,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/gl.rs b/apps/magpie/src/service/gl.rs index 9c0a620..e46e11e 100644 --- a/apps/magpie/src/service/gl.rs +++ b/apps/magpie/src/service/gl.rs @@ -1,5 +1,8 @@ +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: AGPL-3.0-or-later + use canary::{DrawCommand, Vec2, PX_PER_MM}; -use glium::Surface; +use glium::{program::ProgramCreationInput, Surface}; #[derive(Copy, Clone)] pub struct Vertex { @@ -56,9 +59,21 @@ pub struct Graphics { impl Graphics { pub fn new(display: glium::Display) -> Self { - let program = - glium::Program::from_source(&display, VERTEX_SHADER_SRC, FRAGMENT_SHADER_SRC, None) - .unwrap(); + let program = glium::Program::new( + &display, + ProgramCreationInput::SourceCode { + vertex_shader: VERTEX_SHADER_SRC, + tessellation_control_shader: None, + tessellation_evaluation_shader: None, + geometry_shader: None, + fragment_shader: FRAGMENT_SHADER_SRC, + transform_feedback_varyings: None, + outputs_srgb: true, // don't automatically apply gamma correction + uses_point_size: false, + }, + ) + .unwrap(); + Self { display, program } } @@ -102,7 +117,7 @@ impl Graphics { }; let mut target = self.display.draw(); - target.clear_color(0.0, 0.0, 0.0, 1.0); + target.clear_color(0.0, 0.0, 0.0, 0.0); target .draw( &vertex_buffer, diff --git a/apps/magpie/src/service/ipc.rs b/apps/magpie/src/service/ipc.rs index 07d9273..c4271bf 100644 --- a/apps/magpie/src/service/ipc.rs +++ b/apps/magpie/src/service/ipc.rs @@ -1,3 +1,6 @@ +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: AGPL-3.0-or-later + use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; @@ -11,18 +14,29 @@ use mio_signals::{Signal, Signals}; use parking_lot::RwLock; use slab::Slab; +use crate::protocol::*; use crate::service::window::{WindowMessage, WindowMessageSender}; -use crate::protocol::{CreatePanel, MagpieServerMsg, SendMessage, ServerMessenger}; 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, @@ -54,6 +68,7 @@ impl DerefMut for Listener { pub struct IpcData { poll: Poll, + window_to_client_panel: HashMap, next_window_id: usize, } @@ -73,38 +88,36 @@ 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 { id, script }) => { - let window = self.data.write().new_window_id(); + MagpieServerMsg::CreatePanel(CreatePanel { + id, + protocol, + script, + }) => { + 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 }; let _ = self.window_sender.send_event(msg); } - let msg = WindowMessage::OpenWindow { id: window, script }; + let msg = WindowMessage::OpenWindow { + id: window, + protocol, + script, + }; let _ = self.window_sender.send_event(msg); } MagpieServerMsg::SendMessage(SendMessage { id, msg }) => { @@ -118,13 +131,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, @@ -148,7 +174,6 @@ impl Ipc { 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); @@ -162,12 +187,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, }; @@ -175,7 +201,6 @@ impl Ipc { data: Arc::new(RwLock::new(data)), window_sender, message_recv, - events, quit: false, listener, signals, @@ -188,10 +213,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() { @@ -227,13 +271,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); } } @@ -241,9 +289,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/mod.rs b/apps/magpie/src/service/mod.rs index b57a6fe..15d5746 100644 --- a/apps/magpie/src/service/mod.rs +++ b/apps/magpie/src/service/mod.rs @@ -1,3 +1,6 @@ +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: AGPL-3.0-or-later + pub mod gl; pub mod ipc; pub mod window; diff --git a/apps/magpie/src/service/window.rs b/apps/magpie/src/service/window.rs index ee19c99..fd41d39 100644 --- a/apps/magpie/src/service/window.rs +++ b/apps/magpie/src/service/window.rs @@ -1,46 +1,73 @@ +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: AGPL-3.0-or-later + use std::collections::HashMap; use std::path::PathBuf; use std::time::Instant; -use canary::{Panel, Runtime}; +use canary::{CursorEventKind, Panel, Runtime, Vec2, PX_PER_MM}; use glium::backend::glutin::DisplayCreationError; use glium::{glutin, Surface}; -use glutin::event::{Event, WindowEvent}; +use glutin::event::{ElementState, Event, MouseButton, WindowEvent}; use glutin::event_loop::{ControlFlow, EventLoop, EventLoopProxy, EventLoopWindowTarget}; use glutin::window::WindowId; use crate::service::gl::Graphics; use crate::service::ipc::{IpcMessage, IpcMessageSender}; +#[derive(Clone, Debug)] pub enum WindowMessage { - OpenWindow { id: usize, script: PathBuf }, - CloseWindow { id: usize }, + OpenWindow { + id: usize, + protocol: String, + script: PathBuf, + }, + CloseWindow { + id: usize, + }, Quit, - SendMessage { id: usize, msg: Vec }, + SendMessage { + id: usize, + msg: Vec, + }, } pub type WindowMessageSender = EventLoopProxy; pub struct Window { + pub ipc_sender: IpcMessageSender, + pub ipc_id: usize, pub graphics: Graphics, pub panel: Panel, pub last_update: Instant, + pub cursor_pos: Vec2, + pub cursor_down: bool, } impl Window { pub fn new( + ipc_sender: IpcMessageSender, + ipc_id: usize, panel: Panel, event_loop: &EventLoopWindowTarget, ) -> Result { - let wb = glutin::window::WindowBuilder::new(); - let cb = glutin::ContextBuilder::new(); + let wb = glutin::window::WindowBuilder::new() + .with_transparent(true) + .with_decorations(false); + let cb = glutin::ContextBuilder::new() + .with_vsync(true) + .with_multisampling(4); let display = glium::Display::new(wb, cb, &event_loop)?; let graphics = Graphics::new(display); let last_update = Instant::now(); Ok(Self { + ipc_sender, + ipc_id, graphics, panel, last_update, + cursor_pos: Vec2::ZERO, + cursor_down: false, }) } @@ -52,19 +79,81 @@ impl Window { self.graphics.display.gl_window().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) { let commands = self.panel.draw(); self.graphics.draw(&commands); + 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: Vec2) { + self.panel.on_resize(new_size); + self.recv_messages(); + } + + pub fn on_event(&mut self, event: WindowEvent) { + match event { + WindowEvent::Resized(size) => { + self.resize(Vec2::new(size.width as f32, size.height as f32) * PX_PER_MM); + self.request_redraw() + } + WindowEvent::CursorMoved { position, .. } => { + let x = position.x as f32 * PX_PER_MM; + let y = position.y as f32 * PX_PER_MM; + self.cursor_pos = Vec2::new(x, y); + + let event = if self.cursor_down { + CursorEventKind::Drag + } else { + CursorEventKind::Hover + }; + + self.panel.on_cursor_event(event, self.cursor_pos); + self.recv_messages(); + } + WindowEvent::MouseInput { + state, + button: MouseButton::Left, + .. + } => { + let event = match state { + ElementState::Pressed => { + self.cursor_down = true; + CursorEventKind::Select + } + ElementState::Released => { + self.cursor_down = false; + CursorEventKind::Deselect + } + }; + + self.panel.on_cursor_event(event, self.cursor_pos); + self.recv_messages(); + } + _ => {} + } } } @@ -95,14 +184,47 @@ impl WindowStore { .flatten() } + pub fn on_message( + &mut self, + event_loop: &EventLoopWindowTarget, + message: WindowMessage, + ) -> anyhow::Result { + match message { + 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, &event_loop)?; + let window_id = window.get_id(); + self.windows.insert(window_id, window); + self.ipc_to_window.insert(id, window_id); + } + WindowMessage::CloseWindow { id } => { + if let Some(window_id) = self.ipc_to_window.remove(&id) { + self.windows.remove(&window_id); + } + } + WindowMessage::Quit => return Ok(true), + WindowMessage::SendMessage { id, msg } => { + if let Some(window) = self.get_ipc_window(id) { + window.send_message(msg); + } + } + }; + + Ok(false) + } + pub fn run(mut self, event_loop: EventLoop) -> ! { event_loop.run(move |event, event_loop, control_flow| match event { Event::WindowEvent { window_id, event } => { if let Some(window) = self.windows.get_mut(&window_id) { - match event { - WindowEvent::Resized(_) => window.request_redraw(), - _ => {} - } + window.on_event(event); } } Event::RedrawRequested(id) => { @@ -116,27 +238,11 @@ impl WindowStore { window.request_redraw(); } } - Event::UserEvent(event) => match event { - WindowMessage::OpenWindow { id, script } => { - println!("Opening window {} with script {:?}", id, script); - let module = std::fs::read(script).unwrap(); - let mut script = self.runtime.load_module(&module).unwrap(); - let panel = script.create_panel(vec![]).unwrap(); - let window = Window::new(panel, &event_loop).unwrap(); - let window_id = window.get_id(); - self.windows.insert(window_id, window); - self.ipc_to_window.insert(id, window_id); - } - WindowMessage::CloseWindow { id } => { - if let Some(window_id) = self.ipc_to_window.remove(&id) { - self.windows.remove(&window_id); - } - } - WindowMessage::Quit => *control_flow = ControlFlow::Exit, - WindowMessage::SendMessage { id, msg } => { - if let Some(window) = self.get_ipc_window(id) { - window.send_message(msg); - } + Event::UserEvent(event) => match self.on_message(event_loop, event.clone()) { + Ok(false) => {} + Ok(true) => *control_flow = ControlFlow::Exit, + Err(err) => { + eprintln!("Error while handling message {:?}:\n{}", event, err); } }, _ => {} diff --git a/apps/music-player/Cargo.toml b/apps/music-player/Cargo.toml index bc71b4a..09ab086 100644 --- a/apps/music-player/Cargo.toml +++ b/apps/music-player/Cargo.toml @@ -2,6 +2,7 @@ name = "canary-music-player" version = "0.1.0" edition = "2021" +license = "AGPL-3.0-or-later" [[bin]] name = "canary-music-player" @@ -9,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 2c04850..fa0e96d 100644 --- a/apps/music-player/src/lib.rs +++ b/apps/music-player/src/lib.rs @@ -1,9 +1,12 @@ +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: AGPL-3.0-or-later + use serde::{Deserialize, Serialize}; pub use serde; pub use serde_json; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub enum PlaybackStatus { /// A track is currently playing. Playing, @@ -15,7 +18,7 @@ pub enum PlaybackStatus { Stopped, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub enum LoopStatus { /// The playback will stop when there are no more tracks to play. None, @@ -31,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)] @@ -45,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, @@ -55,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)] @@ -101,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 d142d49..fb647ee 100644 --- a/apps/music-player/src/main.rs +++ b/apps/music-player/src/main.rs @@ -1,63 +1,193 @@ -use canary_music_player::*; -use canary_magpie::client::MagpieClient; -use canary_magpie::protocol::{CreatePanel, MagpieServerMsg}; -use mpris::PlayerFinder; +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: AGPL-3.0-or-later -pub struct MetadataTracker { +use std::path::Path; + +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 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())); + 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())); + 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 @@ -65,104 +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 script = std::path::PathBuf::from(&module_path); - let msg = CreatePanel { id: 0, 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(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..4cf2b4b --- /dev/null +++ b/apps/music-player/src/mpris.rs @@ -0,0 +1,48 @@ +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 a6381eb..5b54f7b 100644 --- a/apps/sandbox/src/main.rs +++ b/apps/sandbox/src/main.rs @@ -1,7 +1,7 @@ // Copyright (c) 2022 Marceline Cramer // SPDX-License-Identifier: AGPL-3.0-or-later -use canary::{CursorEventKind, Panel, Runtime, Script}; +use canary::{CursorEventKind, Panel, Runtime, Script, PX_PER_MM}; use eframe::egui; use std::time::Instant; @@ -31,7 +31,9 @@ struct App { panels: Vec, next_idx: usize, last_update: Instant, + protocol_buf: String, bind_message_buf: String, + panel_bg: egui::Color32, } impl App { @@ -46,7 +48,9 @@ impl App { panels: vec![], next_idx: 0, last_update: Instant::now(), + protocol_buf: String::new(), bind_message_buf: String::new(), + panel_bg: egui::Color32::TRANSPARENT, } } } @@ -56,12 +60,18 @@ 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); + + ui.label("Bind message:"); let text_edit = egui::TextEdit::multiline(&mut self.bind_message_buf).code_editor(); ui.add(text_edit); if ui.button("Bind Panel").clicked() { let msg = self.bind_message_buf.as_bytes().to_vec(); - let panel = self.script.create_panel(msg).unwrap(); + let panel = self.script.create_panel(&self.protocol_buf, msg).unwrap(); let index = self.next_idx; self.next_idx += 1; @@ -70,10 +80,19 @@ impl eframe::App for App { index, msg_buf: String::new(), show_msg: false, + current_size: Default::default(), }; 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(); @@ -81,7 +100,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); } } } @@ -91,80 +110,91 @@ pub struct PanelWindow { pub index: usize, pub msg_buf: String, pub show_msg: bool, + pub current_size: egui::Vec2, } 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 size = egui::vec2(800.0, 800.0); - let sense = egui::Sense { - click: true, - drag: true, - focusable: true, - }; - - let (rect, response) = ui.allocate_at_least(size, sense); - - if let Some(hover_pos) = response.hover_pos() { - let local = (hover_pos - rect.left_top()) / rect.size(); - let norm = local * 2.0 - egui::vec2(1.0, 1.0); - let x = norm.x; - let y = -norm.y; - let pos = canary::Vec2 { x, 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 = egui::pos2(v.position.x, -v.position.y); - let pos = pos.to_vec2() / 2.0 + egui::vec2(0.5, 0.5); - let pos = rect.left_top() + pos * rect.size(); - 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 new file mode 100755 index 0000000..aad8cd3 --- /dev/null +++ b/check_licenses.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +set -e + +# check if we have tomcat(1) +if ! command -v tomcat >/dev/null 2>&1; then + printf "%s: Missing dependency: tomcat(1)\n" + exit 69 # sysexits(3) EX_UNAVAILABLE +fi + +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")" + 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 + printf "%s: Missing or malformed license information\n" \ + "$(printf "%s\n" "$file" | sed "s/^.\+$dir\///g")" + fi + if ! test -n "$(printf "%s\n" "$info" | head -n 1 |\ + sed -n '/\/\/ Copyright (c) .\+/p')" + then + printf "%s: Missing or malformed copyright holder information\n" \ + "$(printf "%s\n" "$file" | sed "s/^.\+$dir\///g")" + fi + done +done diff --git a/crates/script/src/api/abi.rs b/crates/script/src/api/abi.rs index 737e6da..4ebc255 100644 --- a/crates/script/src/api/abi.rs +++ b/crates/script/src/api/abi.rs @@ -7,11 +7,17 @@ use super::*; static mut PANEL_IMPLS: Vec> = Vec::new(); -pub fn bind_panel(panel: u32, msg: u32) -> u32 { +pub fn bind_panel( + cb: impl Fn(Panel, Message, Message) -> Box, + panel: u32, + protocol: u32, + msg: u32, +) -> u32 { unsafe { let panel = Panel(panel); + let protocol = Message(protocol); let msg = Message(msg); - let panel_impl = T::bind(panel, msg); + let panel_impl = cb(panel, protocol, msg); let id = PANEL_IMPLS.len() as u32; PANEL_IMPLS.push(panel_impl); id @@ -28,6 +34,12 @@ pub fn draw(panel_data: u32) { panel.draw(); } +pub fn on_resize(panel_data: u32, width: f32, height: f32) { + let panel = unsafe { &mut PANEL_IMPLS[panel_data as usize] }; + let new_size = Vec2::new(width, height); + panel.on_resize(new_size); +} + pub fn on_cursor_event(panel_data: u32, kind: u32, x: f32, y: f32) { let panel = unsafe { &mut PANEL_IMPLS[panel_data as usize] }; let at = Vec2::new(x, y); diff --git a/crates/script/src/api/mod.rs b/crates/script/src/api/mod.rs index dfa0aef..0df721e 100644 --- a/crates/script/src/api/mod.rs +++ b/crates/script/src/api/mod.rs @@ -9,10 +9,10 @@ pub mod abi; #[macro_export] macro_rules! export_abi { - ($panel_impl: ident) => { + ($bind_panel: ident) => { #[no_mangle] - pub extern "C" fn bind_panel(panel: u32, msg: u32) -> u32 { - ::canary_script::api::abi::bind_panel::<$panel_impl>(panel, msg) + pub extern "C" fn bind_panel(panel: u32, protocol: u32, msg: u32) -> u32 { + ::canary_script::api::abi::bind_panel($bind_panel, panel, protocol, msg) } #[no_mangle] @@ -25,6 +25,11 @@ macro_rules! export_abi { ::canary_script::api::abi::draw(panel_data) } + #[no_mangle] + pub extern "C" fn on_resize(panel_data: u32, width: f32, height: f32) { + ::canary_script::api::abi::on_resize(panel_data, width, height) + } + #[no_mangle] pub extern "C" fn on_cursor_event(panel_data: u32, kind: u32, x: f32, y: f32) { ::canary_script::api::abi::on_cursor_event(panel_data, kind, x, y) @@ -37,13 +42,10 @@ macro_rules! export_abi { }; } -pub trait BindPanel { - fn bind(panel: Panel, msg: Message) -> Box; -} - pub trait PanelImpl { fn update(&mut self, dt: f32); fn draw(&mut self); + fn on_resize(&mut self, new_size: Vec2); fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2); fn on_message(&mut self, msg: Message); } @@ -92,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)] @@ -180,6 +186,24 @@ impl DrawContext { } } + pub fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]) { + let mut vertices = vertices.to_vec(); + + if let Some(offset) = self.offset { + for v in vertices.iter_mut() { + v.position += offset; + } + } + + if let Some(opacity) = self.opacity { + for v in vertices.iter_mut() { + v.color = v.color.alpha_multiply(opacity); + } + } + + self.panel.draw_indexed(&vertices, &indices); + } + pub fn draw_triangle(&self, v1: Vec2, v2: Vec2, v3: Vec2, color: Color) { if let Some(clip_rect) = self.clip_rect.as_ref() { let tri = ColoredTriangle { v1, v2, v3, color }; @@ -231,10 +255,10 @@ impl DrawContext { let delta = PI / 4.0 / spoke_num; let (mut theta, limit) = match corner { - Corner::TopRight => (0.0, FRAC_PI_2), - Corner::BottomRight => (FRAC_PI_2 * 3.0, PI * 2.0), - Corner::BottomLeft => (PI, FRAC_PI_2 * 3.0), - Corner::TopLeft => (FRAC_PI_2, PI), + Corner::TopRight => (FRAC_PI_2 * 3.0, PI * 2.0), + Corner::BottomRight => (0.0, FRAC_PI_2), + Corner::BottomLeft => (FRAC_PI_2, PI), + Corner::TopLeft => (PI, FRAC_PI_2 * 3.0), }; let mut last_spoke = Vec2::from_angle(theta) * radius + center; @@ -281,10 +305,10 @@ impl DrawContext { rect }; - let v1 = rect.bl; - let v2 = Vec2::new(rect.bl.x, rect.tr.y); - let v3 = Vec2::new(rect.tr.x, rect.bl.y); - let v4 = rect.tr; + let v1 = rect.tl; + let v2 = Vec2::new(rect.tl.x, rect.br.y); + let v3 = Vec2::new(rect.br.x, rect.tl.y); + let v4 = rect.br; self.draw_triangle_noclip(v1, v2, v3, color); self.draw_triangle_noclip(v2, v3, v4, color); @@ -309,48 +333,48 @@ impl DrawContext { let mut inner_rect = rect; let inset = rect.inset(radius); - if corners.intersects(CornerFlags::BOTTOM) { - inner_rect.bl.y += radius; - - let mut bottom_edge = Rect { - bl: rect.bl, - tr: Vec2::new(rect.tr.x, rect.bl.y + radius), - }; - - if corners.contains(CornerFlags::BOTTOM_LEFT) { - bottom_edge.bl.x += radius; - self.draw_quarter_circle(Corner::BottomLeft, inset.bl, radius, color); - } - - if corners.contains(CornerFlags::BOTTOM_RIGHT) { - bottom_edge.tr.x -= radius; - self.draw_quarter_circle(Corner::BottomRight, inset.br(), radius, color); - } - - self.draw_rect(bottom_edge, color); - } - if corners.intersects(CornerFlags::TOP) { - inner_rect.tr.y -= radius; + inner_rect.tl.y += radius; let mut top_edge = Rect { - bl: Vec2::new(rect.bl.x, rect.tr.y - radius), - tr: rect.tr, + tl: rect.tl, + br: Vec2::new(rect.br.x, rect.tl.y + radius), }; if corners.contains(CornerFlags::TOP_LEFT) { - top_edge.bl.x += radius; - self.draw_quarter_circle(Corner::TopLeft, inset.tl(), radius, color); + top_edge.tl.x += radius; + self.draw_quarter_circle(Corner::TopLeft, inset.tl, radius, color); } if corners.contains(CornerFlags::TOP_RIGHT) { - top_edge.tr.x -= radius; - self.draw_quarter_circle(Corner::TopRight, inset.tr, radius, color); + top_edge.br.x -= radius; + self.draw_quarter_circle(Corner::TopRight, inset.tr(), radius, color); } self.draw_rect(top_edge, color); } + if corners.intersects(CornerFlags::BOTTOM) { + inner_rect.br.y -= radius; + + let mut bottom_edge = Rect { + tl: Vec2::new(rect.tl.x, rect.br.y - radius), + br: rect.br, + }; + + if corners.contains(CornerFlags::BOTTOM_LEFT) { + bottom_edge.tl.x += radius; + self.draw_quarter_circle(Corner::BottomLeft, inset.bl(), radius, color); + } + + if corners.contains(CornerFlags::BOTTOM_RIGHT) { + bottom_edge.br.x -= radius; + self.draw_quarter_circle(Corner::BottomRight, inset.br, radius, color); + } + + self.draw_rect(bottom_edge, color); + } + self.draw_rect(inner_rect, color); } @@ -423,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 39028ad..58c0266 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -6,84 +6,83 @@ use bytemuck::{Pod, Zeroable}; pub use glam::Vec2; use num_derive::{FromPrimitive, ToPrimitive}; -#[cfg(target_arch = "wasm32")] pub mod api; #[repr(C)] #[derive(Copy, Clone, Debug, Default, Pod, Zeroable)] pub struct Rect { - pub bl: Vec2, - pub tr: Vec2, + pub tl: Vec2, + pub br: Vec2, } impl Rect { pub const NEG_INFINITY: Self = Self { - bl: Vec2::splat(f32::INFINITY), - tr: Vec2::splat(f32::NEG_INFINITY), + tl: Vec2::splat(f32::INFINITY), + br: Vec2::splat(f32::NEG_INFINITY), }; pub fn from_xy_size(xy: Vec2, size: Vec2) -> Self { Self { - bl: xy, - tr: xy + size, + tl: xy, + br: xy + size, } } pub fn from_circle_bounds(center: Vec2, radius: f32) -> Self { Self { - bl: center - radius, - tr: center + radius, + tl: center - radius, + br: center + radius, } } pub fn from_triangle_bounds(tri: &ColoredTriangle) -> Self { Self { - bl: tri.v1.min(tri.v2).min(tri.v3), - tr: tri.v1.max(tri.v2).max(tri.v3), + tl: tri.v1.min(tri.v2).min(tri.v3), + br: tri.v1.max(tri.v2).max(tri.v3), } } pub fn inset(&self, d: f32) -> Self { Self { - bl: self.bl + d, - tr: self.tr - d, + tl: self.tl + d, + br: self.br - d, } } - - pub fn tl(&self) -> Vec2 { - Vec2::new(self.bl.x, self.tr.y) + + pub fn bl(&self) -> Vec2 { + Vec2::new(self.tl.x, self.br.y) } - pub fn br(&self) -> Vec2 { - Vec2::new(self.tr.x, self.bl.y) + pub fn tr(&self) -> Vec2 { + Vec2::new(self.br.x, self.tl.y) } pub fn offset(&self, offset: Vec2) -> Self { Self { - bl: self.bl + offset, - tr: self.tr + offset, + tl: self.tl + offset, + br: self.br + offset, } } pub fn scale(&self, scale: f32) -> Self { Self { - bl: self.bl * scale, - tr: self.tr * scale, + tl: self.tl * scale, + br: self.br * scale, } } pub fn is_valid(&self) -> bool { - self.bl.cmplt(self.tr).all() + self.tl.cmplt(self.br).all() } pub fn intersects_rect(&self, other: &Self) -> bool { - self.bl.cmple(other.tr).all() && self.tr.cmpge(other.bl).all() + self.tl.cmple(other.br).all() && self.br.cmpge(other.tl).all() } pub fn intersection(&self, other: &Self) -> Option { let clipped = Self { - bl: self.bl.max(other.bl), - tr: self.tr.min(other.tr), + tl: self.tl.max(other.tl), + br: self.br.min(other.br), }; if clipped.is_valid() { @@ -93,27 +92,41 @@ impl Rect { } } + pub fn union(&self, other: &Self) -> Self { + Self { + tl: self.tl.min(other.tl), + br: self.br.max(other.br), + } + } + + pub fn union_point(&self, point: Vec2) -> Self { + Self { + tl: self.tl.min(point), + br: self.br.max(point), + } + } + pub fn contains_rect(&self, other: &Self) -> bool { - self.bl.x < other.bl.x - && self.bl.y < other.bl.y - && self.tr.x > other.tr.x - && self.tr.y > other.tr.y + self.tl.x < other.tl.x + && self.tl.y < other.tl.y + && self.br.x > other.br.x + && self.br.y > other.br.y } pub fn contains_point(&self, xy: Vec2) -> bool { - self.bl.x < xy.x && self.bl.y < xy.y && self.tr.x > xy.x && self.tr.y > xy.y + self.tl.x < xy.x && self.tl.y < xy.y && self.br.x > xy.x && self.br.y > xy.y } pub fn size(&self) -> Vec2 { - self.tr - self.bl + self.br - self.tl } pub fn width(&self) -> f32 { - self.tr.x - self.bl.x + self.br.x - self.tl.x } pub fn height(&self) -> f32 { - self.tr.y - self.bl.y + self.br.y - self.tl.y } } @@ -160,15 +173,21 @@ 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) } + + pub fn lerp(self, target: Self, blend: f32) -> Self { + let s: glam::Vec4 = self.into(); + let o: glam::Vec4 = target.into(); + (o * blend + s * (1.0 - blend)).into() + } } #[repr(C)] diff --git a/scripts/music-player/Cargo.toml b/scripts/music-player/Cargo.toml index 8a0e3b8..b7191ae 100644 --- a/scripts/music-player/Cargo.toml +++ b/scripts/music-player/Cargo.toml @@ -2,6 +2,7 @@ name = "canary-music-player-script" version = "0.1.0" edition = "2021" +license = "AGPL-3.0-or-later" [lib] crate-type = ["cdylib"] diff --git a/scripts/music-player/src/lib.rs b/scripts/music-player/src/lib.rs index 998aaf1..956df2b 100644 --- a/scripts/music-player/src/lib.rs +++ b/scripts/music-player/src/lib.rs @@ -1,10 +1,17 @@ +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: AGPL-3.0-or-later + #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; -use canary_script::*; use api::*; +use canary_script::*; -canary_script::export_abi!(MusicPlayerPanel); +canary_script::export_abi!(bind_panel_impl); + +pub fn bind_panel_impl(panel: Panel, _protocol: Message, message: Message) -> Box { + MusicPlayerPanel::bind(panel, message) +} const DISPLAY_FONT: &str = "Liberation Sans"; @@ -14,19 +21,6 @@ pub struct MusicPlayerPanel { label: Label, } -impl BindPanel for MusicPlayerPanel { - fn bind(panel: Panel, message: Message) -> Box { - let display_font = Font::new(DISPLAY_FONT); - let label = Label::new(display_font, "Hello, world!".into(), 1.2); - let panel = Self { - panel, - display_font, - label, - }; - Box::new(panel) - } -} - impl PanelImpl for MusicPlayerPanel { fn update(&mut self, dt: f32) {} @@ -39,16 +33,31 @@ impl PanelImpl for MusicPlayerPanel { self.label.draw(&ctx, offset, size, color); } + fn on_resize(&mut self, new_size: Vec2) {} + fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {} fn on_message(&mut self, msg: Message) { - use canary_music_player::{InMsg, serde_json}; + use canary_music_player::{serde_json, InMsg}; let msg = msg.to_vec(); let msg = serde_json::from_slice::(&msg); self.label.set_text(format!("{:#?}", msg)); } } +impl MusicPlayerPanel { + pub fn bind(panel: Panel, _message: Message) -> Box { + let display_font = Font::new(DISPLAY_FONT); + let label = Label::new(display_font, "Hello, world!".into(), 1.2); + let panel = Self { + panel, + display_font, + label, + }; + Box::new(panel) + } +} + pub struct Label { font: Font, text: String, diff --git a/scripts/sao-ui/Cargo.toml b/scripts/sao-ui/Cargo.toml index 6c6862e..2a09cd0 100644 --- a/scripts/sao-ui/Cargo.toml +++ b/scripts/sao-ui/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["cdylib"] [dependencies] glam = "^0.21" keyframe = "1" +canary-music-player = { path = "../../apps/music-player" } canary-script = { path = "../../crates/script" } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/scripts/sao-ui/src/anim.rs b/scripts/sao-ui/src/anim.rs index bc07f1c..10aae47 100644 --- a/scripts/sao-ui/src/anim.rs +++ b/scripts/sao-ui/src/anim.rs @@ -1,3 +1,6 @@ +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: AGPL-3.0-or-later + use crate::Color; use keyframe::EasingFunction; diff --git a/scripts/sao-ui/src/draw.rs b/scripts/sao-ui/src/draw.rs deleted file mode 100644 index 8b13789..0000000 --- a/scripts/sao-ui/src/draw.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/scripts/sao-ui/src/lib.rs b/scripts/sao-ui/src/lib.rs index 15daa23..948c2e9 100644 --- a/scripts/sao-ui/src/lib.rs +++ b/scripts/sao-ui/src/lib.rs @@ -1,18 +1,33 @@ +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: AGPL-3.0-or-later + #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; pub mod anim; -pub mod draw; pub mod main_menu; -pub mod panel; +pub mod music_player; +pub mod style; pub mod widgets; -use canary_script::*; use api::*; -use widgets::Widget; +use canary_script::*; use main_menu::MainMenuPanel; +use music_player::MusicPlayerPanel; +use widgets::Widget; -export_abi!(MainMenuPanel); +export_abi!(bind_panel_impl); + +fn bind_panel_impl(panel: Panel, protocol: Message, msg: Message) -> Box { + let protocol = protocol.to_vec(); + let protocol = String::from_utf8(protocol).unwrap(); + + match protocol.as_str() { + "tebibyte-media.desktop.music-player-controller" => MusicPlayerPanel::bind(panel, msg), + "wip-dialog" => ConfirmationDialogPanel::bind(panel, msg), + _ => MainMenuPanel::bind(panel, msg), + } +} pub const ICON_FONT: &str = "Iosevka Nerd Font"; pub const DISPLAY_FONT: &str = "Homenaje"; @@ -23,18 +38,6 @@ pub struct ConfirmationDialogPanel { dialog: widgets::dialog::Dialog, } -impl BindPanel for ConfirmationDialogPanel { - fn bind(panel: Panel, msg: Message) -> Box { - let msg = msg.to_vec(); - let info: DialogInfo = serde_json::from_slice(&msg).unwrap(); - - use widgets::dialog::*; - let style = DialogStyle::default(); - let dialog = Dialog::new(style, &info); - Box::new(Self { panel, dialog }) - } -} - impl PanelImpl for ConfirmationDialogPanel { fn update(&mut self, dt: f32) { self.dialog.update(dt); @@ -45,9 +48,31 @@ impl PanelImpl for ConfirmationDialogPanel { self.dialog.draw(&ctx); } - fn on_cursor_event(&mut self, kind: CursorEventKind, at: canary_script::Vec2) { + fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) { self.dialog.on_cursor_event(kind, at.into()); } + 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 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(); + let dialog = Dialog::new(style, &info); + Box::new(Self { panel, dialog }) + } +} diff --git a/scripts/sao-ui/src/main_menu.rs b/scripts/sao-ui/src/main_menu.rs index 1f6ce7c..b921a46 100644 --- a/scripts/sao-ui/src/main_menu.rs +++ b/scripts/sao-ui/src/main_menu.rs @@ -1,9 +1,13 @@ +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: AGPL-3.0-or-later + use crate::widgets::prelude::*; 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; @@ -12,15 +16,6 @@ pub struct MainMenuPanel { menu: MainMenu, } -impl BindPanel for MainMenuPanel { - fn bind(panel: Panel, msg: Message) -> Box { - Box::new(Self { - panel, - menu: MainMenu::default(), - }) - } -} - impl PanelImpl for MainMenuPanel { fn update(&mut self, dt: f32) { Widget::update(&mut self.menu, dt); @@ -31,23 +26,35 @@ impl PanelImpl for MainMenuPanel { Widget::draw(&mut self.menu, &ctx); } - fn on_cursor_event(&mut self, kind: CursorEventKind, at: canary_script::Vec2) { + fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) { Widget::on_cursor_event(&mut self.menu, kind, at.into()); } + fn on_resize(&mut self, _size: Vec2) {} + fn on_message(&mut self, msg: Message) {} } +impl MainMenuPanel { + pub fn bind(panel: Panel, msg: Message) -> Box { + Box::new(Self { + panel, + menu: MainMenu::default(), + }) + } +} + pub struct MainMenu { pub menu: Offset>, pub player_info: Reveal>, pub inventory: Reveal>, + pub palette: Reveal>, pub settings: Reveal>, } impl MainMenu { - pub const POSITION_X: f32 = -0.40; - pub const SUBMENU_SPACING: f32 = 0.1; + pub const ANCHOR: Vec2 = Vec2::new(100.0, 100.0); + pub const SUBMENU_SPACING: f32 = 15.0; } impl Default for MainMenu { @@ -56,12 +63,12 @@ impl Default for MainMenu { let icons = ["", "", "", "", "", ""]; let button_style = RoundButtonStyle { - radius: 0.05, - spacing: 0.01, - thickness: 0.002, - body_color: Color::WHITE, - ring_color: Color::WHITE, - icon_color: Color::BLACK, + radius: 7.5, + spacing: 1.5, + thickness: 0.4, + body_color: THEME.palette.surface, + ring_color: THEME.palette.surface, + icon_color: THEME.palette.text, }; let mut buttons = Vec::new(); @@ -75,12 +82,13 @@ impl Default for MainMenu { buttons.push(button); } - let menu = SlotMenu::new(buttons, 0.18); - let menu = Offset::new(menu, Vec2::new(Self::POSITION_X, 0.0)); + let menu = SlotMenu::new(buttons, 30.0); + let menu = Offset::new(menu, Self::ANCHOR); - let submenu_spacing_left = Vec2::new(Self::POSITION_X - Self::SUBMENU_SPACING, 0.0); - let submenu_spacing_right = Vec2::new(Self::POSITION_X + Self::SUBMENU_SPACING, 0.0); - let reveal_slide = -0.02; + let submenu_spacing = Vec2::new(Self::SUBMENU_SPACING, 0.0); + let submenu_spacing_left = Self::ANCHOR - submenu_spacing; + let submenu_spacing_right = Self::ANCHOR + submenu_spacing; + let reveal_slide = -5.0; let reveal_duration = 0.1; let player_info = PlayerInfo::new(); @@ -96,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); @@ -104,6 +121,7 @@ impl Default for MainMenu { menu, player_info, inventory, + palette, settings, } } @@ -114,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); } @@ -133,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(); + } _ => {} }; } @@ -144,14 +169,16 @@ pub struct PlayerInfo { width: f32, height: f32, rounding: f32, + color: Color, } impl PlayerInfo { pub fn new() -> Self { Self { - width: 0.5, - height: 0.9, - rounding: 0.02, + width: 70.0, + height: 120.0, + rounding: 5.0, + color: THEME.palette.surface, } } } @@ -164,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); } } @@ -175,7 +202,7 @@ pub struct Inventory { impl Inventory { pub fn new(available_width: f32) -> (Self, f32) { - let height = 1.28; + let height = 1024.0; ( Self { @@ -189,8 +216,8 @@ impl Inventory { impl Widget for Inventory { fn draw(&mut self, ctx: &DrawContext) { - let box_size = 0.06; - let box_margin = 0.02; + let box_size = 12.0; + let box_margin = 4.0; let box_stride = box_size + box_margin; let grid_width = (self.width / box_stride).floor() as usize; @@ -227,7 +254,7 @@ impl SettingsMenu { ("Log Out", "﫼"), ]; - let button_size = Vec2::new(0.4, 0.1); + let button_size = Vec2::new(90.0, 20.0); let button_rect = Rect::from_xy_size(Vec2::new(0.0, -button_size.y / 2.0), button_size); let mut buttons = Vec::new(); @@ -248,7 +275,7 @@ impl SettingsMenu { buttons.push(button); } - let menu = SlotMenu::new(buttons, 0.12); + let menu = SlotMenu::new(buttons, 25.0); Self { menu, diff --git a/scripts/sao-ui/src/music_player.rs b/scripts/sao-ui/src/music_player.rs new file mode 100644 index 0000000..ab05398 --- /dev/null +++ b/scripts/sao-ui/src/music_player.rs @@ -0,0 +1,427 @@ +use api::*; +use canary_script::*; + +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 { + panel: Panel, + widget: Option, + disconnected: Offset