Merge branch 'main' into wgpu-renderer
This commit is contained in:
commit
6ac82404e6
|
@ -23,6 +23,7 @@ canary-script = { path = "crates/script" }
|
||||||
lyon = "1"
|
lyon = "1"
|
||||||
ouroboros = "^0.15"
|
ouroboros = "^0.15"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
|
prehash = "0.3.3"
|
||||||
slab = "0.4"
|
slab = "0.4"
|
||||||
wasmtime = "0.38"
|
wasmtime = "0.38"
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
name = "canary-magpie"
|
name = "canary-magpie"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "magpie"
|
name = "magpie"
|
||||||
|
@ -13,6 +14,7 @@ anyhow = { version = "1", optional = true }
|
||||||
byteorder = "1.4"
|
byteorder = "1.4"
|
||||||
canary = { path = "../..", optional = true }
|
canary = { path = "../..", optional = true }
|
||||||
canary-wgpu = { path = "../../renderers/wgpu", 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 = { version = "0.8", features = ["net", "os-poll"], optional = true }
|
||||||
mio-signals = { version = "0.2", optional = true }
|
mio-signals = { version = "0.2", optional = true }
|
||||||
parking_lot = { version = "0.12", 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 }
|
winit = { version = "0.27", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
async = ["dep:futures-util"]
|
||||||
service = [
|
service = [
|
||||||
"dep:anyhow",
|
"dep:anyhow",
|
||||||
"dep:canary",
|
"dep:canary",
|
||||||
|
|
|
@ -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<UnixStream>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MagpieClient {
|
|
||||||
pub fn new() -> std::io::Result<Self> {
|
|
||||||
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<T: Serialize>(&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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
// Copyright (c) 2022 Marceline Cramer
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
pub mod client;
|
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
|
|
||||||
#[cfg(feature = "service")]
|
#[cfg(feature = "service")]
|
||||||
|
|
|
@ -43,71 +43,58 @@ pub enum MagpieServerMsg {
|
||||||
SendMessage(SendMessage),
|
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<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
/// A message sent from the Magpie server to a client.
|
/// A message sent from the Magpie server to a client.
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(tag = "kind")]
|
#[serde(tag = "kind")]
|
||||||
pub enum MagpieClientMsg {}
|
pub enum MagpieClientMsg {
|
||||||
|
RecvMessage(RecvMessage),
|
||||||
|
}
|
||||||
|
|
||||||
/// A [Messenger] specialized for Magpie clients.
|
/// A [Messenger] specialized for Magpie clients.
|
||||||
pub type ClientMessenger<T> = Messenger<T, MagpieClientMsg, MagpieServerMsg>;
|
pub type ClientMessenger<T> = Messenger<T, MagpieClientMsg, MagpieServerMsg>;
|
||||||
|
|
||||||
|
impl<T: Write> ClientMessenger<T> {
|
||||||
|
pub fn send_panel_json<O: Serialize>(&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.
|
/// A [Messenger] specialized for Magpie servers.
|
||||||
pub type ServerMessenger<T> = Messenger<T, MagpieServerMsg, MagpieClientMsg>;
|
pub type ServerMessenger<T> = Messenger<T, MagpieServerMsg, MagpieClientMsg>;
|
||||||
|
|
||||||
/// Bidirectional, transport-agnostic Magpie IO wrapper struct.
|
/// Piecewise packet assembler for [Messenger].
|
||||||
pub struct Messenger<T, I, O> {
|
pub struct MessageQueue<I> {
|
||||||
pub transport: T,
|
|
||||||
expected_len: Option<usize>,
|
expected_len: Option<usize>,
|
||||||
received_buf: VecDeque<u8>,
|
received_buf: VecDeque<u8>,
|
||||||
received_queue: VecDeque<I>,
|
received_queue: VecDeque<I>,
|
||||||
closed: bool,
|
|
||||||
_output: PhantomData<O>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Read + Write, I: DeserializeOwned, O: Serialize> Messenger<T, I, O> {
|
impl<I> Default for MessageQueue<I> {
|
||||||
pub fn new(transport: T) -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
transport,
|
|
||||||
expected_len: None,
|
expected_len: None,
|
||||||
received_buf: Default::default(),
|
received_buf: Default::default(),
|
||||||
received_queue: Default::default(),
|
received_queue: Default::default(),
|
||||||
closed: false,
|
|
||||||
_output: PhantomData,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_closed(&self) -> bool {
|
impl<I: DeserializeOwned> MessageQueue<I> {
|
||||||
self.closed
|
pub fn on_data(&mut self, data: &[u8]) -> std::io::Result<()> {
|
||||||
}
|
self.received_buf.write_all(data)?;
|
||||||
|
|
||||||
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::<LittleEndian>(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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(expected_len) = self.expected_len {
|
if let Some(expected_len) = self.expected_len {
|
||||||
|
@ -139,8 +126,137 @@ impl<T: Read + Write, I: DeserializeOwned, O: Serialize> Messenger<T, I, O> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tries to receive a single input packet.
|
|
||||||
pub fn recv(&mut self) -> Option<I> {
|
pub fn recv(&mut self) -> Option<I> {
|
||||||
self.received_queue.pop_back()
|
self.received_queue.pop_back()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bidirectional, transport-agnostic Magpie IO wrapper struct.
|
||||||
|
pub struct Messenger<T, I, O> {
|
||||||
|
transport: T,
|
||||||
|
queue: MessageQueue<I>,
|
||||||
|
closed: bool,
|
||||||
|
_output: PhantomData<O>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, I, O> Messenger<T, I, O> {
|
||||||
|
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<T: Write, I, O: Serialize> Messenger<T, I, O> {
|
||||||
|
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::<LittleEndian>(len)?;
|
||||||
|
self.transport.write_all(&payload)?;
|
||||||
|
self.transport.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Read, I: DeserializeOwned, O> Messenger<T, I, O> {
|
||||||
|
/// 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<I> {
|
||||||
|
self.queue.recv()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
mod async_messages {
|
||||||
|
use super::*;
|
||||||
|
use futures_util::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use std::marker::Unpin;
|
||||||
|
|
||||||
|
impl<T: AsyncWriteExt + Unpin> ClientMessenger<T> {
|
||||||
|
pub async fn send_panel_json_async<O: Serialize>(&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<T: AsyncWriteExt + Unpin, I, O: Serialize> Messenger<T, I, O> {
|
||||||
|
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::<LittleEndian>(len)?;
|
||||||
|
msg.extend_from_slice(&payload);
|
||||||
|
self.transport.write_all(&msg).await?;
|
||||||
|
self.transport.flush().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsyncReadExt + Unpin, I: DeserializeOwned, O> Messenger<T, I, O> {
|
||||||
|
pub async fn recv(&mut self) -> std::io::Result<I> {
|
||||||
|
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])?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,18 +14,29 @@ use mio_signals::{Signal, Signals};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use slab::Slab;
|
use slab::Slab;
|
||||||
|
|
||||||
use crate::protocol::{CreatePanel, MagpieServerMsg, SendMessage, ServerMessenger};
|
use crate::protocol::*;
|
||||||
use crate::service::window::{WindowMessage, WindowMessageSender};
|
use crate::service::window::{WindowMessage, WindowMessageSender};
|
||||||
|
|
||||||
const SOCK_NAME: &str = "magpie.sock";
|
const SOCK_NAME: &str = "magpie.sock";
|
||||||
|
|
||||||
pub enum IpcMessage {}
|
#[derive(Debug)]
|
||||||
|
pub enum IpcMessage {
|
||||||
|
PanelMessage { window: usize, message: Vec<u8> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct IpcMessageSender {
|
pub struct IpcMessageSender {
|
||||||
waker: Waker,
|
waker: Arc<Waker>,
|
||||||
sender: Sender<IpcMessage>,
|
sender: Sender<IpcMessage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
/// Wraps [mio::net::UnixListener] with automatic file deletion on drop.
|
||||||
pub struct Listener {
|
pub struct Listener {
|
||||||
pub uds: UnixListener,
|
pub uds: UnixListener,
|
||||||
|
@ -55,8 +66,38 @@ impl DerefMut for Listener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Listener {
|
||||||
|
fn new() -> std::io::Result<Self> {
|
||||||
|
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 {
|
pub struct IpcData {
|
||||||
poll: Poll,
|
poll: Poll,
|
||||||
|
window_to_client_panel: HashMap<usize, (usize, PanelId)>,
|
||||||
next_window_id: usize,
|
next_window_id: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,27 +117,13 @@ pub struct Client {
|
||||||
id_to_window: HashMap<u32, usize>,
|
id_to_window: HashMap<u32, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
impl Client {
|
||||||
pub fn on_readable(&mut self) -> std::io::Result<bool> {
|
pub fn on_readable(&mut self) -> std::io::Result<bool> {
|
||||||
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);
|
println!("Client #{}: {:?}", self.token.0, msg);
|
||||||
match msg {
|
match msg {
|
||||||
MagpieServerMsg::CreatePanel(CreatePanel {
|
MagpieServerMsg::CreatePanel(CreatePanel {
|
||||||
|
@ -104,7 +131,11 @@ impl Client {
|
||||||
protocol,
|
protocol,
|
||||||
script,
|
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) {
|
if let Some(old_id) = self.id_to_window.insert(id, window) {
|
||||||
let msg = WindowMessage::CloseWindow { id: old_id };
|
let msg = WindowMessage::CloseWindow { id: old_id };
|
||||||
|
@ -129,13 +160,26 @@ impl Client {
|
||||||
|
|
||||||
Ok(self.messenger.is_closed())
|
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 struct Ipc {
|
||||||
pub data: Arc<RwLock<IpcData>>,
|
pub data: Arc<RwLock<IpcData>>,
|
||||||
pub window_sender: WindowMessageSender,
|
pub window_sender: WindowMessageSender,
|
||||||
pub message_recv: Receiver<IpcMessage>,
|
pub message_recv: Receiver<IpcMessage>,
|
||||||
pub events: Events,
|
|
||||||
pub quit: bool,
|
pub quit: bool,
|
||||||
pub listener: Listener,
|
pub listener: Listener,
|
||||||
pub signals: Signals,
|
pub signals: Signals,
|
||||||
|
@ -147,19 +191,10 @@ pub struct Ipc {
|
||||||
|
|
||||||
impl Ipc {
|
impl Ipc {
|
||||||
pub fn new(window_sender: WindowMessageSender) -> std::io::Result<(Self, IpcMessageSender)> {
|
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 mut listener = Listener::new()?;
|
||||||
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 signals = Signals::new(Signal::Interrupt | Signal::Quit)?;
|
let mut signals = Signals::new(Signal::Interrupt | Signal::Quit)?;
|
||||||
|
|
||||||
let events = Events::with_capacity(128);
|
|
||||||
let poll = Poll::new()?;
|
let poll = Poll::new()?;
|
||||||
let listener_token = Token(usize::MAX);
|
let listener_token = Token(usize::MAX);
|
||||||
let signals_token = Token(listener_token.0 - 1);
|
let signals_token = Token(listener_token.0 - 1);
|
||||||
|
@ -173,12 +208,13 @@ impl Ipc {
|
||||||
let (sender, message_recv) = channel();
|
let (sender, message_recv) = channel();
|
||||||
|
|
||||||
let sender = IpcMessageSender {
|
let sender = IpcMessageSender {
|
||||||
waker: Waker::new(registry, message_recv_token)?,
|
waker: Arc::new(Waker::new(registry, message_recv_token)?),
|
||||||
sender,
|
sender,
|
||||||
};
|
};
|
||||||
|
|
||||||
let data = IpcData {
|
let data = IpcData {
|
||||||
poll,
|
poll,
|
||||||
|
window_to_client_panel: HashMap::new(),
|
||||||
next_window_id: 0,
|
next_window_id: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -186,7 +222,6 @@ impl Ipc {
|
||||||
data: Arc::new(RwLock::new(data)),
|
data: Arc::new(RwLock::new(data)),
|
||||||
window_sender,
|
window_sender,
|
||||||
message_recv,
|
message_recv,
|
||||||
events,
|
|
||||||
quit: false,
|
quit: false,
|
||||||
listener,
|
listener,
|
||||||
signals,
|
signals,
|
||||||
|
@ -199,10 +234,29 @@ impl Ipc {
|
||||||
Ok((ipc, sender))
|
Ok((ipc, sender))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn poll(&mut self, timeout: Option<Duration>) -> std::io::Result<()> {
|
pub fn on_message(&mut self, msg: IpcMessage) -> std::io::Result<()> {
|
||||||
self.data.write().poll.poll(&mut self.events, timeout)?;
|
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<Duration>) -> std::io::Result<()> {
|
||||||
|
self.data.write().poll.poll(events, timeout)?;
|
||||||
|
|
||||||
|
for event in events.iter() {
|
||||||
if event.token() == self.listener_token {
|
if event.token() == self.listener_token {
|
||||||
loop {
|
loop {
|
||||||
match self.listener.accept() {
|
match self.listener.accept() {
|
||||||
|
@ -238,13 +292,17 @@ impl Ipc {
|
||||||
let _ = self.window_sender.send_event(WindowMessage::Quit);
|
let _ = self.window_sender.send_event(WindowMessage::Quit);
|
||||||
self.quit = true;
|
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) {
|
} else if let Some(client) = self.clients.get_mut(event.token().0) {
|
||||||
let disconnected = client.on_readable()?;
|
let disconnected = client.on_readable()?;
|
||||||
if disconnected {
|
if disconnected {
|
||||||
self.clients.remove(event.token().0);
|
self.clients.remove(event.token().0).disconnect();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
panic!("Unrecognized event token: {:?}", event);
|
eprintln!("Unrecognized event token: {:?}", event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,9 +310,10 @@ impl Ipc {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(mut self) {
|
pub fn run(mut self) {
|
||||||
|
let mut events = Events::with_capacity(128);
|
||||||
while !self.quit {
|
while !self.quit {
|
||||||
let wait = Duration::from_millis(100);
|
let wait = Duration::from_millis(100);
|
||||||
match self.poll(Some(wait)) {
|
match self.poll(&mut events, Some(wait)) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("IPC poll error: {:?}", e);
|
eprintln!("IPC poll error: {:?}", e);
|
||||||
|
|
|
@ -6,7 +6,6 @@ use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use canary::{CursorEventKind, Panel, Runtime, Vec2, PX_PER_MM};
|
use canary::{CursorEventKind, Panel, Runtime, Vec2, PX_PER_MM};
|
||||||
use canary_wgpu::{wgpu, DrawTarget, Renderer};
|
use canary_wgpu::{wgpu, DrawTarget, Renderer};
|
||||||
use pollster::FutureExt;
|
use pollster::FutureExt;
|
||||||
|
@ -37,6 +36,8 @@ pub enum WindowMessage {
|
||||||
pub type WindowMessageSender = EventLoopProxy<WindowMessage>;
|
pub type WindowMessageSender = EventLoopProxy<WindowMessage>;
|
||||||
|
|
||||||
pub struct Window {
|
pub struct Window {
|
||||||
|
pub ipc_sender: IpcMessageSender,
|
||||||
|
pub ipc_id: usize,
|
||||||
pub window: winit::window::Window,
|
pub window: winit::window::Window,
|
||||||
pub surface: wgpu::Surface,
|
pub surface: wgpu::Surface,
|
||||||
pub surface_config: wgpu::SurfaceConfiguration,
|
pub surface_config: wgpu::SurfaceConfiguration,
|
||||||
|
@ -51,6 +52,8 @@ pub struct Window {
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
ipc_sender: IpcMessageSender,
|
||||||
|
ipc_id: usize,
|
||||||
panel: Panel,
|
panel: Panel,
|
||||||
instance: &wgpu::Instance,
|
instance: &wgpu::Instance,
|
||||||
adapter: &wgpu::Adapter,
|
adapter: &wgpu::Adapter,
|
||||||
|
@ -78,6 +81,8 @@ impl Window {
|
||||||
surface.configure(&device, &surface_config);
|
surface.configure(&device, &surface_config);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
ipc_sender,
|
||||||
|
ipc_id,
|
||||||
window,
|
window,
|
||||||
surface,
|
surface,
|
||||||
surface_config,
|
surface_config,
|
||||||
|
@ -99,11 +104,22 @@ impl Window {
|
||||||
self.window.request_redraw();
|
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) {
|
pub fn update(&mut self) {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let dt = now.duration_since(self.last_update).as_secs_f32();
|
let dt = now.duration_since(self.last_update).as_secs_f32();
|
||||||
self.panel.update(dt);
|
self.panel.update(dt);
|
||||||
self.last_update = now;
|
self.last_update = now;
|
||||||
|
self.recv_messages();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(&mut self) {
|
pub fn draw(&mut self) {
|
||||||
|
@ -124,15 +140,19 @@ impl Window {
|
||||||
self.renderer.render(target, &commands);
|
self.renderer.render(target, &commands);
|
||||||
|
|
||||||
output.present();
|
output.present();
|
||||||
|
|
||||||
|
self.recv_messages();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_message(&mut self, msg: Vec<u8>) {
|
pub fn send_message(&mut self, msg: Vec<u8>) {
|
||||||
self.panel.on_message(msg);
|
self.panel.on_message(msg);
|
||||||
|
self.recv_messages();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
|
pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
|
||||||
let mm = Vec2::new(new_size.width as f32, new_size.height as f32) * PX_PER_MM;
|
let mm = Vec2::new(new_size.width as f32, new_size.height as f32) * PX_PER_MM;
|
||||||
self.panel.on_resize(mm);
|
self.panel.on_resize(mm);
|
||||||
|
self.recv_messages();
|
||||||
self.window.request_redraw();
|
self.window.request_redraw();
|
||||||
|
|
||||||
if new_size.width > 0 && new_size.height > 0 {
|
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.panel.on_cursor_event(event, self.cursor_pos);
|
||||||
|
self.recv_messages();
|
||||||
}
|
}
|
||||||
WindowEvent::MouseInput {
|
WindowEvent::MouseInput {
|
||||||
state,
|
state,
|
||||||
|
@ -176,6 +197,7 @@ impl Window {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.panel.on_cursor_event(event, self.cursor_pos);
|
self.panel.on_cursor_event(event, self.cursor_pos);
|
||||||
|
self.recv_messages();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -251,12 +273,19 @@ impl WindowStore {
|
||||||
message: WindowMessage,
|
message: WindowMessage,
|
||||||
) -> anyhow::Result<bool> {
|
) -> anyhow::Result<bool> {
|
||||||
match message {
|
match message {
|
||||||
WindowMessage::OpenWindow { id, protocol, script } => {
|
WindowMessage::OpenWindow {
|
||||||
|
id,
|
||||||
|
protocol,
|
||||||
|
script,
|
||||||
|
} => {
|
||||||
println!("Opening window {} with script {:?}", id, script);
|
println!("Opening window {} with script {:?}", id, script);
|
||||||
let module = std::fs::read(script)?;
|
let module = std::fs::read(script)?;
|
||||||
let mut script = self.runtime.load_module(&module)?;
|
let mut script = self.runtime.load_module(&module)?;
|
||||||
let panel = script.create_panel(&protocol, vec![])?;
|
let panel = script.create_panel(&protocol, vec![])?;
|
||||||
|
|
||||||
let window = Window::new(
|
let window = Window::new(
|
||||||
|
self.ipc_sender.to_owned(),
|
||||||
|
id,
|
||||||
panel,
|
panel,
|
||||||
&self.instance,
|
&self.instance,
|
||||||
&self.adapter,
|
&self.adapter,
|
||||||
|
@ -265,6 +294,7 @@ impl WindowStore {
|
||||||
self.renderer.to_owned(),
|
self.renderer.to_owned(),
|
||||||
&event_loop,
|
&event_loop,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let window_id = window.get_id();
|
let window_id = window.get_id();
|
||||||
self.windows.insert(window_id, window);
|
self.windows.insert(window_id, window);
|
||||||
self.ipc_to_window.insert(id, window_id);
|
self.ipc_to_window.insert(id, window_id);
|
||||||
|
|
|
@ -10,10 +10,12 @@ path = "src/main.rs"
|
||||||
required-features = ["bin"]
|
required-features = ["bin"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
canary-magpie = { path = "../magpie", optional = true }
|
canary-magpie = { path = "../magpie", optional = true, features = ["async"] }
|
||||||
mpris = { version = "2.0.0-rc3", optional = true }
|
futures-util = { version = "0.3", optional = true }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
smol = { version = "1.2", optional = true }
|
||||||
|
zbus = { version = "3.5", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
bin = ["dep:canary-magpie", "dep:mpris"]
|
bin = ["dep:canary-magpie", "dep:futures-util", "dep:smol", "dep:zbus"]
|
||||||
|
|
|
@ -34,9 +34,6 @@ pub enum LoopStatus {
|
||||||
pub struct ProgressChanged {
|
pub struct ProgressChanged {
|
||||||
/// Current position into the track in seconds.
|
/// Current position into the track in seconds.
|
||||||
pub position: f32,
|
pub position: f32,
|
||||||
|
|
||||||
/// Length of the current track in seconds.
|
|
||||||
pub length: Option<f32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
@ -48,7 +45,7 @@ pub struct AlbumInfo {
|
||||||
pub artists: Vec<String>,
|
pub artists: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct TrackInfo {
|
pub struct TrackInfo {
|
||||||
/// The title of the current track.
|
/// The title of the current track.
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
|
@ -58,6 +55,9 @@ pub struct TrackInfo {
|
||||||
|
|
||||||
/// The optional track number on the disc the album the track appears on.
|
/// The optional track number on the disc the album the track appears on.
|
||||||
pub track_number: Option<i32>,
|
pub track_number: Option<i32>,
|
||||||
|
|
||||||
|
/// Length of the track in seconds.
|
||||||
|
pub length: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
@ -104,6 +104,6 @@ pub enum OutMsg {
|
||||||
/// Sets the volume. Values are clamped to 0.0 to 1.0.
|
/// Sets the volume. Values are clamped to 0.0 to 1.0.
|
||||||
SetVolume { volume: f32 },
|
SetVolume { volume: f32 },
|
||||||
|
|
||||||
/// Set the current track position in seconds.
|
/// Seeks the current track's position in seconds.
|
||||||
SetPosition { position: f32 },
|
Seek { offset: f32 },
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,81 +1,193 @@
|
||||||
// Copyright (c) 2022 Marceline Cramer
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use canary_music_player::*;
|
use std::path::Path;
|
||||||
use canary_magpie::client::MagpieClient;
|
|
||||||
use canary_magpie::protocol::{CreatePanel, MagpieServerMsg};
|
|
||||||
use canary_music_player::*;
|
|
||||||
use mpris::PlayerFinder;
|
|
||||||
|
|
||||||
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<UnixStream>;
|
||||||
|
|
||||||
|
pub mod mpris;
|
||||||
|
|
||||||
|
use mpris::*;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Metadata {
|
||||||
pub album: AlbumInfo,
|
pub album: AlbumInfo,
|
||||||
pub track: TrackInfo,
|
pub track: TrackInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&mpris::Metadata> for MetadataTracker {
|
impl<'a> From<MetadataMap<'a>> for Metadata {
|
||||||
fn from(metadata: &mpris::Metadata) -> Self {
|
fn from(map: MetadataMap<'a>) -> Self {
|
||||||
let album = AlbumInfo {
|
let album = AlbumInfo {
|
||||||
title: metadata.album_name().map(ToString::to_string),
|
title: map
|
||||||
artists: metadata
|
.get("xesam:album")
|
||||||
.album_artists()
|
.and_then(|v| TryFrom::try_from(v).ok()),
|
||||||
.unwrap_or(Vec::new())
|
artists: map
|
||||||
.iter()
|
.get("xesam:albumArtist")
|
||||||
.map(ToString::to_string)
|
.cloned()
|
||||||
.collect(),
|
.and_then(|v| TryFrom::try_from(v).ok())
|
||||||
|
.unwrap_or(Vec::new()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let track = TrackInfo {
|
let track = TrackInfo {
|
||||||
title: metadata.title().map(ToString::to_string),
|
title: map
|
||||||
artists: metadata
|
.get("xesam:title")
|
||||||
.artists()
|
.and_then(|v| TryFrom::try_from(v).ok()),
|
||||||
.unwrap_or(Vec::new())
|
artists: map
|
||||||
.iter()
|
.get("xesam:artist")
|
||||||
.map(ToString::to_string)
|
.cloned()
|
||||||
.collect(),
|
.and_then(|v| TryFrom::try_from(v).ok())
|
||||||
track_number: metadata.track_number(),
|
.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 }
|
Self { album, track }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MetadataTracker {
|
impl Metadata {
|
||||||
pub fn new(magpie: &mut MagpieClient, metadata: &mpris::Metadata) -> Self {
|
pub async fn update_new(magpie: &mut MagpieClient, metadata: MetadataMap<'_>) -> Self {
|
||||||
let new: Self = metadata.into();
|
let new: Self = metadata.into();
|
||||||
magpie.send_json_message(0, &InMsg::AlbumChanged(new.album.clone()));
|
let msg = InMsg::AlbumChanged(new.album.clone());
|
||||||
magpie.send_json_message(0, &InMsg::TrackChanged(new.track.clone()));
|
magpie.send_panel_json_async(0, &msg).await;
|
||||||
magpie.send_json_message(
|
let msg = InMsg::TrackChanged(new.track.clone());
|
||||||
0,
|
magpie.send_panel_json_async(0, &msg).await;
|
||||||
&InMsg::ProgressChanged(ProgressChanged {
|
|
||||||
position: 0.0,
|
|
||||||
length: metadata.length().map(|l| l.as_secs_f32()),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
new
|
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();
|
let new: Self = metadata.into();
|
||||||
|
|
||||||
if self.album != new.album {
|
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 {
|
if self.track != new.track {
|
||||||
messenger.send_json_message(0, &InMsg::TrackChanged(new.track.clone()));
|
let msg = InMsg::TrackChanged(new.track.clone());
|
||||||
messenger.send_json_message(
|
messenger.send_panel_json_async(0, &msg).await;
|
||||||
0,
|
let progress = ProgressChanged { position: 0.0 };
|
||||||
&InMsg::ProgressChanged(ProgressChanged {
|
let msg = InMsg::ProgressChanged(progress);
|
||||||
position: 0.0,
|
messenger.send_panel_json_async(0, &msg).await;
|
||||||
length: metadata.length().map(|l| l.as_secs_f32()),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*self = new;
|
*self = new;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn on_message(
|
||||||
|
player: &PlayerProxy<'_>,
|
||||||
|
magpie: &mut MagpieClient,
|
||||||
|
message: MagpieClientMsg,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
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<dyn std::error::Error>> {
|
||||||
|
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() {
|
fn main() {
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
let module_path = args
|
let module_path = args
|
||||||
|
@ -83,105 +195,71 @@ fn main() {
|
||||||
.expect("Please pass a path to a Canary script!")
|
.expect("Please pass a path to a Canary script!")
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
let player_finder = PlayerFinder::new().expect("Could not connect to D-Bus");
|
smol::block_on(async {
|
||||||
|
let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set");
|
||||||
let mut magpie = MagpieClient::new().unwrap();
|
let sock_dir = Path::new(&sock_dir);
|
||||||
let protocol = "tebibyte-media.desktop.music-player-controller".to_string();
|
let sock_path = sock_dir.join(MAGPIE_SOCK);
|
||||||
let script = std::path::PathBuf::from(&module_path);
|
let socket = UnixStream::connect(sock_path).await.unwrap();
|
||||||
let msg = CreatePanel { id: 0, protocol, script };
|
let mut magpie = MagpieClient::new(socket);
|
||||||
let msg = MagpieServerMsg::CreatePanel(msg);
|
let protocol = "tebibyte-media.desktop.music-player-controller".to_string();
|
||||||
magpie.messenger.send(&msg).unwrap();
|
let script = std::path::PathBuf::from(&module_path);
|
||||||
|
let msg = CreatePanel {
|
||||||
let mut first_loop = true;
|
id: 0,
|
||||||
let mut connected = false;
|
protocol,
|
||||||
|
script,
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
println!(
|
let msg = MagpieServerMsg::CreatePanel(msg);
|
||||||
"Connected to \"{}\" ({})",
|
magpie.send_async(&msg).await.unwrap();
|
||||||
player.identity(),
|
|
||||||
player.bus_name()
|
|
||||||
);
|
|
||||||
connected = true;
|
|
||||||
magpie.send_json_message(0, &InMsg::Connected);
|
|
||||||
|
|
||||||
let metadata = player.get_metadata().unwrap();
|
let dbus = zbus::Connection::session().await.unwrap();
|
||||||
let mut metadata_tracker = MetadataTracker::new(&mut magpie, &metadata);
|
|
||||||
|
|
||||||
let mut events = match player.events() {
|
let mut first_loop = true;
|
||||||
Ok(events) => events,
|
let mut connected = false;
|
||||||
Err(err) => {
|
|
||||||
eprintln!("Player events D-Bus error: {:?}", err);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let event = match events.next() {
|
if !first_loop {
|
||||||
None => break,
|
let wait = std::time::Duration::from_secs(1);
|
||||||
Some(Ok(e)) => e,
|
std::thread::sleep(wait);
|
||||||
Some(Err(err)) => {
|
}
|
||||||
eprintln!("D-Bus error while reading player events: {:?}", err);
|
|
||||||
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
Err(err) => {
|
||||||
|
eprintln!("D-Bus error while finding player: {:?}", err);
|
||||||
use mpris::Event::*;
|
return;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(msg) = in_msg {
|
println!(
|
||||||
magpie.send_json_message(0, &msg);
|
"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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<String, Value<'a>>;
|
||||||
|
|
||||||
|
#[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<String>;
|
||||||
|
|
||||||
|
#[dbus_proxy(property)]
|
||||||
|
fn position(&self) -> Result<i64>;
|
||||||
|
|
||||||
|
#[dbus_proxy(property)]
|
||||||
|
fn metadata(&self) -> Result<MetadataMap>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find_player(connection: &Connection) -> Result<Option<PlayerProxy>> {
|
||||||
|
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)
|
||||||
|
}
|
|
@ -33,6 +33,7 @@ struct App {
|
||||||
last_update: Instant,
|
last_update: Instant,
|
||||||
protocol_buf: String,
|
protocol_buf: String,
|
||||||
bind_message_buf: String,
|
bind_message_buf: String,
|
||||||
|
panel_bg: egui::Color32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
@ -59,6 +60,7 @@ impl App {
|
||||||
last_update: Instant::now(),
|
last_update: Instant::now(),
|
||||||
protocol_buf: String::new(),
|
protocol_buf: String::new(),
|
||||||
bind_message_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();
|
ctx.request_repaint();
|
||||||
|
|
||||||
egui::SidePanel::left("left_panel").show(ctx, |ui| {
|
egui::SidePanel::left("left_panel").show(ctx, |ui| {
|
||||||
|
ui.heading("New Panel");
|
||||||
|
|
||||||
ui.label("Protocol name:");
|
ui.label("Protocol name:");
|
||||||
ui.text_edit_singleline(&mut self.protocol_buf);
|
ui.text_edit_singleline(&mut self.protocol_buf);
|
||||||
|
|
||||||
|
@ -91,6 +95,14 @@ impl eframe::App for App {
|
||||||
|
|
||||||
self.panels.push(panel);
|
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();
|
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() {
|
for panel in self.panels.iter_mut() {
|
||||||
panel.panel.update(dt);
|
panel.panel.update(dt);
|
||||||
panel.show(ctx);
|
panel.show(self.panel_bg, ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,83 +124,87 @@ pub struct PanelWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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));
|
let window_id = egui::Id::new(format!("panel_{}", self.index));
|
||||||
egui::Window::new("Panel").id(window_id).show(ctx, |ui| {
|
egui::Window::new("Panel")
|
||||||
egui::menu::bar(ui, |ui| {
|
.frame(frame)
|
||||||
ui.checkbox(&mut self.show_msg, "Show Message Editor");
|
.id(window_id)
|
||||||
});
|
.show(ctx, |ui| {
|
||||||
|
egui::menu::bar(ui, |ui| {
|
||||||
|
ui.checkbox(&mut self.show_msg, "Show Message Editor");
|
||||||
|
});
|
||||||
|
|
||||||
let sense = egui::Sense {
|
let sense = egui::Sense {
|
||||||
click: true,
|
click: true,
|
||||||
drag: true,
|
drag: true,
|
||||||
focusable: 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
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
if rect.size() != self.current_size {
|
||||||
let uv = egui::pos2(0.0, 0.0);
|
let size = rect.size();
|
||||||
let mut mesh = egui::Mesh::with_texture(texture);
|
self.current_size = size;
|
||||||
|
|
||||||
let commands = self.panel.draw();
|
let size = canary::Vec2::new(size.x, size.y);
|
||||||
for command in commands.into_iter() {
|
self.panel.on_resize(size * PX_PER_MM);
|
||||||
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);
|
if let Some(hover_pos) = response.hover_pos() {
|
||||||
let shape = egui::Shape::mesh(mesh);
|
let local = (hover_pos - rect.left_top()) * PX_PER_MM;
|
||||||
painter.add(shape);
|
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));
|
let msg_edit_id = egui::Id::new(format!("msg_edit_{}", self.index));
|
||||||
egui::Window::new("Message Editor")
|
egui::Window::new("Message Editor")
|
||||||
|
|
|
@ -12,16 +12,16 @@ dir="$(pwd | sed 's/\//\n/g' | tail -n 1)"
|
||||||
|
|
||||||
for toml in $(find "$PWD" -name "Cargo.toml"); do
|
for toml in $(find "$PWD" -name "Cargo.toml"); do
|
||||||
printf "Project: %s\n" "$(tomcat package.name "$toml")"
|
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" |\
|
for file in $(find "$(printf "%s\n" "$toml" |\
|
||||||
sed 's/Cargo\.toml/src/g')" -name "*.rs")
|
sed 's/Cargo\.toml/src/g')" -name "*.rs")
|
||||||
do
|
do
|
||||||
info="$(head -n 2 "$file")"
|
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 |\
|
if ! [ "$toml_lic" = "$(printf "%s\n" "$info" | tail -n 1 |\
|
||||||
sed -n 's/\/\/ SPDX-License-Identifier: //p')" ]
|
sed -n 's/\/\/ SPDX-License-Identifier: //p')" ]
|
||||||
then
|
then
|
||||||
|
|
|
@ -94,6 +94,10 @@ impl Panel {
|
||||||
|
|
||||||
self.draw_indexed(&vertices, &indices);
|
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)]
|
#[repr(transparent)]
|
||||||
|
@ -443,4 +447,6 @@ extern "C" {
|
||||||
|
|
||||||
fn message_get_len(id: u32) -> u32;
|
fn message_get_len(id: u32) -> u32;
|
||||||
fn message_get_data(id: u32, ptr: u32);
|
fn message_get_data(id: u32, ptr: u32);
|
||||||
|
|
||||||
|
fn panel_send_message(id: u32, message_ptr: u32, message_len: u32);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 a = self.0 as u8 as u16;
|
||||||
let multiplied = ((a * (mul as u16)) >> 8) as u8;
|
let multiplied = ((a * (mul as u16)) >> 8) as u8;
|
||||||
self.with_alpha(multiplied)
|
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)
|
Self(self.0 & 0xffffff00 | alpha as u32)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||||
pub mod anim;
|
pub mod anim;
|
||||||
pub mod main_menu;
|
pub mod main_menu;
|
||||||
pub mod music_player;
|
pub mod music_player;
|
||||||
|
pub mod style;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
use api::*;
|
use api::*;
|
||||||
|
@ -23,6 +24,7 @@ fn bind_panel_impl(panel: Panel, protocol: Message, msg: Message) -> Box<dyn Pan
|
||||||
|
|
||||||
match protocol.as_str() {
|
match protocol.as_str() {
|
||||||
"tebibyte-media.desktop.music-player-controller" => MusicPlayerPanel::bind(panel, msg),
|
"tebibyte-media.desktop.music-player-controller" => MusicPlayerPanel::bind(panel, msg),
|
||||||
|
"wip-dialog" => ConfirmationDialogPanel::bind(panel, msg),
|
||||||
_ => MainMenuPanel::bind(panel, msg),
|
_ => MainMenuPanel::bind(panel, msg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,15 +52,23 @@ impl PanelImpl for ConfirmationDialogPanel {
|
||||||
self.dialog.on_cursor_event(kind, at.into());
|
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) {}
|
fn on_message(&mut self, _msg: Message) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfirmationDialogPanel {
|
impl ConfirmationDialogPanel {
|
||||||
pub fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
pub fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
||||||
let msg = msg.to_vec();
|
// let msg = msg.to_vec();
|
||||||
let info: DialogInfo = serde_json::from_slice(&msg).unwrap();
|
// 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::*;
|
use widgets::dialog::*;
|
||||||
let style = DialogStyle::default();
|
let style = DialogStyle::default();
|
||||||
|
|
|
@ -7,6 +7,7 @@ use crate::{DrawContext, Rect};
|
||||||
use button::{RectButton, RoundButton, RoundButtonStyle};
|
use button::{RectButton, RoundButton, RoundButtonStyle};
|
||||||
use dialog::{Dialog, DialogInfo, DialogResponse, DialogStyle};
|
use dialog::{Dialog, DialogInfo, DialogResponse, DialogStyle};
|
||||||
use menu::{SlotMenu, SlotMenuEvent, TabMenu};
|
use menu::{SlotMenu, SlotMenuEvent, TabMenu};
|
||||||
|
use palette::Palette;
|
||||||
use shell::{Offset, OffsetAlignment, Popup, Reveal};
|
use shell::{Offset, OffsetAlignment, Popup, Reveal};
|
||||||
use text::LabelText;
|
use text::LabelText;
|
||||||
|
|
||||||
|
@ -47,6 +48,7 @@ pub struct MainMenu {
|
||||||
pub menu: Offset<SlotMenu<RoundButton>>,
|
pub menu: Offset<SlotMenu<RoundButton>>,
|
||||||
pub player_info: Reveal<Offset<PlayerInfo>>,
|
pub player_info: Reveal<Offset<PlayerInfo>>,
|
||||||
pub inventory: Reveal<Offset<TabMenu>>,
|
pub inventory: Reveal<Offset<TabMenu>>,
|
||||||
|
pub palette: Reveal<Offset<Palette>>,
|
||||||
pub settings: Reveal<Offset<SettingsMenu>>,
|
pub settings: Reveal<Offset<SettingsMenu>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,9 +66,9 @@ impl Default for MainMenu {
|
||||||
radius: 7.5,
|
radius: 7.5,
|
||||||
spacing: 1.5,
|
spacing: 1.5,
|
||||||
thickness: 0.4,
|
thickness: 0.4,
|
||||||
body_color: Color::WHITE,
|
body_color: THEME.palette.surface,
|
||||||
ring_color: Color::WHITE,
|
ring_color: THEME.palette.surface,
|
||||||
icon_color: Color::BLACK,
|
icon_color: THEME.palette.text,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut buttons = Vec::new();
|
let mut buttons = Vec::new();
|
||||||
|
@ -102,6 +104,15 @@ impl Default for MainMenu {
|
||||||
let inventory = Offset::new(inventory, submenu_spacing_right);
|
let inventory = Offset::new(inventory, submenu_spacing_right);
|
||||||
let inventory = Reveal::new(inventory, reveal_slide, reveal_duration);
|
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 = SettingsMenu::new();
|
||||||
let settings = Offset::new(settings, submenu_spacing_right);
|
let settings = Offset::new(settings, submenu_spacing_right);
|
||||||
let settings = Reveal::new(settings, reveal_slide, reveal_duration);
|
let settings = Reveal::new(settings, reveal_slide, reveal_duration);
|
||||||
|
@ -110,6 +121,7 @@ impl Default for MainMenu {
|
||||||
menu,
|
menu,
|
||||||
player_info,
|
player_info,
|
||||||
inventory,
|
inventory,
|
||||||
|
palette,
|
||||||
settings,
|
settings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,6 +132,7 @@ impl Container for MainMenu {
|
||||||
f(&mut self.menu);
|
f(&mut self.menu);
|
||||||
f(&mut self.player_info);
|
f(&mut self.player_info);
|
||||||
f(&mut self.inventory);
|
f(&mut self.inventory);
|
||||||
|
f(&mut self.palette);
|
||||||
f(&mut self.settings);
|
f(&mut self.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,8 +152,14 @@ impl Container for MainMenu {
|
||||||
self.player_info.hide();
|
self.player_info.hide();
|
||||||
self.inventory.hide();
|
self.inventory.hide();
|
||||||
}
|
}
|
||||||
SlotMenuEvent::SubmenuOpen(4) => self.settings.show(),
|
SlotMenuEvent::SubmenuOpen(4) => {
|
||||||
SlotMenuEvent::SubmenuClose(4) => self.settings.hide(),
|
self.palette.show();
|
||||||
|
self.settings.show();
|
||||||
|
}
|
||||||
|
SlotMenuEvent::SubmenuClose(4) => {
|
||||||
|
self.palette.hide();
|
||||||
|
self.settings.hide();
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -150,6 +169,7 @@ pub struct PlayerInfo {
|
||||||
width: f32,
|
width: f32,
|
||||||
height: f32,
|
height: f32,
|
||||||
rounding: f32,
|
rounding: f32,
|
||||||
|
color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlayerInfo {
|
impl PlayerInfo {
|
||||||
|
@ -158,6 +178,7 @@ impl PlayerInfo {
|
||||||
width: 70.0,
|
width: 70.0,
|
||||||
height: 120.0,
|
height: 120.0,
|
||||||
rounding: 5.0,
|
rounding: 5.0,
|
||||||
|
color: THEME.palette.surface,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,7 +191,7 @@ impl RectBounds for PlayerInfo {
|
||||||
|
|
||||||
impl Widget for PlayerInfo {
|
impl Widget for PlayerInfo {
|
||||||
fn draw(&mut self, ctx: &DrawContext) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
|
// Copyright (c) 2022 Marceline Crmaer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use api::*;
|
use api::*;
|
||||||
use canary_script::*;
|
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 crate::widgets::prelude::*;
|
||||||
use button::{RoundButton, RoundButtonStyle};
|
use button::{RoundButton, RoundButtonStyle};
|
||||||
use dialog::{DialogBodyStyle, DialogFooterStyle};
|
use dialog::{DialogBodyStyle, DialogFooterStyle};
|
||||||
use shell::Offset;
|
use shell::Offset;
|
||||||
|
use slider::Slider;
|
||||||
use text::{HorizontalAlignment, Label, LabelText};
|
use text::{HorizontalAlignment, Label, LabelText};
|
||||||
|
|
||||||
pub struct MusicPlayerPanel {
|
pub struct MusicPlayerPanel {
|
||||||
|
@ -57,7 +61,7 @@ impl PanelImpl for MusicPlayerPanel {
|
||||||
use InMsg::*;
|
use InMsg::*;
|
||||||
match (self.widget.as_mut(), msg) {
|
match (self.widget.as_mut(), msg) {
|
||||||
(Some(_), Disconnected) => self.widget = None,
|
(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), AlbumChanged(info)) => widget.update_album(info),
|
||||||
(Some(widget), TrackChanged(info)) => widget.update_track(info),
|
(Some(widget), TrackChanged(info)) => widget.update_track(info),
|
||||||
(Some(widget), PlaybackStatusChanged(status)) => widget.update_playback_status(status),
|
(Some(widget), PlaybackStatusChanged(status)) => widget.update_playback_status(status),
|
||||||
|
@ -91,6 +95,7 @@ pub struct MusicPlayerStyle {
|
||||||
pub rounding: f32,
|
pub rounding: f32,
|
||||||
pub art_margin: f32,
|
pub art_margin: f32,
|
||||||
pub button_spacing: f32,
|
pub button_spacing: f32,
|
||||||
|
pub slider_height: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MusicPlayerStyle {
|
impl Default for MusicPlayerStyle {
|
||||||
|
@ -101,11 +106,13 @@ impl Default for MusicPlayerStyle {
|
||||||
rounding: 5.0,
|
rounding: 5.0,
|
||||||
art_margin: 5.0,
|
art_margin: 5.0,
|
||||||
button_spacing: 15.0,
|
button_spacing: 15.0,
|
||||||
|
slider_height: 7.5,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MusicPlayerWidget {
|
pub struct MusicPlayerWidget {
|
||||||
|
panel: Panel,
|
||||||
artist: Offset<Label>,
|
artist: Offset<Label>,
|
||||||
album: Offset<Label>,
|
album: Offset<Label>,
|
||||||
track: Offset<Label>,
|
track: Offset<Label>,
|
||||||
|
@ -114,12 +121,15 @@ pub struct MusicPlayerWidget {
|
||||||
next: Offset<RoundButton>,
|
next: Offset<RoundButton>,
|
||||||
position: Offset<Label>,
|
position: Offset<Label>,
|
||||||
duration: Offset<Label>,
|
duration: Offset<Label>,
|
||||||
|
slider: Slider,
|
||||||
style: MusicPlayerStyle,
|
style: MusicPlayerStyle,
|
||||||
art_rect: Rect,
|
art_rect: Rect,
|
||||||
body_rect: Rect,
|
body_rect: Rect,
|
||||||
footer_rect: Rect,
|
footer_rect: Rect,
|
||||||
position_secs: f32,
|
position_secs: f32,
|
||||||
|
duration_secs: f32,
|
||||||
position_dirty: bool,
|
position_dirty: bool,
|
||||||
|
position_updating: bool,
|
||||||
status: PlaybackStatus,
|
status: PlaybackStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,18 +143,48 @@ impl Container for MusicPlayerWidget {
|
||||||
f(&mut self.next);
|
f(&mut self.next);
|
||||||
f(&mut self.position);
|
f(&mut self.position);
|
||||||
f(&mut self.duration);
|
f(&mut self.duration);
|
||||||
|
f(&mut self.slider);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, dt: f32) {
|
fn update(&mut self, dt: f32) {
|
||||||
if let PlaybackStatus::Playing = self.status {
|
let position_display = if let Some(position) = self.slider.has_update() {
|
||||||
|
self.position_updating = true;
|
||||||
|
Some(position * self.duration_secs)
|
||||||
|
} else if self.position_updating {
|
||||||
|
let position = self.slider.get_position() * self.duration_secs;
|
||||||
|
let offset = position - self.position_secs;
|
||||||
|
let msg = OutMsg::Seek { offset };
|
||||||
|
self.send_message(&msg);
|
||||||
|
self.position_secs = position;
|
||||||
|
self.position_updating = false;
|
||||||
|
Some(position)
|
||||||
|
} else if let PlaybackStatus::Playing = self.status {
|
||||||
self.position_secs += dt;
|
self.position_secs += dt;
|
||||||
self.position_dirty = true;
|
Some(self.position_secs)
|
||||||
|
} else if self.position_dirty {
|
||||||
|
self.position_dirty = false;
|
||||||
|
Some(self.position_secs)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(position) = position_display {
|
||||||
|
self.position_dirty = false;
|
||||||
|
self.position.set_text(&Self::format_time(position));
|
||||||
|
self.slider
|
||||||
|
.set_position(self.position_secs / self.duration_secs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.position_dirty {
|
if self.previous.was_clicked() {
|
||||||
self.position_dirty = false;
|
self.send_message(&OutMsg::Previous);
|
||||||
self.position
|
}
|
||||||
.set_text(&Self::format_time(self.position_secs));
|
|
||||||
|
if self.play.was_clicked() {
|
||||||
|
self.send_message(&OutMsg::PlayPause);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.next.was_clicked() {
|
||||||
|
self.send_message(&OutMsg::Next);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,7 +196,8 @@ impl Container for MusicPlayerWidget {
|
||||||
self.style.body.color,
|
self.style.body.color,
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.draw_rounded_rect(self.art_rect, self.style.rounding, Color::MAGENTA);
|
let placeholder_art_color = THEME.palette.overlay;
|
||||||
|
ctx.draw_rounded_rect(self.art_rect, self.style.rounding, placeholder_art_color);
|
||||||
|
|
||||||
ctx.draw_partially_rounded_rect(
|
ctx.draw_partially_rounded_rect(
|
||||||
CornerFlags::BOTTOM,
|
CornerFlags::BOTTOM,
|
||||||
|
@ -168,7 +209,7 @@ impl Container for MusicPlayerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MusicPlayerWidget {
|
impl MusicPlayerWidget {
|
||||||
pub fn new() -> Self {
|
pub fn new(panel: Panel) -> Self {
|
||||||
let style = MusicPlayerStyle::default();
|
let style = MusicPlayerStyle::default();
|
||||||
let display_font = Font::new(crate::DISPLAY_FONT);
|
let display_font = Font::new(crate::DISPLAY_FONT);
|
||||||
let content_font = Font::new(crate::CONTENT_FONT);
|
let content_font = Font::new(crate::CONTENT_FONT);
|
||||||
|
@ -179,7 +220,8 @@ impl MusicPlayerWidget {
|
||||||
text: content.to_string(),
|
text: content.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let label = Label::new_centered(text, 10.0, Color::BLACK);
|
let color = style.body.text_color;
|
||||||
|
let label = Label::new_centered(text, 10.0, color);
|
||||||
Offset::new(label, Vec2::ZERO)
|
Offset::new(label, Vec2::ZERO)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -196,7 +238,7 @@ impl MusicPlayerWidget {
|
||||||
text,
|
text,
|
||||||
HorizontalAlignment::Center,
|
HorizontalAlignment::Center,
|
||||||
scale,
|
scale,
|
||||||
Color::BLACK,
|
THEME.palette.text,
|
||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
baseline,
|
baseline,
|
||||||
|
@ -225,25 +267,31 @@ impl MusicPlayerWidget {
|
||||||
radius: style.footer.height * 0.3,
|
radius: style.footer.height * 0.3,
|
||||||
spacing: style.footer.height * 0.1,
|
spacing: style.footer.height * 0.1,
|
||||||
thickness: style.footer.height * 0.025,
|
thickness: style.footer.height * 0.025,
|
||||||
body_color: Color(0xf1b841ff),
|
body_color: THEME.palette.yellow,
|
||||||
ring_color: Color(0xf1b841ff),
|
ring_color: THEME.palette.yellow,
|
||||||
icon_color: Color::BLACK,
|
icon_color: THEME.palette.black,
|
||||||
};
|
};
|
||||||
|
|
||||||
let secondary_button = RoundButtonStyle {
|
let secondary_button = RoundButtonStyle {
|
||||||
radius: style.footer.height * 0.25,
|
radius: style.footer.height * 0.25,
|
||||||
spacing: style.footer.height * 0.05,
|
spacing: style.footer.height * 0.05,
|
||||||
thickness: style.footer.height * 0.025,
|
thickness: style.footer.height * 0.025,
|
||||||
body_color: Color::WHITE,
|
body_color: style.footer.color,
|
||||||
ring_color: Color::BLACK,
|
ring_color: THEME.palette.black,
|
||||||
icon_color: Color::BLACK,
|
icon_color: THEME.palette.black,
|
||||||
};
|
};
|
||||||
|
|
||||||
let prev = RoundButton::new(secondary_button.clone(), Some(prev_text));
|
let prev = RoundButton::new(secondary_button.clone(), Some(prev_text));
|
||||||
let play = RoundButton::new(primary_button, Some(play_text));
|
let play = RoundButton::new(primary_button, Some(play_text));
|
||||||
let next = RoundButton::new(secondary_button, Some(next_text));
|
let next = RoundButton::new(secondary_button, Some(next_text));
|
||||||
|
|
||||||
|
let slider = Slider::new(
|
||||||
|
Default::default(),
|
||||||
|
Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
||||||
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
panel,
|
||||||
artist: make_body_label("Artist"),
|
artist: make_body_label("Artist"),
|
||||||
album: make_body_label("Album"),
|
album: make_body_label("Album"),
|
||||||
track: make_body_label("Track"),
|
track: make_body_label("Track"),
|
||||||
|
@ -252,16 +300,24 @@ impl MusicPlayerWidget {
|
||||||
next: Offset::new(next, Vec2::ZERO),
|
next: Offset::new(next, Vec2::ZERO),
|
||||||
position: make_footer_label("--:--"),
|
position: make_footer_label("--:--"),
|
||||||
duration: make_footer_label("--:--"),
|
duration: make_footer_label("--:--"),
|
||||||
|
slider,
|
||||||
style,
|
style,
|
||||||
art_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
art_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
||||||
body_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
body_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
||||||
footer_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
footer_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
||||||
position_secs: 0.0,
|
position_secs: 0.0,
|
||||||
|
duration_secs: 0.0,
|
||||||
position_dirty: false,
|
position_dirty: false,
|
||||||
|
position_updating: false,
|
||||||
status: PlaybackStatus::Paused,
|
status: PlaybackStatus::Paused,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn send_message(&self, msg: &OutMsg) {
|
||||||
|
let msg = serde_json::to_vec(msg).unwrap();
|
||||||
|
self.panel.send_message(&msg);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn format_time(secs: f32) -> String {
|
pub fn format_time(secs: f32) -> String {
|
||||||
let duration = secs.floor() as usize;
|
let duration = secs.floor() as usize;
|
||||||
let seconds = duration % 60;
|
let seconds = duration % 60;
|
||||||
|
@ -296,8 +352,17 @@ impl MusicPlayerWidget {
|
||||||
let play_x = style.button_spacing * 1.5;
|
let play_x = style.button_spacing * 1.5;
|
||||||
let next_x = style.button_spacing * 2.5;
|
let next_x = style.button_spacing * 2.5;
|
||||||
let position_x = style.button_spacing * 3.5;
|
let position_x = style.button_spacing * 3.5;
|
||||||
|
let slider_left = style.button_spacing * 4.25;
|
||||||
|
let slider_right = width - style.button_spacing * 1.5;
|
||||||
|
let slider_top = button_y - style.slider_height / 2.0;
|
||||||
|
let slider_bottom = button_y + style.slider_height / 2.0;
|
||||||
let duration_x = width - style.button_spacing * 0.75;
|
let duration_x = width - style.button_spacing * 0.75;
|
||||||
|
|
||||||
|
let slider_rect = Rect {
|
||||||
|
tl: Vec2::new(slider_left, slider_top),
|
||||||
|
br: Vec2::new(slider_right, slider_bottom),
|
||||||
|
};
|
||||||
|
|
||||||
self.artist.set_offset(Vec2::new(label_x, artist_baseline));
|
self.artist.set_offset(Vec2::new(label_x, artist_baseline));
|
||||||
self.album.set_offset(Vec2::new(label_x, album_baseline));
|
self.album.set_offset(Vec2::new(label_x, album_baseline));
|
||||||
self.track.set_offset(Vec2::new(label_x, track_baseline));
|
self.track.set_offset(Vec2::new(label_x, track_baseline));
|
||||||
|
@ -310,6 +375,8 @@ impl MusicPlayerWidget {
|
||||||
self.previous.set_offset(Vec2::new(previous_x, button_y));
|
self.previous.set_offset(Vec2::new(previous_x, button_y));
|
||||||
self.play.set_offset(Vec2::new(play_x, button_y));
|
self.play.set_offset(Vec2::new(play_x, button_y));
|
||||||
self.next.set_offset(Vec2::new(next_x, button_y));
|
self.next.set_offset(Vec2::new(next_x, button_y));
|
||||||
|
|
||||||
|
self.slider.set_rect(slider_rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_album(&mut self, info: AlbumInfo) {
|
pub fn update_album(&mut self, info: AlbumInfo) {
|
||||||
|
@ -335,17 +402,18 @@ impl MusicPlayerWidget {
|
||||||
.map(|s| s.as_str())
|
.map(|s| s.as_str())
|
||||||
.unwrap_or("<album here>"),
|
.unwrap_or("<album here>"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if let Some(length) = info.length {
|
||||||
|
self.duration.set_text(&Self::format_time(length));
|
||||||
|
self.duration_secs = length;
|
||||||
|
} else {
|
||||||
|
self.duration.set_text("--:--");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_progress(&mut self, progress: ProgressChanged) {
|
pub fn update_progress(&mut self, progress: ProgressChanged) {
|
||||||
self.position_secs = progress.position;
|
self.position_secs = progress.position;
|
||||||
self.position_dirty = true;
|
self.position_dirty = true;
|
||||||
|
|
||||||
if let Some(length) = progress.length {
|
|
||||||
self.duration.set_text(&Self::format_time(length));
|
|
||||||
} else {
|
|
||||||
self.duration.set_text("--:--");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_playback_status(&mut self, status: PlaybackStatus) {
|
pub fn update_playback_status(&mut self, status: PlaybackStatus) {
|
||||||
|
|
|
@ -0,0 +1,173 @@
|
||||||
|
// Copyright (c) 2022 Marceline Crmaer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use canary_script::Color;
|
||||||
|
|
||||||
|
/// A reusable set of colors. Used by default widget styles.
|
||||||
|
pub struct Palette {
|
||||||
|
pub base: Color,
|
||||||
|
pub base_hover: Color,
|
||||||
|
pub base_active: Color,
|
||||||
|
pub surface: Color,
|
||||||
|
pub overlay: Color,
|
||||||
|
pub text: Color,
|
||||||
|
pub black: Color,
|
||||||
|
pub red: Color,
|
||||||
|
pub green: Color,
|
||||||
|
pub yellow: Color,
|
||||||
|
pub blue: Color,
|
||||||
|
pub magenta: Color,
|
||||||
|
pub cyan: Color,
|
||||||
|
pub white: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Palette {
|
||||||
|
pub fn make_label_pairs(&self) -> Vec<(&'static str, Color)> {
|
||||||
|
vec![
|
||||||
|
("Base", self.base),
|
||||||
|
("Base Hover", self.base_hover),
|
||||||
|
("Base Active", self.base_active),
|
||||||
|
("Surface", self.surface),
|
||||||
|
("Overlay", self.overlay),
|
||||||
|
("Text", self.text),
|
||||||
|
("Black", self.black),
|
||||||
|
("Red", self.red),
|
||||||
|
("Green", self.green),
|
||||||
|
("Yellow", self.yellow),
|
||||||
|
("Blue", self.blue),
|
||||||
|
("Magenta", self.magenta),
|
||||||
|
("Cyan", self.cyan),
|
||||||
|
("White", self.white),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The common base color alpha shared between all themes.
|
||||||
|
pub const BASE_ALPHA: u8 = 230;
|
||||||
|
|
||||||
|
/// The common base_hover color alpha shared between all themes.
|
||||||
|
pub const BASE_HOVER_ALPHA: u8 = 242;
|
||||||
|
|
||||||
|
/// Converts 0xrrggbb hex to an opaque [Color].
|
||||||
|
pub const fn hex(rgb: u32) -> Color {
|
||||||
|
Color((rgb << 8) | 0xff)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sword Art Online color palette.
|
||||||
|
pub const SAO_PALETTE: Palette = Palette {
|
||||||
|
base: Color::WHITE.with_alpha(BASE_ALPHA),
|
||||||
|
base_hover: Color::WHITE.with_alpha(BASE_HOVER_ALPHA),
|
||||||
|
base_active: Color::YELLOW,
|
||||||
|
surface: Color::WHITE,
|
||||||
|
overlay: Color::WHITE,
|
||||||
|
text: Color::BLACK,
|
||||||
|
black: Color::BLACK,
|
||||||
|
red: Color::RED,
|
||||||
|
green: Color::GREEN,
|
||||||
|
yellow: Color::YELLOW,
|
||||||
|
blue: Color::BLUE,
|
||||||
|
magenta: Color::MAGENTA,
|
||||||
|
cyan: Color::CYAN,
|
||||||
|
white: Color::WHITE,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Rose Pine color palette.
|
||||||
|
pub const ROSE_PINE_PALETTE: Palette = Palette {
|
||||||
|
base: hex(0x191724).with_alpha(BASE_ALPHA),
|
||||||
|
base_hover: hex(0x21202ee0).with_alpha(BASE_HOVER_ALPHA), // Highlight Low
|
||||||
|
base_active: hex(0x403d52), // Highlight Med
|
||||||
|
surface: hex(0x1f1d2e),
|
||||||
|
overlay: hex(0x26233a),
|
||||||
|
text: hex(0xe0def4),
|
||||||
|
black: hex(0x6e6a86), // Muted
|
||||||
|
red: hex(0xeb6f92), // Love
|
||||||
|
green: hex(0x7fb59f), // ??? (not in Rose Pine?)
|
||||||
|
yellow: hex(0xf6c177), // Gold
|
||||||
|
blue: hex(0x31748f), // Pine
|
||||||
|
magenta: hex(0xc4a7e7), // Iris
|
||||||
|
cyan: hex(0x9ccfd8), // Foam
|
||||||
|
white: hex(0xe0def4), // Text
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Rose Pine Moon color palette.
|
||||||
|
pub const ROSE_PINE_MOON_PALETTE: Palette = Palette {
|
||||||
|
base: hex(0x232136).with_alpha(BASE_ALPHA),
|
||||||
|
base_hover: hex(0x2a283e).with_alpha(BASE_HOVER_ALPHA), // Highlight Low
|
||||||
|
base_active: hex(0x44415a), // Highlight Med
|
||||||
|
surface: hex(0x2a273f),
|
||||||
|
overlay: hex(0x393552),
|
||||||
|
text: hex(0xe0def4),
|
||||||
|
black: hex(0x6e6a86), // Muted
|
||||||
|
red: hex(0xeb6f92), // Love
|
||||||
|
green: hex(0x7fb59f), // ??? (not in Rose Pine?)
|
||||||
|
yellow: hex(0xf6c177), // Gold
|
||||||
|
blue: hex(0x3e8fb0), // Pine
|
||||||
|
magenta: hex(0xc4a7e7), // Iris
|
||||||
|
cyan: hex(0x9ccfd8), // Foam
|
||||||
|
white: hex(0xe0def4), // Text
|
||||||
|
};
|
||||||
|
|
||||||
|
/// [Arctica](https://github.com/sashakoshka/arctica) indexable color theme.
|
||||||
|
pub const ARCTICA: [Color; 24] = [
|
||||||
|
hex(0x242933),
|
||||||
|
hex(0x2e3440),
|
||||||
|
hex(0x3b4252),
|
||||||
|
hex(0x4c566a),
|
||||||
|
hex(0xeceff4),
|
||||||
|
hex(0xd8dee9),
|
||||||
|
hex(0xc2c9d6),
|
||||||
|
hex(0xaeb7c6),
|
||||||
|
hex(0xa8555d),
|
||||||
|
hex(0xb77763),
|
||||||
|
hex(0xcdb179),
|
||||||
|
hex(0x8ba277),
|
||||||
|
hex(0x769b9b),
|
||||||
|
hex(0x72a1ae),
|
||||||
|
hex(0x5e81ac),
|
||||||
|
hex(0x92738c),
|
||||||
|
hex(0xbf616a),
|
||||||
|
hex(0xd08770),
|
||||||
|
hex(0xebcb8b),
|
||||||
|
hex(0xa3be8c),
|
||||||
|
hex(0x8fbcbb),
|
||||||
|
hex(0x88c0d0),
|
||||||
|
hex(0x81a1c1),
|
||||||
|
hex(0xb48ead),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// [Arctica](https://github.com/sashakoshka/arctica) color palette.
|
||||||
|
pub const ARCTICA_PALETTE: Palette = Palette {
|
||||||
|
base: ARCTICA[0].with_alpha(BASE_ALPHA),
|
||||||
|
base_hover: ARCTICA[1].with_alpha(BASE_HOVER_ALPHA),
|
||||||
|
base_active: ARCTICA[13],
|
||||||
|
surface: ARCTICA[2],
|
||||||
|
overlay: ARCTICA[3],
|
||||||
|
text: ARCTICA[5],
|
||||||
|
black: ARCTICA[3],
|
||||||
|
red: ARCTICA[8],
|
||||||
|
green: ARCTICA[11],
|
||||||
|
yellow: ARCTICA[10],
|
||||||
|
blue: ARCTICA[14],
|
||||||
|
magenta: ARCTICA[15],
|
||||||
|
cyan: ARCTICA[13],
|
||||||
|
white: ARCTICA[7],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Common measurements for widget shapes.
|
||||||
|
pub struct Metrics {
|
||||||
|
pub surface_rounding: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common default parameters for widget styles.
|
||||||
|
pub struct Theme {
|
||||||
|
pub palette: Palette,
|
||||||
|
pub metrics: Metrics,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The global theme.
|
||||||
|
pub const THEME: Theme = Theme {
|
||||||
|
palette: ARCTICA_PALETTE,
|
||||||
|
metrics: Metrics {
|
||||||
|
surface_rounding: 5.0,
|
||||||
|
},
|
||||||
|
};
|
|
@ -118,6 +118,8 @@ pub struct RectButtonStyle {
|
||||||
pub inactive_color: Color,
|
pub inactive_color: Color,
|
||||||
pub hover_color: Color,
|
pub hover_color: Color,
|
||||||
pub selected_color: Color,
|
pub selected_color: Color,
|
||||||
|
pub icon_color: Color,
|
||||||
|
pub label_color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RectButtonStyle {
|
impl Default for RectButtonStyle {
|
||||||
|
@ -129,9 +131,11 @@ impl Default for RectButtonStyle {
|
||||||
label_baseline: 0.25,
|
label_baseline: 0.25,
|
||||||
icon_scale_factor: 0.8,
|
icon_scale_factor: 0.8,
|
||||||
icon_margin_factor: 1.1,
|
icon_margin_factor: 1.1,
|
||||||
inactive_color: Color::WHITE.with_alpha(0x40),
|
inactive_color: THEME.palette.base,
|
||||||
hover_color: Color::WHITE.with_alpha(0xb0),
|
hover_color: THEME.palette.base_hover,
|
||||||
selected_color: Color::YELLOW,
|
selected_color: THEME.palette.base_active,
|
||||||
|
icon_color: THEME.palette.black,
|
||||||
|
label_color: THEME.palette.text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,7 +172,7 @@ impl RectButton {
|
||||||
label_left += margin;
|
label_left += margin;
|
||||||
alignment = HorizontalAlignment::Left;
|
alignment = HorizontalAlignment::Left;
|
||||||
let scale = rect.height() * style.icon_scale_factor;
|
let scale = rect.height() * style.icon_scale_factor;
|
||||||
let color = Color::BLACK;
|
let color = style.icon_color;
|
||||||
let cx = rect.tl.x + margin / 2.0;
|
let cx = rect.tl.x + margin / 2.0;
|
||||||
let cy = rect.tl.y + rect.height() / 2.0;
|
let cy = rect.tl.y + rect.height() / 2.0;
|
||||||
let center = Vec2::new(cx, cy);
|
let center = Vec2::new(cx, cy);
|
||||||
|
@ -182,7 +186,7 @@ impl RectButton {
|
||||||
let right = rect.br.x;
|
let right = rect.br.x;
|
||||||
let baseline = rect.tl.y;
|
let baseline = rect.tl.y;
|
||||||
let baseline = (rect.height() * (1.0 - style.label_baseline)) + baseline;
|
let baseline = (rect.height() * (1.0 - style.label_baseline)) + baseline;
|
||||||
let color = Color::BLACK;
|
let color = style.label_color;
|
||||||
|
|
||||||
Label::new(text, alignment, scale, color, left, right, baseline)
|
Label::new(text, alignment, scale, color, left, right, baseline)
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,8 +23,8 @@ impl DialogResponse {
|
||||||
|
|
||||||
pub fn get_color(&self) -> Color {
|
pub fn get_color(&self) -> Color {
|
||||||
match self {
|
match self {
|
||||||
DialogResponse::Yes => Color::BLUE,
|
DialogResponse::Yes => THEME.palette.blue,
|
||||||
DialogResponse::No => Color::RED,
|
DialogResponse::No => THEME.palette.red,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ pub struct DialogStyle {
|
||||||
impl Default for DialogStyle {
|
impl Default for DialogStyle {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
rounding: 5.0,
|
rounding: THEME.metrics.surface_rounding,
|
||||||
header: Default::default(),
|
header: Default::default(),
|
||||||
body: Default::default(),
|
body: Default::default(),
|
||||||
footer: Default::default(),
|
footer: Default::default(),
|
||||||
|
@ -61,10 +61,10 @@ pub struct DialogHeaderStyle {
|
||||||
impl Default for DialogHeaderStyle {
|
impl Default for DialogHeaderStyle {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
color: Color::WHITE,
|
color: THEME.palette.surface,
|
||||||
height: 20.0,
|
height: 20.0,
|
||||||
text_font: Font::new(crate::DISPLAY_FONT),
|
text_font: Font::new(crate::DISPLAY_FONT),
|
||||||
text_color: Color::BLACK,
|
text_color: THEME.palette.text,
|
||||||
text_scale_factor: 0.65,
|
text_scale_factor: 0.65,
|
||||||
text_baseline: 0.25,
|
text_baseline: 0.25,
|
||||||
}
|
}
|
||||||
|
@ -82,9 +82,9 @@ pub struct DialogBodyStyle {
|
||||||
impl Default for DialogBodyStyle {
|
impl Default for DialogBodyStyle {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
color: Color::WHITE.with_alpha(0xb0),
|
color: THEME.palette.base,
|
||||||
text_font: Font::new(crate::CONTENT_FONT),
|
text_font: Font::new(crate::CONTENT_FONT),
|
||||||
text_color: Color::BLACK,
|
text_color: THEME.palette.text,
|
||||||
text_size: 5.0,
|
text_size: 5.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,6 +95,7 @@ pub struct DialogFooterStyle {
|
||||||
pub icon_font: Font,
|
pub icon_font: Font,
|
||||||
pub button_radius: f32,
|
pub button_radius: f32,
|
||||||
pub color: Color,
|
pub color: Color,
|
||||||
|
pub button_fg: Color,
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +104,8 @@ impl Default for DialogFooterStyle {
|
||||||
Self {
|
Self {
|
||||||
icon_font: Font::new(crate::ICON_FONT),
|
icon_font: Font::new(crate::ICON_FONT),
|
||||||
button_radius: 7.5,
|
button_radius: 7.5,
|
||||||
color: Color::WHITE,
|
color: THEME.palette.surface,
|
||||||
|
button_fg: THEME.palette.white,
|
||||||
height: 15.0,
|
height: 15.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,7 +138,7 @@ impl Dialog {
|
||||||
thickness: radius * 0.05,
|
thickness: radius * 0.05,
|
||||||
body_color: color,
|
body_color: color,
|
||||||
ring_color: color,
|
ring_color: color,
|
||||||
icon_color: Color::WHITE,
|
icon_color: style.footer.button_fg,
|
||||||
};
|
};
|
||||||
|
|
||||||
let text = LabelText {
|
let text = LabelText {
|
||||||
|
|
|
@ -224,6 +224,7 @@ pub struct TabMenu {
|
||||||
impl TabMenu {
|
impl TabMenu {
|
||||||
const HEAD_RADIUS: f32 = 5.0;
|
const HEAD_RADIUS: f32 = 5.0;
|
||||||
const HEAD_HEIGHT: f32 = 15.0;
|
const HEAD_HEIGHT: f32 = 15.0;
|
||||||
|
const HEAD_COLOR: Color = THEME.palette.surface;
|
||||||
const TAB_WIDTH: f32 = 15.0;
|
const TAB_WIDTH: f32 = 15.0;
|
||||||
const TAB_HEIGHT: f32 = 25.0;
|
const TAB_HEIGHT: f32 = 25.0;
|
||||||
const TAB_NUM: usize = 6;
|
const TAB_NUM: usize = 6;
|
||||||
|
@ -235,9 +236,9 @@ impl TabMenu {
|
||||||
radius: Self::HEAD_HEIGHT * 0.25,
|
radius: Self::HEAD_HEIGHT * 0.25,
|
||||||
spacing: Self::HEAD_HEIGHT * 0.1,
|
spacing: Self::HEAD_HEIGHT * 0.1,
|
||||||
thickness: Self::HEAD_HEIGHT * 0.05,
|
thickness: Self::HEAD_HEIGHT * 0.05,
|
||||||
body_color: Color::WHITE,
|
body_color: Self::HEAD_COLOR,
|
||||||
ring_color: Color::BLACK,
|
ring_color: THEME.palette.black,
|
||||||
icon_color: Color::BLACK,
|
icon_color: THEME.palette.black,
|
||||||
};
|
};
|
||||||
|
|
||||||
const HEAD_BUTTON_MARGIN: f32 = Self::HEAD_HEIGHT / 2.0;
|
const HEAD_BUTTON_MARGIN: f32 = Self::HEAD_HEIGHT / 2.0;
|
||||||
|
@ -348,20 +349,18 @@ impl Container for TabMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&mut self, ctx: &DrawContext) {
|
fn draw(&mut self, ctx: &DrawContext) {
|
||||||
let head_color = Color::WHITE;
|
|
||||||
|
|
||||||
ctx.draw_partially_rounded_rect(
|
ctx.draw_partially_rounded_rect(
|
||||||
CornerFlags::BOTTOM_RIGHT,
|
CornerFlags::BOTTOM_RIGHT,
|
||||||
self.separator_rect,
|
self.separator_rect,
|
||||||
Self::INNER_RADIUS,
|
Self::INNER_RADIUS,
|
||||||
head_color,
|
Self::HEAD_COLOR,
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.draw_partially_rounded_rect(
|
ctx.draw_partially_rounded_rect(
|
||||||
CornerFlags::TOP,
|
CornerFlags::TOP,
|
||||||
self.head_rect,
|
self.head_rect,
|
||||||
Self::HEAD_RADIUS,
|
Self::HEAD_RADIUS,
|
||||||
head_color,
|
Self::HEAD_COLOR,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,9 @@ pub mod button;
|
||||||
pub mod dialog;
|
pub mod dialog;
|
||||||
pub mod flex;
|
pub mod flex;
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
|
pub mod palette;
|
||||||
pub mod scroll;
|
pub mod scroll;
|
||||||
|
pub mod slider;
|
||||||
pub mod shell;
|
pub mod shell;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
|
||||||
|
@ -75,6 +77,7 @@ impl<T: Container> Widget for T {
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use super::*;
|
pub use super::*;
|
||||||
pub use crate::anim::Animation;
|
pub use crate::anim::Animation;
|
||||||
|
pub use crate::style::{self, THEME};
|
||||||
pub use canary_script::{*, api::*};
|
pub use canary_script::{*, api::*};
|
||||||
pub use keyframe::functions::*;
|
pub use keyframe::functions::*;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
// Copyright (c) 2022 Marceline Crmaer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use super::prelude::*;
|
||||||
|
use shell::Offset;
|
||||||
|
use text::{HorizontalAlignment, Label, LabelText};
|
||||||
|
|
||||||
|
pub struct PaletteStyle {
|
||||||
|
pub bg: Color,
|
||||||
|
pub text: Color,
|
||||||
|
pub rounding: f32,
|
||||||
|
pub text_size: f32,
|
||||||
|
pub line_spacing: f32,
|
||||||
|
pub color_radius: f32,
|
||||||
|
pub margin: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PaletteStyle {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
bg: THEME.palette.surface,
|
||||||
|
text: THEME.palette.text,
|
||||||
|
rounding: THEME.metrics.surface_rounding,
|
||||||
|
text_size: 5.0,
|
||||||
|
line_spacing: 8.0,
|
||||||
|
color_radius: 3.0,
|
||||||
|
margin: Rect::from_xy_size(Vec2::splat(10.0), Vec2::ZERO),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that displays all the colors in the global palette.
|
||||||
|
pub struct Palette {
|
||||||
|
body: Rect,
|
||||||
|
style: PaletteStyle,
|
||||||
|
labels: Vec<Offset<Label>>,
|
||||||
|
colors: Vec<(Vec2, Color)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Palette {
|
||||||
|
pub fn new(style: PaletteStyle) -> Self {
|
||||||
|
let width = 70.0;
|
||||||
|
let pairs = THEME.palette.make_label_pairs();
|
||||||
|
let label_font = Font::new(crate::CONTENT_FONT);
|
||||||
|
|
||||||
|
let mut label_cursor = Vec2::new(0.0, style.line_spacing) + style.margin.tl;
|
||||||
|
let mut color_cursor = Vec2::new(
|
||||||
|
width - style.margin.br.x,
|
||||||
|
style.line_spacing / 2.0 + style.margin.tl.y,
|
||||||
|
);
|
||||||
|
let mut labels = Vec::new();
|
||||||
|
let mut colors = Vec::new();
|
||||||
|
|
||||||
|
for (text, color) in pairs {
|
||||||
|
let text = LabelText {
|
||||||
|
font: label_font,
|
||||||
|
text: text.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let label = Label::new(
|
||||||
|
text,
|
||||||
|
HorizontalAlignment::Left,
|
||||||
|
style.text_size,
|
||||||
|
style.text,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
let label = Offset::new(label, label_cursor);
|
||||||
|
|
||||||
|
labels.push(label);
|
||||||
|
|
||||||
|
colors.push((color_cursor, color));
|
||||||
|
|
||||||
|
label_cursor.y += style.line_spacing;
|
||||||
|
color_cursor.y += style.line_spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
let height = label_cursor.y + style.margin.br.y;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
body: Rect::from_xy_size(Vec2::ZERO, Vec2::new(width, height)),
|
||||||
|
style,
|
||||||
|
labels,
|
||||||
|
colors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RectBounds for Palette {
|
||||||
|
fn get_bounds(&self) -> Rect {
|
||||||
|
self.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Palette {
|
||||||
|
fn draw(&mut self, ctx: &DrawContext) {
|
||||||
|
ctx.draw_rounded_rect(self.body, self.style.rounding, self.style.bg);
|
||||||
|
|
||||||
|
for label in self.labels.iter_mut() {
|
||||||
|
label.draw(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (center, color) in self.colors.iter() {
|
||||||
|
ctx.draw_circle(*center, self.style.color_radius, *color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,11 +21,11 @@ impl Default for ScrollBarStyle {
|
||||||
margin: Vec2::splat(2.0),
|
margin: Vec2::splat(2.0),
|
||||||
body_radius: 1.0,
|
body_radius: 1.0,
|
||||||
body_width: 3.0,
|
body_width: 3.0,
|
||||||
body_idle_color: Color(0x7f7f7fff),
|
body_idle_color: THEME.palette.base,
|
||||||
body_hover_color: Color(0xb0b0b0ff),
|
body_hover_color: THEME.palette.base_hover,
|
||||||
body_selected_color: Color::WHITE,
|
body_selected_color: THEME.palette.base_active,
|
||||||
rail_width: 1.0,
|
rail_width: 1.0,
|
||||||
rail_color: Color(0xa0a0a07f),
|
rail_color: THEME.palette.base,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
// Copyright (c) 2022 Marceline Crmaer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use super::prelude::*;
|
||||||
|
|
||||||
|
pub struct SliderStyle {
|
||||||
|
pub bg_color: Color,
|
||||||
|
pub bg_padding: f32,
|
||||||
|
pub bg_rounding: f32,
|
||||||
|
pub fg_color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SliderStyle {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
bg_color: THEME.palette.overlay,
|
||||||
|
bg_padding: 2.5,
|
||||||
|
bg_rounding: 2.5,
|
||||||
|
fg_color: THEME.palette.blue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Slider {
|
||||||
|
style: SliderStyle,
|
||||||
|
bg_rect: Rect,
|
||||||
|
fg_rect: Rect,
|
||||||
|
position: f32,
|
||||||
|
dirty: bool,
|
||||||
|
updating: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Slider {
|
||||||
|
pub fn new(style: SliderStyle, rect: Rect) -> Self {
|
||||||
|
Self {
|
||||||
|
style,
|
||||||
|
bg_rect: rect,
|
||||||
|
fg_rect: rect,
|
||||||
|
position: 0.5,
|
||||||
|
dirty: true,
|
||||||
|
updating: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_position(&mut self, position: f32) {
|
||||||
|
if !self.updating {
|
||||||
|
self.position = position;
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_update(&mut self) -> Option<f32> {
|
||||||
|
if self.updating {
|
||||||
|
Some(self.position)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_rect(&mut self, rect: Rect) {
|
||||||
|
self.bg_rect = rect;
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn undirty(&mut self) {
|
||||||
|
if !self.dirty {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut fg_space = self.bg_rect.inset(self.style.bg_padding);
|
||||||
|
fg_space.br.x = (fg_space.width() * self.position) + fg_space.tl.x;
|
||||||
|
self.fg_rect = fg_space;
|
||||||
|
self.dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_position(&self) -> f32 {
|
||||||
|
self.position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Slider {
|
||||||
|
fn draw(&mut self, ctx: &DrawContext) {
|
||||||
|
self.undirty();
|
||||||
|
ctx.draw_rounded_rect(self.bg_rect, self.style.bg_rounding, self.style.bg_color);
|
||||||
|
ctx.draw_rect(self.fg_rect, self.style.fg_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
|
||||||
|
if let CursorEventKind::Select = kind {
|
||||||
|
if self.bg_rect.contains_point(at) {
|
||||||
|
self.updating = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.updating {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match kind {
|
||||||
|
CursorEventKind::Hover => {}
|
||||||
|
CursorEventKind::Select | CursorEventKind::Drag => {
|
||||||
|
let offset = at.x - self.fg_rect.tl.x;
|
||||||
|
let range = self.bg_rect.inset(self.style.bg_padding).width();
|
||||||
|
self.position = (offset / range).clamp(0.0, 1.0);
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
CursorEventKind::Deselect => {
|
||||||
|
self.updating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,8 @@
|
||||||
//! implemented, but in the future, [wasm3](https://github.com/wasm3/wasm3)
|
//! implemented, but in the future, [wasm3](https://github.com/wasm3/wasm3)
|
||||||
//! will also be provided.
|
//! will also be provided.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub mod wasmtime;
|
pub mod wasmtime;
|
||||||
|
@ -22,7 +24,7 @@ pub fn make_default_backend() -> anyhow::Result<Box<dyn Backend>> {
|
||||||
|
|
||||||
/// A WebAssembly runtime backend.
|
/// A WebAssembly runtime backend.
|
||||||
pub trait Backend {
|
pub trait Backend {
|
||||||
fn load_module(&self, abi: ScriptAbi, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>>;
|
fn load_module(&self, abi: Arc<ScriptAbi>, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An instance of a WebAssembly module.
|
/// An instance of a WebAssembly module.
|
||||||
|
@ -69,6 +71,7 @@ pub struct ScriptAbi {
|
||||||
loaded_fonts: RwLock<Vec<Arc<text::Font>>>,
|
loaded_fonts: RwLock<Vec<Arc<text::Font>>>,
|
||||||
text_layouts: RwLock<Slab<text::TextLayout>>,
|
text_layouts: RwLock<Slab<text::TextLayout>>,
|
||||||
message_store: RwLock<Slab<Vec<u8>>>,
|
message_store: RwLock<Slab<Vec<u8>>>,
|
||||||
|
panels: RwLock<Slab<PanelAbi>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScriptAbi {
|
impl ScriptAbi {
|
||||||
|
@ -79,6 +82,13 @@ impl ScriptAbi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Allocates a new ID and host-side storage for a panel.
|
||||||
|
pub fn create_panel(&self) -> PanelId {
|
||||||
|
let abi = PanelAbi::default();
|
||||||
|
let id = self.panels.write().insert(abi);
|
||||||
|
PanelId(id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn start_draw(&self) {
|
pub fn start_draw(&self) {
|
||||||
let mut lock = self.draw_cmds.lock();
|
let mut lock = self.draw_cmds.lock();
|
||||||
lock.clear();
|
lock.clear();
|
||||||
|
@ -157,4 +167,23 @@ impl ScriptAbi {
|
||||||
let src = store.get(id as usize).unwrap();
|
let src = store.get(id as usize).unwrap();
|
||||||
dst.copy_from_slice(src);
|
dst.copy_from_slice(src);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn panel_send_message(&self, id: u32, message: Vec<u8>) {
|
||||||
|
if let Some(panel) = self.panels.read().get(id as usize) {
|
||||||
|
panel.outgoing_messages.write().push_back(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recv_panel_messages(&self, id: PanelId) -> Vec<Vec<u8>> {
|
||||||
|
if let Some(panel) = self.panels.read().get(id.0) {
|
||||||
|
panel.outgoing_messages.write().drain(..).collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PanelAbi {
|
||||||
|
outgoing_messages: RwLock<VecDeque<Vec<u8>>>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// Copyright (c) 2022 Marceline Cramer
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::collections::{hash_map::DefaultHasher, HashMap};
|
||||||
|
use std::hash::Hasher;
|
||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
|
|
||||||
use super::{Arc, Backend, Instance, PanelId, ScriptAbi};
|
use super::{Arc, Backend, Instance, PanelId, ScriptAbi};
|
||||||
|
@ -8,34 +10,57 @@ use crate::DrawCommand;
|
||||||
|
|
||||||
use canary_script::{Color, CursorEventKind, Rect, Vec2};
|
use canary_script::{Color, CursorEventKind, Rect, Vec2};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use prehash::{DefaultPrehasher, Prehashed, Prehasher};
|
||||||
|
|
||||||
type Caller<'a> = wasmtime::Caller<'a, ScriptAbi>;
|
type Caller<'a> = wasmtime::Caller<'a, Arc<ScriptAbi>>;
|
||||||
type Store = wasmtime::Store<ScriptAbi>;
|
type Store = wasmtime::Store<Arc<ScriptAbi>>;
|
||||||
type Linker = wasmtime::Linker<ScriptAbi>;
|
type Linker = wasmtime::Linker<Arc<ScriptAbi>>;
|
||||||
|
type ModuleCache = Mutex<HashMap<Prehashed<u64>, wasmtime::Module, DefaultPrehasher>>;
|
||||||
|
|
||||||
pub struct WasmtimeBackend {
|
pub struct WasmtimeBackend {
|
||||||
engine: wasmtime::Engine,
|
engine: wasmtime::Engine,
|
||||||
|
module_cache: ModuleCache,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WasmtimeBackend {
|
impl WasmtimeBackend {
|
||||||
pub fn new() -> anyhow::Result<Self> {
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
let mut config = wasmtime::Config::new();
|
let mut config = wasmtime::Config::new();
|
||||||
config.wasm_simd(true);
|
config.wasm_simd(true);
|
||||||
config.wasm_bulk_memory(true);
|
config.wasm_bulk_memory(true);
|
||||||
config.cranelift_opt_level(wasmtime::OptLevel::Speed);
|
config.cranelift_opt_level(wasmtime::OptLevel::Speed);
|
||||||
|
config.cache_config_load_default()?;
|
||||||
|
|
||||||
let engine = wasmtime::Engine::new(&config)?;
|
let engine = wasmtime::Engine::new(&config)?;
|
||||||
|
let module_cache = Default::default();
|
||||||
Ok(Self { engine })
|
Ok(Self {
|
||||||
|
engine,
|
||||||
|
module_cache,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend for WasmtimeBackend {
|
impl Backend for WasmtimeBackend {
|
||||||
fn load_module(&self, abi: ScriptAbi, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>> {
|
fn load_module(&self, abi: Arc<ScriptAbi>, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>> {
|
||||||
let module = wasmtime::Module::new(&self.engine, module)?;
|
let mut hasher = DefaultHasher::new();
|
||||||
|
hasher.write(module);
|
||||||
|
let hashed = hasher.finish();
|
||||||
|
|
||||||
|
let prehasher = DefaultPrehasher::new();
|
||||||
|
let prehashed = prehasher.prehash(hashed);
|
||||||
|
let mut cache = self.module_cache.lock();
|
||||||
|
|
||||||
|
let module = if let Some(module) = cache.get(&prehashed) {
|
||||||
|
module
|
||||||
|
} else {
|
||||||
|
let module = wasmtime::Module::new(&self.engine, module)?;
|
||||||
|
cache.insert(prehashed, module);
|
||||||
|
cache.get(&prehashed).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
let mut store = wasmtime::Store::new(&self.engine, abi);
|
let mut store = wasmtime::Store::new(&self.engine, abi);
|
||||||
let mut linker = Linker::new(&self.engine);
|
let mut linker = Linker::new(&self.engine);
|
||||||
WasmtimeInstance::link(&mut linker)?;
|
WasmtimeInstance::link(&mut linker)?;
|
||||||
let instance = linker.instantiate(&mut store, &module)?;
|
let instance = linker.instantiate(&mut store, module)?;
|
||||||
let bind_panel = instance.get_typed_func(&mut store, "bind_panel")?;
|
let bind_panel = instance.get_typed_func(&mut store, "bind_panel")?;
|
||||||
let update = instance.get_typed_func(&mut store, "update")?;
|
let update = instance.get_typed_func(&mut store, "update")?;
|
||||||
let draw = instance.get_typed_func(&mut store, "draw")?;
|
let draw = instance.get_typed_func(&mut store, "draw")?;
|
||||||
|
@ -147,6 +172,15 @@ impl WasmtimeInstance {
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
linker.func_wrap(
|
||||||
|
module,
|
||||||
|
"panel_send_message",
|
||||||
|
|mut caller: Caller<'_>, id: u32, ptr: u32, len: u32| {
|
||||||
|
let message = Self::get_memory_slice_bytes(&mut caller, ptr as usize, len as usize);
|
||||||
|
caller.data().panel_send_message(id, message.to_vec())
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
src/lib.rs
16
src/lib.rs
|
@ -29,11 +29,12 @@ impl Runtime {
|
||||||
|
|
||||||
pub fn load_module(&self, module: &[u8]) -> anyhow::Result<Script> {
|
pub fn load_module(&self, module: &[u8]) -> anyhow::Result<Script> {
|
||||||
let abi = ScriptAbi::new(self.font_store.to_owned());
|
let abi = ScriptAbi::new(self.font_store.to_owned());
|
||||||
let instance = self.backend.load_module(abi, module)?;
|
let abi = Arc::new(abi);
|
||||||
|
let instance = self.backend.load_module(abi.to_owned(), module)?;
|
||||||
|
|
||||||
Ok(Script {
|
Ok(Script {
|
||||||
instance,
|
instance,
|
||||||
next_panel: 0,
|
abi,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,16 +42,16 @@ impl Runtime {
|
||||||
/// A loaded instance of a Canary script.
|
/// A loaded instance of a Canary script.
|
||||||
pub struct Script {
|
pub struct Script {
|
||||||
instance: Arc<dyn Instance>,
|
instance: Arc<dyn Instance>,
|
||||||
next_panel: usize,
|
abi: Arc<ScriptAbi>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Script {
|
impl Script {
|
||||||
pub fn create_panel(&mut self, protocol: &str, msg: Vec<u8>) -> anyhow::Result<Panel> {
|
pub fn create_panel(&mut self, protocol: &str, msg: Vec<u8>) -> anyhow::Result<Panel> {
|
||||||
let id = PanelId(self.next_panel);
|
let id = self.abi.create_panel();
|
||||||
self.next_panel += 1;
|
|
||||||
let userdata = self.instance.bind_panel(id, protocol, msg);
|
let userdata = self.instance.bind_panel(id, protocol, msg);
|
||||||
Ok(Panel {
|
Ok(Panel {
|
||||||
instance: self.instance.clone(),
|
instance: self.instance.clone(),
|
||||||
|
abi: self.abi.clone(),
|
||||||
id,
|
id,
|
||||||
userdata,
|
userdata,
|
||||||
})
|
})
|
||||||
|
@ -60,6 +61,7 @@ impl Script {
|
||||||
/// A Canary panel.
|
/// A Canary panel.
|
||||||
pub struct Panel {
|
pub struct Panel {
|
||||||
instance: Arc<dyn Instance>,
|
instance: Arc<dyn Instance>,
|
||||||
|
abi: Arc<ScriptAbi>,
|
||||||
id: PanelId,
|
id: PanelId,
|
||||||
userdata: u32,
|
userdata: u32,
|
||||||
}
|
}
|
||||||
|
@ -84,6 +86,10 @@ impl Panel {
|
||||||
pub fn on_message(&self, msg: Vec<u8>) {
|
pub fn on_message(&self, msg: Vec<u8>) {
|
||||||
self.instance.on_message(self.userdata, msg);
|
self.instance.on_message(self.userdata, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn recv_messages(&self) -> Vec<Vec<u8>> {
|
||||||
|
self.abi.recv_panel_messages(self.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Proportion constant between pixels (at 96dpi) to millimeters (Canary's unit measurement).
|
/// Proportion constant between pixels (at 96dpi) to millimeters (Canary's unit measurement).
|
||||||
|
|
Loading…
Reference in New Issue