wasmtime module caching #46
|
@ -9,8 +9,10 @@ path = "src/main.rs"
|
|||
required-features = ["service"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1", optional = true }
|
||||
byteorder = "1.4"
|
||||
canary = { path = "../..", optional = true }
|
||||
futures-util = { version = "0.3", optional = true, features = ["io"] }
|
||||
glium = { version = "0.32", optional = true}
|
||||
mio = { version = "0.8", features = ["net", "os-poll"], optional = true }
|
||||
mio-signals = { version = "0.2", optional = true }
|
||||
|
@ -20,4 +22,5 @@ serde_json = "1"
|
|||
slab = { version = "0.4", optional = true}
|
||||
|
||||
[features]
|
||||
service = ["dep:canary", "dep:glium", "dep:mio", "dep:mio-signals", "dep:parking_lot", "dep:slab"]
|
||||
async = ["dep:futures-util"]
|
||||
service = ["dep:anyhow", "dep:canary", "dep:glium", "dep:mio", "dep:mio-signals", "dep:parking_lot", "dep:slab"]
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::protocol::{ClientMessenger, MagpieServerMsg, PanelId, SendMessage, MAGPIE_SOCK};
|
||||
|
||||
/// A client to a Magpie server.
|
||||
pub struct MagpieClient {
|
||||
pub messenger: ClientMessenger<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,4 +1,6 @@
|
|||
pub mod client;
|
||||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub mod protocol;
|
||||
|
||||
#[cfg(feature = "service")]
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use glium::glutin::event_loop::EventLoopBuilder;
|
||||
|
||||
use canary_magpie::service::*;
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::io::{Read, Write};
|
||||
use std::marker::PhantomData;
|
||||
|
@ -21,6 +24,7 @@ pub type PanelId = u32;
|
|||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct CreatePanel {
|
||||
pub id: PanelId,
|
||||
pub protocol: String,
|
||||
pub script: PathBuf,
|
||||
}
|
||||
|
||||
|
@ -39,71 +43,58 @@ pub enum MagpieServerMsg {
|
|||
SendMessage(SendMessage),
|
||||
}
|
||||
|
||||
/// A message sent from a script's panel to a client.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct RecvMessage {
|
||||
pub id: PanelId,
|
||||
pub msg: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A message sent from the Magpie server to a client.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum MagpieClientMsg {}
|
||||
pub enum MagpieClientMsg {
|
||||
RecvMessage(RecvMessage),
|
||||
}
|
||||
|
||||
/// A [Messenger] specialized for Magpie clients.
|
||||
pub type ClientMessenger<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.
|
||||
pub type ServerMessenger<T> = Messenger<T, MagpieServerMsg, MagpieClientMsg>;
|
||||
|
||||
/// Bidirectional, transport-agnostic Magpie IO wrapper struct.
|
||||
pub struct Messenger<T, I, O> {
|
||||
pub transport: T,
|
||||
/// Piecewise packet assembler for [Messenger].
|
||||
pub struct MessageQueue<I> {
|
||||
expected_len: Option<usize>,
|
||||
received_buf: VecDeque<u8>,
|
||||
received_queue: VecDeque<I>,
|
||||
closed: bool,
|
||||
_output: PhantomData<O>,
|
||||
}
|
||||
|
||||
impl<T: Read + Write, I: DeserializeOwned, O: Serialize> Messenger<T, I, O> {
|
||||
pub fn new(transport: T) -> Self {
|
||||
impl<I> Default for MessageQueue<I> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
transport,
|
||||
expected_len: None,
|
||||
received_buf: Default::default(),
|
||||
received_queue: Default::default(),
|
||||
closed: false,
|
||||
_output: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_closed(&self) -> bool {
|
||||
self.closed
|
||||
}
|
||||
|
||||
pub fn send(&mut self, msg: &O) -> std::io::Result<()> {
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
let payload = serde_json::to_vec(msg).unwrap();
|
||||
let len = payload.len() as u32;
|
||||
self.transport.write_u32::<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),
|
||||
}
|
||||
}
|
||||
impl<I: DeserializeOwned> MessageQueue<I> {
|
||||
pub fn on_data(&mut self, data: &[u8]) -> std::io::Result<()> {
|
||||
self.received_buf.write_all(data)?;
|
||||
|
||||
loop {
|
||||
if let Some(expected_len) = self.expected_len {
|
||||
|
@ -135,8 +126,137 @@ impl<T: Read + Write, I: DeserializeOwned, O: Serialize> Messenger<T, I, O> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to receive a single input packet.
|
||||
pub fn recv(&mut self) -> Option<I> {
|
||||
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])?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use canary::{DrawCommand, Vec2, PX_PER_MM};
|
||||
use glium::Surface;
|
||||
use glium::{program::ProgramCreationInput, Surface};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Vertex {
|
||||
|
@ -56,9 +59,21 @@ pub struct Graphics {
|
|||
|
||||
impl Graphics {
|
||||
pub fn new(display: glium::Display) -> Self {
|
||||
let program =
|
||||
glium::Program::from_source(&display, VERTEX_SHADER_SRC, FRAGMENT_SHADER_SRC, None)
|
||||
.unwrap();
|
||||
let program = glium::Program::new(
|
||||
&display,
|
||||
ProgramCreationInput::SourceCode {
|
||||
vertex_shader: VERTEX_SHADER_SRC,
|
||||
tessellation_control_shader: None,
|
||||
tessellation_evaluation_shader: None,
|
||||
geometry_shader: None,
|
||||
fragment_shader: FRAGMENT_SHADER_SRC,
|
||||
transform_feedback_varyings: None,
|
||||
outputs_srgb: true, // don't automatically apply gamma correction
|
||||
uses_point_size: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Self { display, program }
|
||||
}
|
||||
|
||||
|
@ -102,7 +117,7 @@ impl Graphics {
|
|||
};
|
||||
|
||||
let mut target = self.display.draw();
|
||||
target.clear_color(0.0, 0.0, 0.0, 1.0);
|
||||
target.clear_color(0.0, 0.0, 0.0, 0.0);
|
||||
target
|
||||
.draw(
|
||||
&vertex_buffer,
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
@ -11,18 +14,29 @@ use mio_signals::{Signal, Signals};
|
|||
use parking_lot::RwLock;
|
||||
use slab::Slab;
|
||||
|
||||
use crate::protocol::*;
|
||||
use crate::service::window::{WindowMessage, WindowMessageSender};
|
||||
use crate::protocol::{CreatePanel, MagpieServerMsg, SendMessage, ServerMessenger};
|
||||
|
||||
const SOCK_NAME: &str = "magpie.sock";
|
||||
|
||||
pub enum IpcMessage {}
|
||||
#[derive(Debug)]
|
||||
pub enum IpcMessage {
|
||||
PanelMessage { window: usize, message: Vec<u8> },
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IpcMessageSender {
|
||||
waker: Waker,
|
||||
waker: Arc<Waker>,
|
||||
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.
|
||||
pub struct Listener {
|
||||
pub uds: UnixListener,
|
||||
|
@ -54,6 +68,7 @@ impl DerefMut for Listener {
|
|||
|
||||
pub struct IpcData {
|
||||
poll: Poll,
|
||||
window_to_client_panel: HashMap<usize, (usize, PanelId)>,
|
||||
next_window_id: usize,
|
||||
}
|
||||
|
||||
|
@ -73,38 +88,36 @@ pub struct Client {
|
|||
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 {
|
||||
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);
|
||||
match msg {
|
||||
MagpieServerMsg::CreatePanel(CreatePanel { id, script }) => {
|
||||
let window = self.data.write().new_window_id();
|
||||
MagpieServerMsg::CreatePanel(CreatePanel {
|
||||
id,
|
||||
protocol,
|
||||
script,
|
||||
}) => {
|
||||
let mut data = self.data.write();
|
||||
|
||||
let window = data.new_window_id();
|
||||
data.window_to_client_panel
|
||||
.insert(window, (self.token.0, id));
|
||||
|
||||
if let Some(old_id) = self.id_to_window.insert(id, window) {
|
||||
let msg = WindowMessage::CloseWindow { id: old_id };
|
||||
let _ = self.window_sender.send_event(msg);
|
||||
}
|
||||
|
||||
let msg = WindowMessage::OpenWindow { id: window, script };
|
||||
let msg = WindowMessage::OpenWindow {
|
||||
id: window,
|
||||
protocol,
|
||||
script,
|
||||
};
|
||||
let _ = self.window_sender.send_event(msg);
|
||||
}
|
||||
MagpieServerMsg::SendMessage(SendMessage { id, msg }) => {
|
||||
|
@ -118,13 +131,26 @@ impl Client {
|
|||
|
||||
Ok(self.messenger.is_closed())
|
||||
}
|
||||
|
||||
pub fn disconnect(mut self) {
|
||||
println!("Client #{} disconnected", self.token.0);
|
||||
|
||||
let mut transport = self.messenger.into_transport();
|
||||
let mut data = self.data.write();
|
||||
let _ = data.poll.registry().deregister(&mut transport);
|
||||
|
||||
for (_id, window) in self.id_to_window.drain() {
|
||||
let msg = WindowMessage::CloseWindow { id: window };
|
||||
let _ = self.window_sender.send_event(msg);
|
||||
data.window_to_client_panel.remove(&window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Ipc {
|
||||
pub data: Arc<RwLock<IpcData>>,
|
||||
pub window_sender: WindowMessageSender,
|
||||
pub message_recv: Receiver<IpcMessage>,
|
||||
pub events: Events,
|
||||
pub quit: bool,
|
||||
pub listener: Listener,
|
||||
pub signals: Signals,
|
||||
|
@ -148,7 +174,6 @@ impl Ipc {
|
|||
|
||||
let mut signals = Signals::new(Signal::Interrupt | Signal::Quit)?;
|
||||
|
||||
let events = Events::with_capacity(128);
|
||||
let poll = Poll::new()?;
|
||||
let listener_token = Token(usize::MAX);
|
||||
let signals_token = Token(listener_token.0 - 1);
|
||||
|
@ -162,12 +187,13 @@ impl Ipc {
|
|||
let (sender, message_recv) = channel();
|
||||
|
||||
let sender = IpcMessageSender {
|
||||
waker: Waker::new(registry, message_recv_token)?,
|
||||
waker: Arc::new(Waker::new(registry, message_recv_token)?),
|
||||
sender,
|
||||
};
|
||||
|
||||
let data = IpcData {
|
||||
poll,
|
||||
window_to_client_panel: HashMap::new(),
|
||||
next_window_id: 0,
|
||||
};
|
||||
|
||||
|
@ -175,7 +201,6 @@ impl Ipc {
|
|||
data: Arc::new(RwLock::new(data)),
|
||||
window_sender,
|
||||
message_recv,
|
||||
events,
|
||||
quit: false,
|
||||
listener,
|
||||
signals,
|
||||
|
@ -188,10 +213,29 @@ impl Ipc {
|
|||
Ok((ipc, sender))
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, timeout: Option<Duration>) -> std::io::Result<()> {
|
||||
self.data.write().poll.poll(&mut self.events, timeout)?;
|
||||
pub fn on_message(&mut self, msg: IpcMessage) -> std::io::Result<()> {
|
||||
match msg {
|
||||
IpcMessage::PanelMessage { window, message } => {
|
||||
let data = self.data.read();
|
||||
let (client, panel) = *data.window_to_client_panel.get(&window).unwrap();
|
||||
let client = self.clients.get_mut(client).unwrap();
|
||||
let reply = RecvMessage {
|
||||
id: panel,
|
||||
msg: message,
|
||||
};
|
||||
client
|
||||
.messenger
|
||||
.send(&MagpieClientMsg::RecvMessage(reply))?;
|
||||
}
|
||||
}
|
||||
|
||||
for event in self.events.iter() {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, events: &mut Events, timeout: Option<Duration>) -> std::io::Result<()> {
|
||||
self.data.write().poll.poll(events, timeout)?;
|
||||
|
||||
for event in events.iter() {
|
||||
if event.token() == self.listener_token {
|
||||
loop {
|
||||
match self.listener.accept() {
|
||||
|
@ -227,13 +271,17 @@ impl Ipc {
|
|||
let _ = self.window_sender.send_event(WindowMessage::Quit);
|
||||
self.quit = true;
|
||||
}
|
||||
} else if event.token() == self.message_recv_token {
|
||||
while let Ok(received) = self.message_recv.try_recv() {
|
||||
self.on_message(received)?;
|
||||
}
|
||||
} else if let Some(client) = self.clients.get_mut(event.token().0) {
|
||||
let disconnected = client.on_readable()?;
|
||||
if disconnected {
|
||||
self.clients.remove(event.token().0);
|
||||
self.clients.remove(event.token().0).disconnect();
|
||||
}
|
||||
} else {
|
||||
panic!("Unrecognized event token: {:?}", event);
|
||||
eprintln!("Unrecognized event token: {:?}", event);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,9 +289,10 @@ impl Ipc {
|
|||
}
|
||||
|
||||
pub fn run(mut self) {
|
||||
let mut events = Events::with_capacity(128);
|
||||
while !self.quit {
|
||||
let wait = Duration::from_millis(100);
|
||||
match self.poll(Some(wait)) {
|
||||
match self.poll(&mut events, Some(wait)) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
eprintln!("IPC poll error: {:?}", e);
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub mod gl;
|
||||
pub mod ipc;
|
||||
pub mod window;
|
||||
|
|
|
@ -1,46 +1,73 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
use canary::{Panel, Runtime};
|
||||
use canary::{CursorEventKind, Panel, Runtime, Vec2, PX_PER_MM};
|
||||
use glium::backend::glutin::DisplayCreationError;
|
||||
use glium::{glutin, Surface};
|
||||
use glutin::event::{Event, WindowEvent};
|
||||
use glutin::event::{ElementState, Event, MouseButton, WindowEvent};
|
||||
use glutin::event_loop::{ControlFlow, EventLoop, EventLoopProxy, EventLoopWindowTarget};
|
||||
use glutin::window::WindowId;
|
||||
|
||||
use crate::service::gl::Graphics;
|
||||
use crate::service::ipc::{IpcMessage, IpcMessageSender};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum WindowMessage {
|
||||
OpenWindow { id: usize, script: PathBuf },
|
||||
CloseWindow { id: usize },
|
||||
OpenWindow {
|
||||
id: usize,
|
||||
protocol: String,
|
||||
script: PathBuf,
|
||||
},
|
||||
CloseWindow {
|
||||
id: usize,
|
||||
},
|
||||
Quit,
|
||||
SendMessage { id: usize, msg: Vec<u8> },
|
||||
SendMessage {
|
||||
id: usize,
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
pub type WindowMessageSender = EventLoopProxy<WindowMessage>;
|
||||
|
||||
pub struct Window {
|
||||
pub ipc_sender: IpcMessageSender,
|
||||
pub ipc_id: usize,
|
||||
pub graphics: Graphics,
|
||||
pub panel: Panel,
|
||||
pub last_update: Instant,
|
||||
pub cursor_pos: Vec2,
|
||||
pub cursor_down: bool,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn new(
|
||||
ipc_sender: IpcMessageSender,
|
||||
ipc_id: usize,
|
||||
panel: Panel,
|
||||
event_loop: &EventLoopWindowTarget<WindowMessage>,
|
||||
) -> Result<Self, DisplayCreationError> {
|
||||
let wb = glutin::window::WindowBuilder::new();
|
||||
let cb = glutin::ContextBuilder::new();
|
||||
let wb = glutin::window::WindowBuilder::new()
|
||||
.with_transparent(true)
|
||||
.with_decorations(false);
|
||||
let cb = glutin::ContextBuilder::new()
|
||||
.with_vsync(true)
|
||||
.with_multisampling(4);
|
||||
let display = glium::Display::new(wb, cb, &event_loop)?;
|
||||
let graphics = Graphics::new(display);
|
||||
let last_update = Instant::now();
|
||||
Ok(Self {
|
||||
ipc_sender,
|
||||
ipc_id,
|
||||
graphics,
|
||||
panel,
|
||||
last_update,
|
||||
cursor_pos: Vec2::ZERO,
|
||||
cursor_down: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -52,19 +79,81 @@ impl Window {
|
|||
self.graphics.display.gl_window().window().request_redraw();
|
||||
}
|
||||
|
||||
/// Receives all messages from the script and forwards them to IPC.
|
||||
pub fn recv_messages(&mut self) {
|
||||
for message in self.panel.recv_messages() {
|
||||
self.ipc_sender.send(IpcMessage::PanelMessage {
|
||||
window: self.ipc_id,
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self) {
|
||||
let now = Instant::now();
|
||||
let dt = now.duration_since(self.last_update).as_secs_f32();
|
||||
self.panel.update(dt);
|
||||
self.last_update = now;
|
||||
self.recv_messages();
|
||||
}
|
||||
|
||||
pub fn draw(&mut self) {
|
||||
let commands = self.panel.draw();
|
||||
self.graphics.draw(&commands);
|
||||
self.recv_messages();
|
||||
}
|
||||
|
||||
pub fn send_message(&mut self, msg: Vec<u8>) {
|
||||
self.panel.on_message(msg);
|
||||
self.recv_messages();
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, new_size: Vec2) {
|
||||
self.panel.on_resize(new_size);
|
||||
self.recv_messages();
|
||||
}
|
||||
|
||||
pub fn on_event(&mut self, event: WindowEvent) {
|
||||
match event {
|
||||
WindowEvent::Resized(size) => {
|
||||
self.resize(Vec2::new(size.width as f32, size.height as f32) * PX_PER_MM);
|
||||
self.request_redraw()
|
||||
}
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
let x = position.x as f32 * PX_PER_MM;
|
||||
let y = position.y as f32 * PX_PER_MM;
|
||||
self.cursor_pos = Vec2::new(x, y);
|
||||
|
||||
let event = if self.cursor_down {
|
||||
CursorEventKind::Drag
|
||||
} else {
|
||||
CursorEventKind::Hover
|
||||
};
|
||||
|
||||
self.panel.on_cursor_event(event, self.cursor_pos);
|
||||
self.recv_messages();
|
||||
}
|
||||
WindowEvent::MouseInput {
|
||||
state,
|
||||
button: MouseButton::Left,
|
||||
..
|
||||
} => {
|
||||
let event = match state {
|
||||
ElementState::Pressed => {
|
||||
self.cursor_down = true;
|
||||
CursorEventKind::Select
|
||||
}
|
||||
ElementState::Released => {
|
||||
self.cursor_down = false;
|
||||
CursorEventKind::Deselect
|
||||
}
|
||||
};
|
||||
|
||||
self.panel.on_cursor_event(event, self.cursor_pos);
|
||||
self.recv_messages();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,14 +184,47 @@ impl WindowStore {
|
|||
.flatten()
|
||||
}
|
||||
|
||||
pub fn on_message(
|
||||
&mut self,
|
||||
event_loop: &EventLoopWindowTarget<WindowMessage>,
|
||||
message: WindowMessage,
|
||||
) -> anyhow::Result<bool> {
|
||||
match message {
|
||||
WindowMessage::OpenWindow {
|
||||
id,
|
||||
protocol,
|
||||
script,
|
||||
} => {
|
||||
println!("Opening window {} with script {:?}", id, script);
|
||||
let module = std::fs::read(script)?;
|
||||
let mut script = self.runtime.load_module(&module)?;
|
||||
let panel = script.create_panel(&protocol, vec![])?;
|
||||
let window = Window::new(self.ipc_sender.to_owned(), id, panel, &event_loop)?;
|
||||
let window_id = window.get_id();
|
||||
self.windows.insert(window_id, window);
|
||||
self.ipc_to_window.insert(id, window_id);
|
||||
}
|
||||
WindowMessage::CloseWindow { id } => {
|
||||
if let Some(window_id) = self.ipc_to_window.remove(&id) {
|
||||
self.windows.remove(&window_id);
|
||||
}
|
||||
}
|
||||
WindowMessage::Quit => return Ok(true),
|
||||
WindowMessage::SendMessage { id, msg } => {
|
||||
if let Some(window) = self.get_ipc_window(id) {
|
||||
window.send_message(msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub fn run(mut self, event_loop: EventLoop<WindowMessage>) -> ! {
|
||||
event_loop.run(move |event, event_loop, control_flow| match event {
|
||||
Event::WindowEvent { window_id, event } => {
|
||||
if let Some(window) = self.windows.get_mut(&window_id) {
|
||||
match event {
|
||||
WindowEvent::Resized(_) => window.request_redraw(),
|
||||
_ => {}
|
||||
}
|
||||
window.on_event(event);
|
||||
}
|
||||
}
|
||||
Event::RedrawRequested(id) => {
|
||||
|
@ -116,27 +238,11 @@ impl WindowStore {
|
|||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
Event::UserEvent(event) => match event {
|
||||
WindowMessage::OpenWindow { id, script } => {
|
||||
println!("Opening window {} with script {:?}", id, script);
|
||||
let module = std::fs::read(script).unwrap();
|
||||
let mut script = self.runtime.load_module(&module).unwrap();
|
||||
let panel = script.create_panel(vec![]).unwrap();
|
||||
let window = Window::new(panel, &event_loop).unwrap();
|
||||
let window_id = window.get_id();
|
||||
self.windows.insert(window_id, window);
|
||||
self.ipc_to_window.insert(id, window_id);
|
||||
}
|
||||
WindowMessage::CloseWindow { id } => {
|
||||
if let Some(window_id) = self.ipc_to_window.remove(&id) {
|
||||
self.windows.remove(&window_id);
|
||||
}
|
||||
}
|
||||
WindowMessage::Quit => *control_flow = ControlFlow::Exit,
|
||||
WindowMessage::SendMessage { id, msg } => {
|
||||
if let Some(window) = self.get_ipc_window(id) {
|
||||
window.send_message(msg);
|
||||
}
|
||||
Event::UserEvent(event) => match self.on_message(event_loop, event.clone()) {
|
||||
Ok(false) => {}
|
||||
Ok(true) => *control_flow = ControlFlow::Exit,
|
||||
Err(err) => {
|
||||
eprintln!("Error while handling message {:?}:\n{}", event, err);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
name = "canary-music-player"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[[bin]]
|
||||
name = "canary-music-player"
|
||||
|
@ -9,10 +10,12 @@ path = "src/main.rs"
|
|||
required-features = ["bin"]
|
||||
|
||||
[dependencies]
|
||||
canary-magpie = { path = "../magpie", optional = true }
|
||||
mpris = { version = "2.0.0-rc3", optional = true }
|
||||
canary-magpie = { path = "../magpie", optional = true, features = ["async"] }
|
||||
futures-util = { version = "0.3", optional = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
smol = { version = "1.2", optional = true }
|
||||
zbus = { version = "3.5", optional = true }
|
||||
|
||||
[features]
|
||||
bin = ["dep:canary-magpie", "dep:mpris"]
|
||||
bin = ["dep:canary-magpie", "dep:futures-util", "dep:smol", "dep:zbus"]
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use serde;
|
||||
pub use serde_json;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum PlaybackStatus {
|
||||
/// A track is currently playing.
|
||||
Playing,
|
||||
|
@ -15,7 +18,7 @@ pub enum PlaybackStatus {
|
|||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum LoopStatus {
|
||||
/// The playback will stop when there are no more tracks to play.
|
||||
None,
|
||||
|
@ -31,9 +34,6 @@ pub enum LoopStatus {
|
|||
pub struct ProgressChanged {
|
||||
/// Current position into the track in seconds.
|
||||
pub position: f32,
|
||||
|
||||
/// Length of the current track in seconds.
|
||||
pub length: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
|
@ -45,7 +45,7 @@ pub struct AlbumInfo {
|
|||
pub artists: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TrackInfo {
|
||||
/// The title of the current track.
|
||||
pub title: Option<String>,
|
||||
|
@ -55,6 +55,9 @@ pub struct TrackInfo {
|
|||
|
||||
/// The optional track number on the disc the album the track appears on.
|
||||
pub track_number: Option<i32>,
|
||||
|
||||
/// Length of the track in seconds.
|
||||
pub length: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
|
@ -101,6 +104,6 @@ pub enum OutMsg {
|
|||
/// Sets the volume. Values are clamped to 0.0 to 1.0.
|
||||
SetVolume { volume: f32 },
|
||||
|
||||
/// Set the current track position in seconds.
|
||||
SetPosition { position: f32 },
|
||||
/// Seeks the current track's position in seconds.
|
||||
Seek { offset: f32 },
|
||||
}
|
||||
|
|
|
@ -1,63 +1,193 @@
|
|||
use canary_music_player::*;
|
||||
use canary_magpie::client::MagpieClient;
|
||||
use canary_magpie::protocol::{CreatePanel, MagpieServerMsg};
|
||||
use mpris::PlayerFinder;
|
||||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub struct MetadataTracker {
|
||||
use std::path::Path;
|
||||
|
||||
use canary_magpie::protocol::{
|
||||
ClientMessenger, CreatePanel, MagpieClientMsg, MagpieServerMsg, RecvMessage, MAGPIE_SOCK,
|
||||
};
|
||||
use canary_music_player::*;
|
||||
use smol::net::unix::UnixStream;
|
||||
|
||||
pub type MagpieClient = ClientMessenger<UnixStream>;
|
||||
|
||||
pub mod mpris;
|
||||
|
||||
use mpris::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Metadata {
|
||||
pub album: AlbumInfo,
|
||||
pub track: TrackInfo,
|
||||
}
|
||||
|
||||
impl From<mpris::Metadata> for MetadataTracker {
|
||||
fn from(metadata: mpris::Metadata) -> Self {
|
||||
impl<'a> From<MetadataMap<'a>> for Metadata {
|
||||
fn from(map: MetadataMap<'a>) -> Self {
|
||||
let album = AlbumInfo {
|
||||
title: metadata.album_name().map(ToString::to_string),
|
||||
artists: metadata
|
||||
.album_artists()
|
||||
.unwrap_or(Vec::new())
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect(),
|
||||
title: map
|
||||
.get("xesam:album")
|
||||
.and_then(|v| TryFrom::try_from(v).ok()),
|
||||
artists: map
|
||||
.get("xesam:albumArtist")
|
||||
.cloned()
|
||||
.and_then(|v| TryFrom::try_from(v).ok())
|
||||
.unwrap_or(Vec::new()),
|
||||
};
|
||||
|
||||
let track = TrackInfo {
|
||||
title: metadata.title().map(ToString::to_string),
|
||||
artists: metadata
|
||||
.artists()
|
||||
.unwrap_or(Vec::new())
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect(),
|
||||
track_number: metadata.track_number(),
|
||||
title: map
|
||||
.get("xesam:title")
|
||||
.and_then(|v| TryFrom::try_from(v).ok()),
|
||||
artists: map
|
||||
.get("xesam:artist")
|
||||
.cloned()
|
||||
.and_then(|v| TryFrom::try_from(v).ok())
|
||||
.unwrap_or(Vec::new()),
|
||||
track_number: map
|
||||
.get("xesam:trackNumber")
|
||||
.and_then(|v| TryFrom::try_from(v).ok()),
|
||||
length: map
|
||||
.get("mpris:length")
|
||||
.and_then(|v| i64::try_from(v).ok())
|
||||
.map(|us| us as f32 / 1_000_000.0), // 1,000,000 microseconds in a second
|
||||
};
|
||||
|
||||
Self { album, track }
|
||||
}
|
||||
}
|
||||
|
||||
impl MetadataTracker {
|
||||
pub fn new(magpie: &mut MagpieClient, metadata: mpris::Metadata) -> Self {
|
||||
impl Metadata {
|
||||
pub async fn update_new(magpie: &mut MagpieClient, metadata: MetadataMap<'_>) -> Self {
|
||||
let new: Self = metadata.into();
|
||||
magpie.send_json_message(0, &InMsg::AlbumChanged(new.album.clone()));
|
||||
magpie.send_json_message(0, &InMsg::TrackChanged(new.track.clone()));
|
||||
let msg = InMsg::AlbumChanged(new.album.clone());
|
||||
magpie.send_panel_json_async(0, &msg).await;
|
||||
let msg = InMsg::TrackChanged(new.track.clone());
|
||||
magpie.send_panel_json_async(0, &msg).await;
|
||||
new
|
||||
}
|
||||
|
||||
pub fn update(&mut self, messenger: &mut MagpieClient, metadata: mpris::Metadata) {
|
||||
pub async fn update_diff(&mut self, messenger: &mut MagpieClient, metadata: MetadataMap<'_>) {
|
||||
let new: Self = metadata.into();
|
||||
|
||||
if self.album != new.album {
|
||||
messenger.send_json_message(0, &InMsg::AlbumChanged(new.album.clone()));
|
||||
let msg = InMsg::AlbumChanged(new.album.clone());
|
||||
messenger.send_panel_json_async(0, &msg).await;
|
||||
}
|
||||
|
||||
if self.track != new.track {
|
||||
messenger.send_json_message(0, &InMsg::TrackChanged(new.track.clone()));
|
||||
let msg = InMsg::TrackChanged(new.track.clone());
|
||||
messenger.send_panel_json_async(0, &msg).await;
|
||||
let progress = ProgressChanged { position: 0.0 };
|
||||
let msg = InMsg::ProgressChanged(progress);
|
||||
messenger.send_panel_json_async(0, &msg).await;
|
||||
}
|
||||
|
||||
*self = new;
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_message(
|
||||
player: &PlayerProxy<'_>,
|
||||
magpie: &mut MagpieClient,
|
||||
message: MagpieClientMsg,
|
||||
) -> Result<(), Box<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() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let module_path = args
|
||||
|
@ -65,104 +195,71 @@ fn main() {
|
|||
.expect("Please pass a path to a Canary script!")
|
||||
.to_owned();
|
||||
|
||||
let player_finder = PlayerFinder::new().expect("Could not connect to D-Bus");
|
||||
|
||||
let mut magpie = MagpieClient::new().unwrap();
|
||||
let script = std::path::PathBuf::from(&module_path);
|
||||
let msg = CreatePanel { id: 0, script };
|
||||
let msg = MagpieServerMsg::CreatePanel(msg);
|
||||
magpie.messenger.send(&msg).unwrap();
|
||||
|
||||
let mut first_loop = true;
|
||||
let mut connected = false;
|
||||
|
||||
loop {
|
||||
if !first_loop {
|
||||
let wait = std::time::Duration::from_secs(1);
|
||||
std::thread::sleep(wait);
|
||||
}
|
||||
|
||||
first_loop = false;
|
||||
|
||||
if connected {
|
||||
println!("Disconnected from MPRIS");
|
||||
let msg = InMsg::Disconnected;
|
||||
magpie.send_json_message(0, &msg);
|
||||
connected = false;
|
||||
}
|
||||
|
||||
println!("Connecting to MPRIS...");
|
||||
|
||||
let player = match player_finder.find_active() {
|
||||
Ok(player) => player,
|
||||
Err(err) => {
|
||||
eprintln!("Couldn't find player: {:?}", err);
|
||||
continue;
|
||||
}
|
||||
smol::block_on(async {
|
||||
let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set");
|
||||
let sock_dir = Path::new(&sock_dir);
|
||||
let sock_path = sock_dir.join(MAGPIE_SOCK);
|
||||
let socket = UnixStream::connect(sock_path).await.unwrap();
|
||||
let mut magpie = MagpieClient::new(socket);
|
||||
let protocol = "tebibyte-media.desktop.music-player-controller".to_string();
|
||||
let script = std::path::PathBuf::from(&module_path);
|
||||
let msg = CreatePanel {
|
||||
id: 0,
|
||||
protocol,
|
||||
script,
|
||||
};
|
||||
|
||||
println!(
|
||||
"Connected to \"{}\" ({})",
|
||||
player.identity(),
|
||||
player.bus_name()
|
||||
);
|
||||
connected = true;
|
||||
magpie.send_json_message(0, &InMsg::Connected);
|
||||
let msg = MagpieServerMsg::CreatePanel(msg);
|
||||
magpie.send_async(&msg).await.unwrap();
|
||||
|
||||
let metadata = player.get_metadata().unwrap();
|
||||
let mut metadata_tracker = MetadataTracker::new(&mut magpie, metadata);
|
||||
let dbus = zbus::Connection::session().await.unwrap();
|
||||
|
||||
let mut events = match player.events() {
|
||||
Ok(events) => events,
|
||||
Err(err) => {
|
||||
eprintln!("Player events D-Bus error: {:?}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let mut first_loop = true;
|
||||
let mut connected = false;
|
||||
|
||||
loop {
|
||||
let event = match events.next() {
|
||||
None => break,
|
||||
Some(Ok(e)) => e,
|
||||
Some(Err(err)) => {
|
||||
eprintln!("D-Bus error while reading player events: {:?}", err);
|
||||
if !first_loop {
|
||||
let wait = std::time::Duration::from_secs(1);
|
||||
std::thread::sleep(wait);
|
||||
}
|
||||
|
||||
first_loop = false;
|
||||
|
||||
if connected {
|
||||
println!("Disconnected from MPRIS");
|
||||
let msg = InMsg::Disconnected;
|
||||
magpie.send_panel_json_async(0, &msg).await;
|
||||
connected = false;
|
||||
}
|
||||
|
||||
println!("Connecting to MPRIS...");
|
||||
|
||||
let player = match find_player(&dbus).await {
|
||||
Ok(Some(player)) => player,
|
||||
Ok(None) => {
|
||||
eprintln!("Couldn't find player");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
use mpris::Event::*;
|
||||
let in_msg = match event {
|
||||
Playing => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Playing)),
|
||||
Paused => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Paused)),
|
||||
Stopped => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Stopped)),
|
||||
LoopingChanged(status) => {
|
||||
use mpris::LoopStatus::*;
|
||||
let status = match status {
|
||||
None => LoopStatus::None,
|
||||
Track => LoopStatus::Track,
|
||||
Playlist => LoopStatus::Playlist,
|
||||
};
|
||||
|
||||
Some(InMsg::LoopingChanged(status))
|
||||
}
|
||||
ShuffleToggled(shuffle) => Some(InMsg::ShuffleChanged { shuffle }),
|
||||
VolumeChanged(volume) => Some(InMsg::VolumeChanged {
|
||||
volume: volume as f32,
|
||||
}),
|
||||
PlayerShutDown => None,
|
||||
TrackChanged(metadata) => {
|
||||
metadata_tracker.update(&mut magpie, metadata);
|
||||
None
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Unhandled MPRIS message: {:?}", event);
|
||||
None
|
||||
Err(err) => {
|
||||
eprintln!("D-Bus error while finding player: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(msg) = in_msg {
|
||||
magpie.send_json_message(0, &msg);
|
||||
println!(
|
||||
"Connected to \"{}\" ({})",
|
||||
player.path().as_str(),
|
||||
player.destination().as_str()
|
||||
);
|
||||
connected = true;
|
||||
magpie.send_panel_json_async(0, &InMsg::Connected).await;
|
||||
|
||||
match player_main(&player, &mut magpie).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
eprintln!("D-Bus error while connected to player: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use zbus::fdo::DBusProxy;
|
||||
use zbus::zvariant::Value;
|
||||
use zbus::{dbus_proxy, Connection, Result};
|
||||
|
||||
pub type MetadataMap<'a> = HashMap<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)
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use canary::{CursorEventKind, Panel, Runtime, Script};
|
||||
use canary::{CursorEventKind, Panel, Runtime, Script, PX_PER_MM};
|
||||
use eframe::egui;
|
||||
use std::time::Instant;
|
||||
|
||||
|
@ -31,7 +31,9 @@ struct App {
|
|||
panels: Vec<PanelWindow>,
|
||||
next_idx: usize,
|
||||
last_update: Instant,
|
||||
protocol_buf: String,
|
||||
bind_message_buf: String,
|
||||
panel_bg: egui::Color32,
|
||||
}
|
||||
|
||||
impl App {
|
||||
|
@ -46,7 +48,9 @@ impl App {
|
|||
panels: vec![],
|
||||
next_idx: 0,
|
||||
last_update: Instant::now(),
|
||||
protocol_buf: String::new(),
|
||||
bind_message_buf: String::new(),
|
||||
panel_bg: egui::Color32::TRANSPARENT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,12 +60,18 @@ impl eframe::App for App {
|
|||
ctx.request_repaint();
|
||||
|
||||
egui::SidePanel::left("left_panel").show(ctx, |ui| {
|
||||
ui.heading("New Panel");
|
||||
|
||||
ui.label("Protocol name:");
|
||||
ui.text_edit_singleline(&mut self.protocol_buf);
|
||||
|
||||
ui.label("Bind message:");
|
||||
let text_edit = egui::TextEdit::multiline(&mut self.bind_message_buf).code_editor();
|
||||
ui.add(text_edit);
|
||||
|
||||
if ui.button("Bind Panel").clicked() {
|
||||
let msg = self.bind_message_buf.as_bytes().to_vec();
|
||||
let panel = self.script.create_panel(msg).unwrap();
|
||||
let panel = self.script.create_panel(&self.protocol_buf, msg).unwrap();
|
||||
let index = self.next_idx;
|
||||
self.next_idx += 1;
|
||||
|
||||
|
@ -70,10 +80,19 @@ impl eframe::App for App {
|
|||
index,
|
||||
msg_buf: String::new(),
|
||||
show_msg: false,
|
||||
current_size: Default::default(),
|
||||
};
|
||||
|
||||
self.panels.push(panel);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.heading("Global Settings");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Panel background color: ");
|
||||
ui.color_edit_button_srgba(&mut self.panel_bg);
|
||||
});
|
||||
});
|
||||
|
||||
let dt = self.last_update.elapsed().as_secs_f32();
|
||||
|
@ -81,7 +100,7 @@ impl eframe::App for App {
|
|||
|
||||
for panel in self.panels.iter_mut() {
|
||||
panel.panel.update(dt);
|
||||
panel.show(ctx);
|
||||
panel.show(self.panel_bg, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,80 +110,91 @@ pub struct PanelWindow {
|
|||
pub index: usize,
|
||||
pub msg_buf: String,
|
||||
pub show_msg: bool,
|
||||
pub current_size: egui::Vec2,
|
||||
}
|
||||
|
||||
impl PanelWindow {
|
||||
pub fn show(&mut self, ctx: &egui::Context) {
|
||||
pub fn show(&mut self, bg: egui::Color32, ctx: &egui::Context) {
|
||||
let frame = egui::Frame::window(&ctx.style()).fill(bg);
|
||||
let window_id = egui::Id::new(format!("panel_{}", self.index));
|
||||
egui::Window::new("Panel").id(window_id).show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
ui.checkbox(&mut self.show_msg, "Show Message Editor");
|
||||
});
|
||||
egui::Window::new("Panel")
|
||||
.frame(frame)
|
||||
.id(window_id)
|
||||
.show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
ui.checkbox(&mut self.show_msg, "Show Message Editor");
|
||||
});
|
||||
|
||||
let size = egui::vec2(800.0, 800.0);
|
||||
let sense = egui::Sense {
|
||||
click: true,
|
||||
drag: true,
|
||||
focusable: true,
|
||||
};
|
||||
|
||||
let (rect, response) = ui.allocate_at_least(size, sense);
|
||||
|
||||
if let Some(hover_pos) = response.hover_pos() {
|
||||
let local = (hover_pos - rect.left_top()) / rect.size();
|
||||
let norm = local * 2.0 - egui::vec2(1.0, 1.0);
|
||||
let x = norm.x;
|
||||
let y = -norm.y;
|
||||
let pos = canary::Vec2 { x, y };
|
||||
|
||||
let kind = if response.drag_started() {
|
||||
CursorEventKind::Select
|
||||
} else if response.drag_released() {
|
||||
CursorEventKind::Deselect
|
||||
} else if response.dragged() {
|
||||
CursorEventKind::Drag
|
||||
} else {
|
||||
CursorEventKind::Hover
|
||||
let sense = egui::Sense {
|
||||
click: true,
|
||||
drag: true,
|
||||
focusable: true,
|
||||
};
|
||||
|
||||
self.panel.on_cursor_event(kind, pos);
|
||||
}
|
||||
let desired_size = ui.available_size();
|
||||
let response = ui.allocate_response(desired_size, sense);
|
||||
let rect = response.rect;
|
||||
|
||||
let texture = egui::TextureId::Managed(0);
|
||||
let uv = egui::pos2(0.0, 0.0);
|
||||
let mut mesh = egui::Mesh::with_texture(texture);
|
||||
if rect.size() != self.current_size {
|
||||
let size = rect.size();
|
||||
self.current_size = size;
|
||||
|
||||
let commands = self.panel.draw();
|
||||
for command in commands.into_iter() {
|
||||
let voff = mesh.vertices.len() as u32;
|
||||
|
||||
match command {
|
||||
canary::DrawCommand::Mesh { vertices, indices } => {
|
||||
for v in vertices.iter() {
|
||||
use egui::epaint::Vertex;
|
||||
let pos = egui::pos2(v.position.x, -v.position.y);
|
||||
let pos = pos.to_vec2() / 2.0 + egui::vec2(0.5, 0.5);
|
||||
let pos = rect.left_top() + pos * rect.size();
|
||||
let (r, g, b, a) = v.color.to_rgba_unmultiplied();
|
||||
let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a);
|
||||
let v = Vertex { pos, uv, color };
|
||||
mesh.vertices.push(v);
|
||||
}
|
||||
|
||||
for i in indices.iter() {
|
||||
mesh.indices.push(i + voff);
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
let size = canary::Vec2::new(size.x, size.y);
|
||||
self.panel.on_resize(size * PX_PER_MM);
|
||||
}
|
||||
}
|
||||
|
||||
let painter = ui.painter_at(rect);
|
||||
let shape = egui::Shape::mesh(mesh);
|
||||
painter.add(shape);
|
||||
if let Some(hover_pos) = response.hover_pos() {
|
||||
let local = (hover_pos - rect.left_top()) * PX_PER_MM;
|
||||
let pos = canary::Vec2::new(local.x, local.y);
|
||||
|
||||
response
|
||||
});
|
||||
let kind = if response.drag_started() {
|
||||
CursorEventKind::Select
|
||||
} else if response.drag_released() {
|
||||
CursorEventKind::Deselect
|
||||
} else if response.dragged() {
|
||||
CursorEventKind::Drag
|
||||
} else {
|
||||
CursorEventKind::Hover
|
||||
};
|
||||
|
||||
self.panel.on_cursor_event(kind, pos);
|
||||
}
|
||||
|
||||
let texture = egui::TextureId::Managed(0);
|
||||
let uv = egui::pos2(0.0, 0.0);
|
||||
let mut mesh = egui::Mesh::with_texture(texture);
|
||||
|
||||
let commands = self.panel.draw();
|
||||
for command in commands.into_iter() {
|
||||
let voff = mesh.vertices.len() as u32;
|
||||
|
||||
match command {
|
||||
canary::DrawCommand::Mesh { vertices, indices } => {
|
||||
for v in vertices.iter() {
|
||||
use egui::epaint::Vertex;
|
||||
let pos = v.position / PX_PER_MM;
|
||||
let pos = egui::pos2(pos.x, pos.y);
|
||||
let pos = pos + rect.left_top().to_vec2();
|
||||
let (r, g, b, a) = v.color.to_rgba_unmultiplied();
|
||||
let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a);
|
||||
let v = Vertex { pos, uv, color };
|
||||
mesh.vertices.push(v);
|
||||
}
|
||||
|
||||
for i in indices.iter() {
|
||||
mesh.indices.push(i + voff);
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
let painter = ui.painter_at(rect);
|
||||
let shape = egui::Shape::mesh(mesh);
|
||||
painter.add(shape);
|
||||
|
||||
response
|
||||
});
|
||||
|
||||
let msg_edit_id = egui::Id::new(format!("msg_edit_{}", self.index));
|
||||
egui::Window::new("Message Editor")
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# check if we have tomcat(1)
|
||||
if ! command -v tomcat >/dev/null 2>&1; then
|
||||
printf "%s: Missing dependency: tomcat(1)\n"
|
||||
exit 69 # sysexits(3) EX_UNAVAILABLE
|
||||
fi
|
||||
|
||||
dir="$(pwd | sed 's/\//\n/g' | tail -n 1)"
|
||||
|
||||
for toml in $(find "$PWD" -name "Cargo.toml"); do
|
||||
printf "Project: %s\n" "$(tomcat package.name "$toml")"
|
||||
for file in $(find "$(printf "%s\n" "$toml" |\
|
||||
sed 's/Cargo\.toml/src/g')" -name "*.rs")
|
||||
do
|
||||
info="$(head -n 2 "$file")"
|
||||
toml_lic="$(tomcat package.license "$toml")"
|
||||
if ! test -n "$toml_lic"; then
|
||||
printf "%s: Missing license information\n" "$(printf "%s\n" "$toml" |\
|
||||
sed "s/^.\+$dir\///g")"
|
||||
continue 2
|
||||
fi
|
||||
if ! [ "$toml_lic" = "$(printf "%s\n" "$info" | tail -n 1 |\
|
||||
sed -n 's/\/\/ SPDX-License-Identifier: //p')" ]
|
||||
then
|
||||
printf "%s: Missing or malformed license information\n" \
|
||||
"$(printf "%s\n" "$file" | sed "s/^.\+$dir\///g")"
|
||||
fi
|
||||
if ! test -n "$(printf "%s\n" "$info" | head -n 1 |\
|
||||
sed -n '/\/\/ Copyright (c) .\+/p')"
|
||||
then
|
||||
printf "%s: Missing or malformed copyright holder information\n" \
|
||||
"$(printf "%s\n" "$file" | sed "s/^.\+$dir\///g")"
|
||||
fi
|
||||
done
|
||||
done
|
|
@ -7,11 +7,17 @@ use super::*;
|
|||
|
||||
static mut PANEL_IMPLS: Vec<Box<dyn PanelImpl>> = Vec::new();
|
||||
|
||||
pub fn bind_panel<T: BindPanel>(panel: u32, msg: u32) -> u32 {
|
||||
pub fn bind_panel(
|
||||
cb: impl Fn(Panel, Message, Message) -> Box<dyn PanelImpl>,
|
||||
panel: u32,
|
||||
protocol: u32,
|
||||
msg: u32,
|
||||
) -> u32 {
|
||||
unsafe {
|
||||
let panel = Panel(panel);
|
||||
let protocol = Message(protocol);
|
||||
let msg = Message(msg);
|
||||
let panel_impl = T::bind(panel, msg);
|
||||
let panel_impl = cb(panel, protocol, msg);
|
||||
let id = PANEL_IMPLS.len() as u32;
|
||||
PANEL_IMPLS.push(panel_impl);
|
||||
id
|
||||
|
@ -28,6 +34,12 @@ pub fn draw(panel_data: u32) {
|
|||
panel.draw();
|
||||
}
|
||||
|
||||
pub fn on_resize(panel_data: u32, width: f32, height: f32) {
|
||||
let panel = unsafe { &mut PANEL_IMPLS[panel_data as usize] };
|
||||
let new_size = Vec2::new(width, height);
|
||||
panel.on_resize(new_size);
|
||||
}
|
||||
|
||||
pub fn on_cursor_event(panel_data: u32, kind: u32, x: f32, y: f32) {
|
||||
let panel = unsafe { &mut PANEL_IMPLS[panel_data as usize] };
|
||||
let at = Vec2::new(x, y);
|
||||
|
|
|
@ -9,10 +9,10 @@ pub mod abi;
|
|||
|
||||
#[macro_export]
|
||||
macro_rules! export_abi {
|
||||
($panel_impl: ident) => {
|
||||
($bind_panel: ident) => {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bind_panel(panel: u32, msg: u32) -> u32 {
|
||||
::canary_script::api::abi::bind_panel::<$panel_impl>(panel, msg)
|
||||
pub extern "C" fn bind_panel(panel: u32, protocol: u32, msg: u32) -> u32 {
|
||||
::canary_script::api::abi::bind_panel($bind_panel, panel, protocol, msg)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
@ -25,6 +25,11 @@ macro_rules! export_abi {
|
|||
::canary_script::api::abi::draw(panel_data)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn on_resize(panel_data: u32, width: f32, height: f32) {
|
||||
::canary_script::api::abi::on_resize(panel_data, width, height)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn on_cursor_event(panel_data: u32, kind: u32, x: f32, y: f32) {
|
||||
::canary_script::api::abi::on_cursor_event(panel_data, kind, x, y)
|
||||
|
@ -37,13 +42,10 @@ macro_rules! export_abi {
|
|||
};
|
||||
}
|
||||
|
||||
pub trait BindPanel {
|
||||
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl>;
|
||||
}
|
||||
|
||||
pub trait PanelImpl {
|
||||
fn update(&mut self, dt: f32);
|
||||
fn draw(&mut self);
|
||||
fn on_resize(&mut self, new_size: Vec2);
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2);
|
||||
fn on_message(&mut self, msg: Message);
|
||||
}
|
||||
|
@ -92,6 +94,10 @@ impl Panel {
|
|||
|
||||
self.draw_indexed(&vertices, &indices);
|
||||
}
|
||||
|
||||
pub fn send_message(&self, message: &[u8]) {
|
||||
unsafe { panel_send_message(self.0, message.as_ptr() as u32, message.len() as u32) }
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
|
@ -180,6 +186,24 @@ impl DrawContext {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]) {
|
||||
let mut vertices = vertices.to_vec();
|
||||
|
||||
if let Some(offset) = self.offset {
|
||||
for v in vertices.iter_mut() {
|
||||
v.position += offset;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(opacity) = self.opacity {
|
||||
for v in vertices.iter_mut() {
|
||||
v.color = v.color.alpha_multiply(opacity);
|
||||
}
|
||||
}
|
||||
|
||||
self.panel.draw_indexed(&vertices, &indices);
|
||||
}
|
||||
|
||||
pub fn draw_triangle(&self, v1: Vec2, v2: Vec2, v3: Vec2, color: Color) {
|
||||
if let Some(clip_rect) = self.clip_rect.as_ref() {
|
||||
let tri = ColoredTriangle { v1, v2, v3, color };
|
||||
|
@ -231,10 +255,10 @@ impl DrawContext {
|
|||
let delta = PI / 4.0 / spoke_num;
|
||||
|
||||
let (mut theta, limit) = match corner {
|
||||
Corner::TopRight => (0.0, FRAC_PI_2),
|
||||
Corner::BottomRight => (FRAC_PI_2 * 3.0, PI * 2.0),
|
||||
Corner::BottomLeft => (PI, FRAC_PI_2 * 3.0),
|
||||
Corner::TopLeft => (FRAC_PI_2, PI),
|
||||
Corner::TopRight => (FRAC_PI_2 * 3.0, PI * 2.0),
|
||||
Corner::BottomRight => (0.0, FRAC_PI_2),
|
||||
Corner::BottomLeft => (FRAC_PI_2, PI),
|
||||
Corner::TopLeft => (PI, FRAC_PI_2 * 3.0),
|
||||
};
|
||||
|
||||
let mut last_spoke = Vec2::from_angle(theta) * radius + center;
|
||||
|
@ -281,10 +305,10 @@ impl DrawContext {
|
|||
rect
|
||||
};
|
||||
|
||||
let v1 = rect.bl;
|
||||
let v2 = Vec2::new(rect.bl.x, rect.tr.y);
|
||||
let v3 = Vec2::new(rect.tr.x, rect.bl.y);
|
||||
let v4 = rect.tr;
|
||||
let v1 = rect.tl;
|
||||
let v2 = Vec2::new(rect.tl.x, rect.br.y);
|
||||
let v3 = Vec2::new(rect.br.x, rect.tl.y);
|
||||
let v4 = rect.br;
|
||||
|
||||
self.draw_triangle_noclip(v1, v2, v3, color);
|
||||
self.draw_triangle_noclip(v2, v3, v4, color);
|
||||
|
@ -309,48 +333,48 @@ impl DrawContext {
|
|||
let mut inner_rect = rect;
|
||||
let inset = rect.inset(radius);
|
||||
|
||||
if corners.intersects(CornerFlags::BOTTOM) {
|
||||
inner_rect.bl.y += radius;
|
||||
|
||||
let mut bottom_edge = Rect {
|
||||
bl: rect.bl,
|
||||
tr: Vec2::new(rect.tr.x, rect.bl.y + radius),
|
||||
};
|
||||
|
||||
if corners.contains(CornerFlags::BOTTOM_LEFT) {
|
||||
bottom_edge.bl.x += radius;
|
||||
self.draw_quarter_circle(Corner::BottomLeft, inset.bl, radius, color);
|
||||
}
|
||||
|
||||
if corners.contains(CornerFlags::BOTTOM_RIGHT) {
|
||||
bottom_edge.tr.x -= radius;
|
||||
self.draw_quarter_circle(Corner::BottomRight, inset.br(), radius, color);
|
||||
}
|
||||
|
||||
self.draw_rect(bottom_edge, color);
|
||||
}
|
||||
|
||||
if corners.intersects(CornerFlags::TOP) {
|
||||
inner_rect.tr.y -= radius;
|
||||
inner_rect.tl.y += radius;
|
||||
|
||||
let mut top_edge = Rect {
|
||||
bl: Vec2::new(rect.bl.x, rect.tr.y - radius),
|
||||
tr: rect.tr,
|
||||
tl: rect.tl,
|
||||
br: Vec2::new(rect.br.x, rect.tl.y + radius),
|
||||
};
|
||||
|
||||
if corners.contains(CornerFlags::TOP_LEFT) {
|
||||
top_edge.bl.x += radius;
|
||||
self.draw_quarter_circle(Corner::TopLeft, inset.tl(), radius, color);
|
||||
top_edge.tl.x += radius;
|
||||
self.draw_quarter_circle(Corner::TopLeft, inset.tl, radius, color);
|
||||
}
|
||||
|
||||
if corners.contains(CornerFlags::TOP_RIGHT) {
|
||||
top_edge.tr.x -= radius;
|
||||
self.draw_quarter_circle(Corner::TopRight, inset.tr, radius, color);
|
||||
top_edge.br.x -= radius;
|
||||
self.draw_quarter_circle(Corner::TopRight, inset.tr(), radius, color);
|
||||
}
|
||||
|
||||
self.draw_rect(top_edge, color);
|
||||
}
|
||||
|
||||
if corners.intersects(CornerFlags::BOTTOM) {
|
||||
inner_rect.br.y -= radius;
|
||||
|
||||
let mut bottom_edge = Rect {
|
||||
tl: Vec2::new(rect.tl.x, rect.br.y - radius),
|
||||
br: rect.br,
|
||||
};
|
||||
|
||||
if corners.contains(CornerFlags::BOTTOM_LEFT) {
|
||||
bottom_edge.tl.x += radius;
|
||||
self.draw_quarter_circle(Corner::BottomLeft, inset.bl(), radius, color);
|
||||
}
|
||||
|
||||
if corners.contains(CornerFlags::BOTTOM_RIGHT) {
|
||||
bottom_edge.br.x -= radius;
|
||||
self.draw_quarter_circle(Corner::BottomRight, inset.br, radius, color);
|
||||
}
|
||||
|
||||
self.draw_rect(bottom_edge, color);
|
||||
}
|
||||
|
||||
self.draw_rect(inner_rect, color);
|
||||
}
|
||||
|
||||
|
@ -423,4 +447,6 @@ extern "C" {
|
|||
|
||||
fn message_get_len(id: u32) -> u32;
|
||||
fn message_get_data(id: u32, ptr: u32);
|
||||
|
||||
fn panel_send_message(id: u32, message_ptr: u32, message_len: u32);
|
||||
}
|
||||
|
|
|
@ -6,84 +6,83 @@ use bytemuck::{Pod, Zeroable};
|
|||
pub use glam::Vec2;
|
||||
use num_derive::{FromPrimitive, ToPrimitive};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod api;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||
pub struct Rect {
|
||||
pub bl: Vec2,
|
||||
pub tr: Vec2,
|
||||
pub tl: Vec2,
|
||||
pub br: Vec2,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub const NEG_INFINITY: Self = Self {
|
||||
bl: Vec2::splat(f32::INFINITY),
|
||||
tr: Vec2::splat(f32::NEG_INFINITY),
|
||||
tl: Vec2::splat(f32::INFINITY),
|
||||
br: Vec2::splat(f32::NEG_INFINITY),
|
||||
};
|
||||
|
||||
pub fn from_xy_size(xy: Vec2, size: Vec2) -> Self {
|
||||
Self {
|
||||
bl: xy,
|
||||
tr: xy + size,
|
||||
tl: xy,
|
||||
br: xy + size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_circle_bounds(center: Vec2, radius: f32) -> Self {
|
||||
Self {
|
||||
bl: center - radius,
|
||||
tr: center + radius,
|
||||
tl: center - radius,
|
||||
br: center + radius,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_triangle_bounds(tri: &ColoredTriangle) -> Self {
|
||||
Self {
|
||||
bl: tri.v1.min(tri.v2).min(tri.v3),
|
||||
tr: tri.v1.max(tri.v2).max(tri.v3),
|
||||
tl: tri.v1.min(tri.v2).min(tri.v3),
|
||||
br: tri.v1.max(tri.v2).max(tri.v3),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inset(&self, d: f32) -> Self {
|
||||
Self {
|
||||
bl: self.bl + d,
|
||||
tr: self.tr - d,
|
||||
tl: self.tl + d,
|
||||
br: self.br - d,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tl(&self) -> Vec2 {
|
||||
Vec2::new(self.bl.x, self.tr.y)
|
||||
|
||||
pub fn bl(&self) -> Vec2 {
|
||||
Vec2::new(self.tl.x, self.br.y)
|
||||
}
|
||||
|
||||
pub fn br(&self) -> Vec2 {
|
||||
Vec2::new(self.tr.x, self.bl.y)
|
||||
pub fn tr(&self) -> Vec2 {
|
||||
Vec2::new(self.br.x, self.tl.y)
|
||||
}
|
||||
|
||||
pub fn offset(&self, offset: Vec2) -> Self {
|
||||
Self {
|
||||
bl: self.bl + offset,
|
||||
tr: self.tr + offset,
|
||||
tl: self.tl + offset,
|
||||
br: self.br + offset,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale(&self, scale: f32) -> Self {
|
||||
Self {
|
||||
bl: self.bl * scale,
|
||||
tr: self.tr * scale,
|
||||
tl: self.tl * scale,
|
||||
br: self.br * scale,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.bl.cmplt(self.tr).all()
|
||||
self.tl.cmplt(self.br).all()
|
||||
}
|
||||
|
||||
pub fn intersects_rect(&self, other: &Self) -> bool {
|
||||
self.bl.cmple(other.tr).all() && self.tr.cmpge(other.bl).all()
|
||||
self.tl.cmple(other.br).all() && self.br.cmpge(other.tl).all()
|
||||
}
|
||||
|
||||
pub fn intersection(&self, other: &Self) -> Option<Self> {
|
||||
let clipped = Self {
|
||||
bl: self.bl.max(other.bl),
|
||||
tr: self.tr.min(other.tr),
|
||||
tl: self.tl.max(other.tl),
|
||||
br: self.br.min(other.br),
|
||||
};
|
||||
|
||||
if clipped.is_valid() {
|
||||
|
@ -93,27 +92,41 @@ impl Rect {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn union(&self, other: &Self) -> Self {
|
||||
Self {
|
||||
tl: self.tl.min(other.tl),
|
||||
br: self.br.max(other.br),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn union_point(&self, point: Vec2) -> Self {
|
||||
Self {
|
||||
tl: self.tl.min(point),
|
||||
br: self.br.max(point),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains_rect(&self, other: &Self) -> bool {
|
||||
self.bl.x < other.bl.x
|
||||
&& self.bl.y < other.bl.y
|
||||
&& self.tr.x > other.tr.x
|
||||
&& self.tr.y > other.tr.y
|
||||
self.tl.x < other.tl.x
|
||||
&& self.tl.y < other.tl.y
|
||||
&& self.br.x > other.br.x
|
||||
&& self.br.y > other.br.y
|
||||
}
|
||||
|
||||
pub fn contains_point(&self, xy: Vec2) -> bool {
|
||||
self.bl.x < xy.x && self.bl.y < xy.y && self.tr.x > xy.x && self.tr.y > xy.y
|
||||
self.tl.x < xy.x && self.tl.y < xy.y && self.br.x > xy.x && self.br.y > xy.y
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.tr - self.bl
|
||||
self.br - self.tl
|
||||
}
|
||||
|
||||
pub fn width(&self) -> f32 {
|
||||
self.tr.x - self.bl.x
|
||||
self.br.x - self.tl.x
|
||||
}
|
||||
|
||||
pub fn height(&self) -> f32 {
|
||||
self.tr.y - self.bl.y
|
||||
self.br.y - self.tl.y
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,15 +173,21 @@ impl Color {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn alpha_multiply(&self, mul: u8) -> Self {
|
||||
pub const fn alpha_multiply(&self, mul: u8) -> Self {
|
||||
let a = self.0 as u8 as u16;
|
||||
let multiplied = ((a * (mul as u16)) >> 8) as u8;
|
||||
self.with_alpha(multiplied)
|
||||
}
|
||||
|
||||
pub fn with_alpha(&self, alpha: u8) -> Self {
|
||||
pub const fn with_alpha(&self, alpha: u8) -> Self {
|
||||
Self(self.0 & 0xffffff00 | alpha as u32)
|
||||
}
|
||||
|
||||
pub fn lerp(self, target: Self, blend: f32) -> Self {
|
||||
let s: glam::Vec4 = self.into();
|
||||
let o: glam::Vec4 = target.into();
|
||||
(o * blend + s * (1.0 - blend)).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
name = "canary-music-player-script"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
use canary_script::*;
|
||||
use api::*;
|
||||
use canary_script::*;
|
||||
|
||||
canary_script::export_abi!(MusicPlayerPanel);
|
||||
canary_script::export_abi!(bind_panel_impl);
|
||||
|
||||
pub fn bind_panel_impl(panel: Panel, _protocol: Message, message: Message) -> Box<dyn PanelImpl> {
|
||||
MusicPlayerPanel::bind(panel, message)
|
||||
}
|
||||
|
||||
const DISPLAY_FONT: &str = "Liberation Sans";
|
||||
|
||||
|
@ -14,19 +21,6 @@ pub struct MusicPlayerPanel {
|
|||
label: Label,
|
||||
}
|
||||
|
||||
impl BindPanel for MusicPlayerPanel {
|
||||
fn bind(panel: Panel, message: Message) -> Box<dyn PanelImpl> {
|
||||
let display_font = Font::new(DISPLAY_FONT);
|
||||
let label = Label::new(display_font, "Hello, world!".into(), 1.2);
|
||||
let panel = Self {
|
||||
panel,
|
||||
display_font,
|
||||
label,
|
||||
};
|
||||
Box::new(panel)
|
||||
}
|
||||
}
|
||||
|
||||
impl PanelImpl for MusicPlayerPanel {
|
||||
fn update(&mut self, dt: f32) {}
|
||||
|
||||
|
@ -39,16 +33,31 @@ impl PanelImpl for MusicPlayerPanel {
|
|||
self.label.draw(&ctx, offset, size, color);
|
||||
}
|
||||
|
||||
fn on_resize(&mut self, new_size: Vec2) {}
|
||||
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {}
|
||||
|
||||
fn on_message(&mut self, msg: Message) {
|
||||
use canary_music_player::{InMsg, serde_json};
|
||||
use canary_music_player::{serde_json, InMsg};
|
||||
let msg = msg.to_vec();
|
||||
let msg = serde_json::from_slice::<InMsg>(&msg);
|
||||
self.label.set_text(format!("{:#?}", msg));
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicPlayerPanel {
|
||||
pub fn bind(panel: Panel, _message: Message) -> Box<dyn PanelImpl> {
|
||||
let display_font = Font::new(DISPLAY_FONT);
|
||||
let label = Label::new(display_font, "Hello, world!".into(), 1.2);
|
||||
let panel = Self {
|
||||
panel,
|
||||
display_font,
|
||||
label,
|
||||
};
|
||||
Box::new(panel)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Label {
|
||||
font: Font,
|
||||
text: String,
|
||||
|
|
|
@ -10,6 +10,7 @@ crate-type = ["cdylib"]
|
|||
[dependencies]
|
||||
glam = "^0.21"
|
||||
keyframe = "1"
|
||||
canary-music-player = { path = "../../apps/music-player" }
|
||||
canary-script = { path = "../../crates/script" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::Color;
|
||||
use keyframe::EasingFunction;
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -1,18 +1,33 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
pub mod anim;
|
||||
pub mod draw;
|
||||
pub mod main_menu;
|
||||
pub mod panel;
|
||||
pub mod music_player;
|
||||
pub mod style;
|
||||
pub mod widgets;
|
||||
|
||||
use canary_script::*;
|
||||
use api::*;
|
||||
use widgets::Widget;
|
||||
use canary_script::*;
|
||||
use main_menu::MainMenuPanel;
|
||||
use music_player::MusicPlayerPanel;
|
||||
use widgets::Widget;
|
||||
|
||||
export_abi!(MainMenuPanel);
|
||||
export_abi!(bind_panel_impl);
|
||||
|
||||
fn bind_panel_impl(panel: Panel, protocol: Message, msg: Message) -> Box<dyn PanelImpl> {
|
||||
let protocol = protocol.to_vec();
|
||||
let protocol = String::from_utf8(protocol).unwrap();
|
||||
|
||||
match protocol.as_str() {
|
||||
"tebibyte-media.desktop.music-player-controller" => MusicPlayerPanel::bind(panel, msg),
|
||||
"wip-dialog" => ConfirmationDialogPanel::bind(panel, msg),
|
||||
_ => MainMenuPanel::bind(panel, msg),
|
||||
}
|
||||
}
|
||||
|
||||
pub const ICON_FONT: &str = "Iosevka Nerd Font";
|
||||
pub const DISPLAY_FONT: &str = "Homenaje";
|
||||
|
@ -23,18 +38,6 @@ pub struct ConfirmationDialogPanel {
|
|||
dialog: widgets::dialog::Dialog,
|
||||
}
|
||||
|
||||
impl BindPanel for ConfirmationDialogPanel {
|
||||
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
||||
let msg = msg.to_vec();
|
||||
let info: DialogInfo = serde_json::from_slice(&msg).unwrap();
|
||||
|
||||
use widgets::dialog::*;
|
||||
let style = DialogStyle::default();
|
||||
let dialog = Dialog::new(style, &info);
|
||||
Box::new(Self { panel, dialog })
|
||||
}
|
||||
}
|
||||
|
||||
impl PanelImpl for ConfirmationDialogPanel {
|
||||
fn update(&mut self, dt: f32) {
|
||||
self.dialog.update(dt);
|
||||
|
@ -45,9 +48,31 @@ impl PanelImpl for ConfirmationDialogPanel {
|
|||
self.dialog.draw(&ctx);
|
||||
}
|
||||
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: canary_script::Vec2) {
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
|
||||
self.dialog.on_cursor_event(kind, at.into());
|
||||
}
|
||||
|
||||
fn on_resize(&mut self, size: Vec2) {
|
||||
self.dialog.resize(size);
|
||||
}
|
||||
|
||||
fn on_message(&mut self, _msg: Message) {}
|
||||
}
|
||||
|
||||
impl ConfirmationDialogPanel {
|
||||
pub fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
||||
// let msg = msg.to_vec();
|
||||
// let info: DialogInfo = serde_json::from_slice(&msg).unwrap();
|
||||
|
||||
let info = DialogInfo {
|
||||
title: "Hello world!".to_string(),
|
||||
content: "Testing, testing...".to_string(),
|
||||
responses: vec![DialogResponse::Yes, DialogResponse::No],
|
||||
};
|
||||
|
||||
use widgets::dialog::*;
|
||||
let style = DialogStyle::default();
|
||||
let dialog = Dialog::new(style, &info);
|
||||
Box::new(Self { panel, dialog })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::widgets::prelude::*;
|
||||
use crate::{DrawContext, Rect};
|
||||
|
||||
use button::{RectButton, RoundButton, RoundButtonStyle};
|
||||
use dialog::{Dialog, DialogInfo, DialogResponse, DialogStyle};
|
||||
use menu::{SlotMenu, SlotMenuEvent, TabMenu};
|
||||
use palette::Palette;
|
||||
use shell::{Offset, OffsetAlignment, Popup, Reveal};
|
||||
use text::LabelText;
|
||||
|
||||
|
@ -12,15 +16,6 @@ pub struct MainMenuPanel {
|
|||
menu: MainMenu,
|
||||
}
|
||||
|
||||
impl BindPanel for MainMenuPanel {
|
||||
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
||||
Box::new(Self {
|
||||
panel,
|
||||
menu: MainMenu::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PanelImpl for MainMenuPanel {
|
||||
fn update(&mut self, dt: f32) {
|
||||
Widget::update(&mut self.menu, dt);
|
||||
|
@ -31,23 +26,35 @@ impl PanelImpl for MainMenuPanel {
|
|||
Widget::draw(&mut self.menu, &ctx);
|
||||
}
|
||||
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: canary_script::Vec2) {
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
|
||||
Widget::on_cursor_event(&mut self.menu, kind, at.into());
|
||||
}
|
||||
|
||||
fn on_resize(&mut self, _size: Vec2) {}
|
||||
|
||||
fn on_message(&mut self, msg: Message) {}
|
||||
}
|
||||
|
||||
impl MainMenuPanel {
|
||||
pub fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
||||
Box::new(Self {
|
||||
panel,
|
||||
menu: MainMenu::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MainMenu {
|
||||
pub menu: Offset<SlotMenu<RoundButton>>,
|
||||
pub player_info: Reveal<Offset<PlayerInfo>>,
|
||||
pub inventory: Reveal<Offset<TabMenu>>,
|
||||
pub palette: Reveal<Offset<Palette>>,
|
||||
pub settings: Reveal<Offset<SettingsMenu>>,
|
||||
}
|
||||
|
||||
impl MainMenu {
|
||||
pub const POSITION_X: f32 = -0.40;
|
||||
pub const SUBMENU_SPACING: f32 = 0.1;
|
||||
pub const ANCHOR: Vec2 = Vec2::new(100.0, 100.0);
|
||||
pub const SUBMENU_SPACING: f32 = 15.0;
|
||||
}
|
||||
|
||||
impl Default for MainMenu {
|
||||
|
@ -56,12 +63,12 @@ impl Default for MainMenu {
|
|||
let icons = ["", "", "", "", "", ""];
|
||||
|
||||
let button_style = RoundButtonStyle {
|
||||
radius: 0.05,
|
||||
spacing: 0.01,
|
||||
thickness: 0.002,
|
||||
body_color: Color::WHITE,
|
||||
ring_color: Color::WHITE,
|
||||
icon_color: Color::BLACK,
|
||||
radius: 7.5,
|
||||
spacing: 1.5,
|
||||
thickness: 0.4,
|
||||
body_color: THEME.palette.surface,
|
||||
ring_color: THEME.palette.surface,
|
||||
icon_color: THEME.palette.text,
|
||||
};
|
||||
|
||||
let mut buttons = Vec::new();
|
||||
|
@ -75,12 +82,13 @@ impl Default for MainMenu {
|
|||
buttons.push(button);
|
||||
}
|
||||
|
||||
let menu = SlotMenu::new(buttons, 0.18);
|
||||
let menu = Offset::new(menu, Vec2::new(Self::POSITION_X, 0.0));
|
||||
let menu = SlotMenu::new(buttons, 30.0);
|
||||
let menu = Offset::new(menu, Self::ANCHOR);
|
||||
|
||||
let submenu_spacing_left = Vec2::new(Self::POSITION_X - Self::SUBMENU_SPACING, 0.0);
|
||||
let submenu_spacing_right = Vec2::new(Self::POSITION_X + Self::SUBMENU_SPACING, 0.0);
|
||||
let reveal_slide = -0.02;
|
||||
let submenu_spacing = Vec2::new(Self::SUBMENU_SPACING, 0.0);
|
||||
let submenu_spacing_left = Self::ANCHOR - submenu_spacing;
|
||||
let submenu_spacing_right = Self::ANCHOR + submenu_spacing;
|
||||
let reveal_slide = -5.0;
|
||||
let reveal_duration = 0.1;
|
||||
|
||||
let player_info = PlayerInfo::new();
|
||||
|
@ -96,6 +104,15 @@ impl Default for MainMenu {
|
|||
let inventory = Offset::new(inventory, submenu_spacing_right);
|
||||
let inventory = Reveal::new(inventory, reveal_slide, reveal_duration);
|
||||
|
||||
let palette = Palette::new(Default::default());
|
||||
let palette = Offset::new_aligned(
|
||||
palette,
|
||||
submenu_spacing_left,
|
||||
OffsetAlignment::End,
|
||||
OffsetAlignment::Center,
|
||||
);
|
||||
let palette = Reveal::new(palette, -reveal_slide, reveal_duration);
|
||||
|
||||
let settings = SettingsMenu::new();
|
||||
let settings = Offset::new(settings, submenu_spacing_right);
|
||||
let settings = Reveal::new(settings, reveal_slide, reveal_duration);
|
||||
|
@ -104,6 +121,7 @@ impl Default for MainMenu {
|
|||
menu,
|
||||
player_info,
|
||||
inventory,
|
||||
palette,
|
||||
settings,
|
||||
}
|
||||
}
|
||||
|
@ -114,6 +132,7 @@ impl Container for MainMenu {
|
|||
f(&mut self.menu);
|
||||
f(&mut self.player_info);
|
||||
f(&mut self.inventory);
|
||||
f(&mut self.palette);
|
||||
f(&mut self.settings);
|
||||
}
|
||||
|
||||
|
@ -133,8 +152,14 @@ impl Container for MainMenu {
|
|||
self.player_info.hide();
|
||||
self.inventory.hide();
|
||||
}
|
||||
SlotMenuEvent::SubmenuOpen(4) => self.settings.show(),
|
||||
SlotMenuEvent::SubmenuClose(4) => self.settings.hide(),
|
||||
SlotMenuEvent::SubmenuOpen(4) => {
|
||||
self.palette.show();
|
||||
self.settings.show();
|
||||
}
|
||||
SlotMenuEvent::SubmenuClose(4) => {
|
||||
self.palette.hide();
|
||||
self.settings.hide();
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
@ -144,14 +169,16 @@ pub struct PlayerInfo {
|
|||
width: f32,
|
||||
height: f32,
|
||||
rounding: f32,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl PlayerInfo {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
width: 0.5,
|
||||
height: 0.9,
|
||||
rounding: 0.02,
|
||||
width: 70.0,
|
||||
height: 120.0,
|
||||
rounding: 5.0,
|
||||
color: THEME.palette.surface,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -164,7 +191,7 @@ impl RectBounds for PlayerInfo {
|
|||
|
||||
impl Widget for PlayerInfo {
|
||||
fn draw(&mut self, ctx: &DrawContext) {
|
||||
ctx.draw_rounded_rect(self.get_bounds(), self.rounding, Color::WHITE);
|
||||
ctx.draw_rounded_rect(self.get_bounds(), self.rounding, self.color);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,7 +202,7 @@ pub struct Inventory {
|
|||
|
||||
impl Inventory {
|
||||
pub fn new(available_width: f32) -> (Self, f32) {
|
||||
let height = 1.28;
|
||||
let height = 1024.0;
|
||||
|
||||
(
|
||||
Self {
|
||||
|
@ -189,8 +216,8 @@ impl Inventory {
|
|||
|
||||
impl Widget for Inventory {
|
||||
fn draw(&mut self, ctx: &DrawContext) {
|
||||
let box_size = 0.06;
|
||||
let box_margin = 0.02;
|
||||
let box_size = 12.0;
|
||||
let box_margin = 4.0;
|
||||
let box_stride = box_size + box_margin;
|
||||
|
||||
let grid_width = (self.width / box_stride).floor() as usize;
|
||||
|
@ -227,7 +254,7 @@ impl SettingsMenu {
|
|||
("Log Out", ""),
|
||||
];
|
||||
|
||||
let button_size = Vec2::new(0.4, 0.1);
|
||||
let button_size = Vec2::new(90.0, 20.0);
|
||||
let button_rect = Rect::from_xy_size(Vec2::new(0.0, -button_size.y / 2.0), button_size);
|
||||
|
||||
let mut buttons = Vec::new();
|
||||
|
@ -248,7 +275,7 @@ impl SettingsMenu {
|
|||
buttons.push(button);
|
||||
}
|
||||
|
||||
let menu = SlotMenu::new(buttons, 0.12);
|
||||
let menu = SlotMenu::new(buttons, 25.0);
|
||||
|
||||
Self {
|
||||
menu,
|
||||
|
|
|
@ -0,0 +1,427 @@
|
|||
use api::*;
|
||||
use canary_script::*;
|
||||
|
||||
use canary_music_player::{AlbumInfo, OutMsg, PlaybackStatus, ProgressChanged, TrackInfo};
|
||||
|
||||
use crate::widgets::prelude::*;
|
||||
use button::{RoundButton, RoundButtonStyle};
|
||||
use dialog::{DialogBodyStyle, DialogFooterStyle};
|
||||
use shell::Offset;
|
||||
use slider::Slider;
|
||||
use text::{HorizontalAlignment, Label, LabelText};
|
||||
|
||||
pub struct MusicPlayerPanel {
|
||||
panel: Panel,
|
||||
widget: Option<MusicPlayerWidget>,
|
||||
disconnected: Offset<Label>,
|
||||
}
|
||||
|
||||
impl PanelImpl for MusicPlayerPanel {
|
||||
fn update(&mut self, dt: f32) {
|
||||
if let Some(widget) = self.widget.as_mut() {
|
||||
Widget::update(widget, dt);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self) {
|
||||
let ctx = DrawContext::new(self.panel);
|
||||
|
||||
if let Some(widget) = self.widget.as_mut() {
|
||||
Widget::draw(widget, &ctx);
|
||||
} else {
|
||||
self.disconnected.draw(&ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_resize(&mut self, new_size: Vec2) {
|
||||
self.disconnected.set_offset(new_size / 2.0);
|
||||
|
||||
if let Some(widget) = self.widget.as_mut() {
|
||||
widget.resize(new_size);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
|
||||
if let Some(widget) = self.widget.as_mut() {
|
||||
Widget::on_cursor_event(widget, kind, at);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_message(&mut self, msg: Message) {
|
||||
use canary_music_player::{serde_json::from_slice, InMsg};
|
||||
let msg = msg.to_vec();
|
||||
let msg: InMsg = match from_slice(&msg) {
|
||||
Ok(msg) => msg,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
use InMsg::*;
|
||||
match (self.widget.as_mut(), msg) {
|
||||
(Some(_), Disconnected) => self.widget = None,
|
||||
(None, Connected) => self.widget = Some(MusicPlayerWidget::new(self.panel)),
|
||||
(Some(widget), AlbumChanged(info)) => widget.update_album(info),
|
||||
(Some(widget), TrackChanged(info)) => widget.update_track(info),
|
||||
(Some(widget), PlaybackStatusChanged(status)) => widget.update_playback_status(status),
|
||||
(Some(widget), ProgressChanged(progress)) => widget.update_progress(progress),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicPlayerPanel {
|
||||
pub fn bind(panel: Panel, _msg: Message) -> Box<dyn PanelImpl> {
|
||||
let dc_text = LabelText {
|
||||
font: Font::new(crate::DISPLAY_FONT),
|
||||
text: "Disconnected".to_string(),
|
||||
};
|
||||
|
||||
let disconnected = Label::new_centered(dc_text, 10.0, Color::WHITE);
|
||||
let disconnected = Offset::new(disconnected, Vec2::ZERO);
|
||||
|
||||
Box::new(Self {
|
||||
panel,
|
||||
widget: None,
|
||||
disconnected,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MusicPlayerStyle {
|
||||
pub body: DialogBodyStyle,
|
||||
pub footer: DialogFooterStyle,
|
||||
pub rounding: f32,
|
||||
pub art_margin: f32,
|
||||
pub button_spacing: f32,
|
||||
pub slider_height: f32,
|
||||
}
|
||||
|
||||
impl Default for MusicPlayerStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
body: Default::default(),
|
||||
footer: Default::default(),
|
||||
rounding: 5.0,
|
||||
art_margin: 5.0,
|
||||
button_spacing: 15.0,
|
||||
slider_height: 7.5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MusicPlayerWidget {
|
||||
panel: Panel,
|
||||
artist: Offset<Label>,
|
||||
album: Offset<Label>,
|
||||
track: Offset<Label>,
|
||||
previous: Offset<RoundButton>,
|
||||
play: Offset<RoundButton>,
|
||||
next: Offset<RoundButton>,
|
||||
position: Offset<Label>,
|
||||
duration: Offset<Label>,
|
||||
slider: Slider,
|
||||
style: MusicPlayerStyle,
|
||||
art_rect: Rect,
|
||||
body_rect: Rect,
|
||||
footer_rect: Rect,
|
||||
position_secs: f32,
|
||||
duration_secs: f32,
|
||||
position_dirty: bool,
|
||||
position_updating: bool,
|
||||
status: PlaybackStatus,
|
||||
}
|
||||
|
||||
impl Container for MusicPlayerWidget {
|
||||
fn with_children(&mut self, mut f: impl FnMut(&mut dyn Widget)) {
|
||||
f(&mut self.artist);
|
||||
f(&mut self.album);
|
||||
f(&mut self.track);
|
||||
f(&mut self.previous);
|
||||
f(&mut self.play);
|
||||
f(&mut self.next);
|
||||
f(&mut self.position);
|
||||
f(&mut self.duration);
|
||||
f(&mut self.slider);
|
||||
}
|
||||
|
||||
fn update(&mut self, dt: f32) {
|
||||
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;
|
||||
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.previous.was_clicked() {
|
||||
self.send_message(&OutMsg::Previous);
|
||||
}
|
||||
|
||||
if self.play.was_clicked() {
|
||||
self.send_message(&OutMsg::PlayPause);
|
||||
}
|
||||
|
||||
if self.next.was_clicked() {
|
||||
self.send_message(&OutMsg::Next);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, ctx: &DrawContext) {
|
||||
ctx.draw_partially_rounded_rect(
|
||||
CornerFlags::TOP,
|
||||
self.body_rect,
|
||||
self.style.rounding,
|
||||
self.style.body.color,
|
||||
);
|
||||
|
||||
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(
|
||||
CornerFlags::BOTTOM,
|
||||
self.footer_rect,
|
||||
self.style.rounding,
|
||||
self.style.footer.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicPlayerWidget {
|
||||
pub fn new(panel: Panel) -> Self {
|
||||
let style = MusicPlayerStyle::default();
|
||||
let display_font = Font::new(crate::DISPLAY_FONT);
|
||||
let content_font = Font::new(crate::CONTENT_FONT);
|
||||
|
||||
let make_body_label = |content: &str| {
|
||||
let text = LabelText {
|
||||
font: display_font,
|
||||
text: content.to_string(),
|
||||
};
|
||||
|
||||
let color = style.body.text_color;
|
||||
let label = Label::new_centered(text, 10.0, color);
|
||||
Offset::new(label, Vec2::ZERO)
|
||||
};
|
||||
|
||||
let make_footer_label = |content: &str| {
|
||||
let text = LabelText {
|
||||
font: content_font,
|
||||
text: content.to_string(),
|
||||
};
|
||||
|
||||
let size = style.footer.height;
|
||||
let scale = size * 0.4;
|
||||
let baseline = size * 0.125;
|
||||
let label = Label::new(
|
||||
text,
|
||||
HorizontalAlignment::Center,
|
||||
scale,
|
||||
THEME.palette.text,
|
||||
0.0,
|
||||
0.0,
|
||||
baseline,
|
||||
);
|
||||
Offset::new(label, Vec2::ZERO)
|
||||
};
|
||||
|
||||
let icon_font = Font::new(crate::ICON_FONT);
|
||||
|
||||
let prev_text = LabelText {
|
||||
font: icon_font,
|
||||
text: "玲".to_string(),
|
||||
};
|
||||
|
||||
let play_text = LabelText {
|
||||
font: icon_font,
|
||||
text: "契".to_string(),
|
||||
};
|
||||
|
||||
let next_text = LabelText {
|
||||
font: icon_font,
|
||||
text: "怜".to_string(),
|
||||
};
|
||||
|
||||
let primary_button = RoundButtonStyle {
|
||||
radius: style.footer.height * 0.3,
|
||||
spacing: style.footer.height * 0.1,
|
||||
thickness: style.footer.height * 0.025,
|
||||
body_color: THEME.palette.yellow,
|
||||
ring_color: THEME.palette.yellow,
|
||||
icon_color: THEME.palette.black,
|
||||
};
|
||||
|
||||
let secondary_button = RoundButtonStyle {
|
||||
radius: style.footer.height * 0.25,
|
||||
spacing: style.footer.height * 0.05,
|
||||
thickness: style.footer.height * 0.025,
|
||||
body_color: style.footer.color,
|
||||
ring_color: THEME.palette.black,
|
||||
icon_color: THEME.palette.black,
|
||||
};
|
||||
|
||||
let prev = RoundButton::new(secondary_button.clone(), Some(prev_text));
|
||||
let play = RoundButton::new(primary_button, Some(play_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 {
|
||||
panel,
|
||||
artist: make_body_label("Artist"),
|
||||
album: make_body_label("Album"),
|
||||
track: make_body_label("Track"),
|
||||
previous: Offset::new(prev, Vec2::ZERO),
|
||||
play: Offset::new(play, Vec2::ZERO),
|
||||
next: Offset::new(next, Vec2::ZERO),
|
||||
position: make_footer_label("--:--"),
|
||||
duration: make_footer_label("--:--"),
|
||||
slider,
|
||||
style,
|
||||
art_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),
|
||||
position_secs: 0.0,
|
||||
duration_secs: 0.0,
|
||||
position_dirty: false,
|
||||
position_updating: false,
|
||||
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 {
|
||||
let duration = secs.floor() as usize;
|
||||
let seconds = duration % 60;
|
||||
let minutes = (duration / 60) % 60;
|
||||
let hours = (duration / 60) / 60;
|
||||
|
||||
if hours > 0 {
|
||||
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
|
||||
} else {
|
||||
format!("{:02}:{:02}", minutes, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, new_size: Vec2) {
|
||||
let style = &self.style;
|
||||
let width = new_size.x;
|
||||
let body_height = new_size.y - style.footer.height;
|
||||
let body_height = body_height.max(0.0);
|
||||
let body_size = Vec2::new(width, body_height);
|
||||
let footer_size = Vec2::new(width, style.footer.height);
|
||||
|
||||
let art_size = body_height - style.art_margin * 2.0;
|
||||
self.art_rect = Rect::from_xy_size(Vec2::splat(style.art_margin), Vec2::splat(art_size));
|
||||
|
||||
let label_x = (width + self.art_rect.br.x) / 2.0;
|
||||
let artist_baseline = body_height * 0.25;
|
||||
let album_baseline = body_height * 0.55;
|
||||
let track_baseline = body_height * 0.85;
|
||||
|
||||
let button_y = body_height + style.footer.height / 2.0;
|
||||
let previous_x = style.button_spacing * 0.5;
|
||||
let play_x = style.button_spacing * 1.5;
|
||||
let next_x = style.button_spacing * 2.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 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.album.set_offset(Vec2::new(label_x, album_baseline));
|
||||
self.track.set_offset(Vec2::new(label_x, track_baseline));
|
||||
self.position.set_offset(Vec2::new(position_x, button_y));
|
||||
self.duration.set_offset(Vec2::new(duration_x, button_y));
|
||||
|
||||
self.body_rect = Rect::from_xy_size(Vec2::ZERO, body_size);
|
||||
self.footer_rect = Rect::from_xy_size(self.body_rect.bl(), footer_size);
|
||||
|
||||
self.previous.set_offset(Vec2::new(previous_x, button_y));
|
||||
self.play.set_offset(Vec2::new(play_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) {
|
||||
self.album.set_text(
|
||||
info.title
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("<album here>"),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn update_track(&mut self, info: TrackInfo) {
|
||||
self.artist.set_text(
|
||||
info.artists
|
||||
.first()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("<artist here>"),
|
||||
);
|
||||
|
||||
self.track.set_text(
|
||||
info.title
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.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) {
|
||||
self.position_secs = progress.position;
|
||||
self.position_dirty = true;
|
||||
}
|
||||
|
||||
pub fn update_playback_status(&mut self, status: PlaybackStatus) {
|
||||
self.status = status;
|
||||
|
||||
let icon = match status {
|
||||
PlaybackStatus::Playing => "契",
|
||||
PlaybackStatus::Paused => "",
|
||||
PlaybackStatus::Stopped => "栗",
|
||||
};
|
||||
|
||||
self.play.set_text(icon);
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
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,
|
||||
},
|
||||
};
|
|
@ -48,6 +48,12 @@ impl RoundButton {
|
|||
icon,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: &str) {
|
||||
if let Some(icon) = self.icon.as_mut() {
|
||||
icon.set_text(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Button for RoundButton {
|
||||
|
@ -112,6 +118,8 @@ pub struct RectButtonStyle {
|
|||
pub inactive_color: Color,
|
||||
pub hover_color: Color,
|
||||
pub selected_color: Color,
|
||||
pub icon_color: Color,
|
||||
pub label_color: Color,
|
||||
}
|
||||
|
||||
impl Default for RectButtonStyle {
|
||||
|
@ -123,9 +131,11 @@ impl Default for RectButtonStyle {
|
|||
label_baseline: 0.25,
|
||||
icon_scale_factor: 0.8,
|
||||
icon_margin_factor: 1.1,
|
||||
inactive_color: Color::WHITE.with_alpha(0x40),
|
||||
hover_color: Color::WHITE.with_alpha(0xb0),
|
||||
selected_color: Color::YELLOW,
|
||||
inactive_color: THEME.palette.base,
|
||||
hover_color: THEME.palette.base_hover,
|
||||
selected_color: THEME.palette.base_active,
|
||||
icon_color: THEME.palette.black,
|
||||
label_color: THEME.palette.text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -154,7 +164,7 @@ impl RectButton {
|
|||
label: Option<LabelText>,
|
||||
icon: Option<LabelText>,
|
||||
) -> Self {
|
||||
let mut label_left = rect.bl.x;
|
||||
let mut label_left = rect.tl.x;
|
||||
let mut alignment = HorizontalAlignment::Center;
|
||||
|
||||
let icon = icon.map(|text| {
|
||||
|
@ -162,9 +172,9 @@ impl RectButton {
|
|||
label_left += margin;
|
||||
alignment = HorizontalAlignment::Left;
|
||||
let scale = rect.height() * style.icon_scale_factor;
|
||||
let color = Color::BLACK;
|
||||
let cx = rect.bl.x + margin / 2.0;
|
||||
let cy = rect.bl.y + rect.height() / 2.0;
|
||||
let color = style.icon_color;
|
||||
let cx = rect.tl.x + margin / 2.0;
|
||||
let cy = rect.tl.y + rect.height() / 2.0;
|
||||
let center = Vec2::new(cx, cy);
|
||||
|
||||
Icon::new(text, scale, color, center)
|
||||
|
@ -173,10 +183,10 @@ impl RectButton {
|
|||
let label = label.map(|text| {
|
||||
let scale = rect.height() * style.label_scale_factor;
|
||||
let left = label_left;
|
||||
let right = rect.tr.x;
|
||||
let baseline = rect.bl.y;
|
||||
let baseline = (rect.height() * style.label_baseline) + baseline;
|
||||
let color = Color::BLACK;
|
||||
let right = rect.br.x;
|
||||
let baseline = rect.tl.y;
|
||||
let baseline = (rect.height() * (1.0 - style.label_baseline)) + baseline;
|
||||
let color = style.label_color;
|
||||
|
||||
Label::new(text, alignment, scale, color, left, right, baseline)
|
||||
});
|
||||
|
|
|
@ -23,15 +23,14 @@ impl DialogResponse {
|
|||
|
||||
pub fn get_color(&self) -> Color {
|
||||
match self {
|
||||
DialogResponse::Yes => Color::BLUE,
|
||||
DialogResponse::No => Color::RED,
|
||||
DialogResponse::Yes => THEME.palette.blue,
|
||||
DialogResponse::No => THEME.palette.red,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DialogStyle {
|
||||
pub width: f32,
|
||||
pub rounding: f32,
|
||||
pub header: DialogHeaderStyle,
|
||||
pub body: DialogBodyStyle,
|
||||
|
@ -41,8 +40,7 @@ pub struct DialogStyle {
|
|||
impl Default for DialogStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
width: 1.2,
|
||||
rounding: 0.02,
|
||||
rounding: THEME.metrics.surface_rounding,
|
||||
header: Default::default(),
|
||||
body: Default::default(),
|
||||
footer: Default::default(),
|
||||
|
@ -63,10 +61,10 @@ pub struct DialogHeaderStyle {
|
|||
impl Default for DialogHeaderStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
color: Color::WHITE,
|
||||
height: 0.3,
|
||||
color: THEME.palette.surface,
|
||||
height: 20.0,
|
||||
text_font: Font::new(crate::DISPLAY_FONT),
|
||||
text_color: Color::BLACK,
|
||||
text_color: THEME.palette.text,
|
||||
text_scale_factor: 0.65,
|
||||
text_baseline: 0.25,
|
||||
}
|
||||
|
@ -76,20 +74,18 @@ impl Default for DialogHeaderStyle {
|
|||
#[derive(Clone)]
|
||||
pub struct DialogBodyStyle {
|
||||
pub color: Color,
|
||||
pub height: f32,
|
||||
pub text_font: Font,
|
||||
pub text_color: Color,
|
||||
pub text_scale_factor: f32,
|
||||
pub text_size: f32,
|
||||
}
|
||||
|
||||
impl Default for DialogBodyStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
color: Color::WHITE.with_alpha(0xb0),
|
||||
height: 0.6,
|
||||
color: THEME.palette.base,
|
||||
text_font: Font::new(crate::CONTENT_FONT),
|
||||
text_color: Color::BLACK,
|
||||
text_scale_factor: 0.15,
|
||||
text_color: THEME.palette.text,
|
||||
text_size: 5.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,6 +95,7 @@ pub struct DialogFooterStyle {
|
|||
pub icon_font: Font,
|
||||
pub button_radius: f32,
|
||||
pub color: Color,
|
||||
pub button_fg: Color,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
|
@ -106,9 +103,10 @@ impl Default for DialogFooterStyle {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
icon_font: Font::new(crate::ICON_FONT),
|
||||
button_radius: 0.1,
|
||||
color: Color::WHITE,
|
||||
height: 0.25,
|
||||
button_radius: 7.5,
|
||||
color: THEME.palette.surface,
|
||||
button_fg: THEME.palette.white,
|
||||
height: 15.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -122,53 +120,16 @@ pub struct DialogInfo {
|
|||
|
||||
pub struct Dialog {
|
||||
style: DialogStyle,
|
||||
title: Label,
|
||||
content: Label,
|
||||
title: Offset<Label>,
|
||||
content: Offset<Label>,
|
||||
content_size: Vec2,
|
||||
buttons: Vec<DialogButton>,
|
||||
}
|
||||
|
||||
impl Dialog {
|
||||
pub fn new(style: DialogStyle, info: &DialogInfo) -> Self {
|
||||
let width2 = style.width / 2.0;
|
||||
|
||||
let button_y = -(style.body.height + style.footer.height) / 2.0;
|
||||
let button_spacing = style.width / info.responses.len() as f32;
|
||||
let button_spacing2 = button_spacing / 2.0;
|
||||
|
||||
let title_scale = style.header.height * style.header.text_scale_factor;
|
||||
let title_baseline =
|
||||
style.header.height * style.header.text_baseline + style.body.height / 2.0;
|
||||
let title = Label::new(
|
||||
LabelText {
|
||||
font: style.header.text_font,
|
||||
text: info.title.to_string(),
|
||||
},
|
||||
HorizontalAlignment::Center,
|
||||
title_scale,
|
||||
style.header.text_color,
|
||||
-width2,
|
||||
width2,
|
||||
title_baseline,
|
||||
);
|
||||
|
||||
let content_scale = style.body.height * style.body.text_scale_factor;
|
||||
let content = Label::new(
|
||||
LabelText {
|
||||
font: style.body.text_font,
|
||||
text: info.content.to_string(),
|
||||
},
|
||||
HorizontalAlignment::Center,
|
||||
content_scale,
|
||||
style.body.text_color,
|
||||
-width2,
|
||||
width2,
|
||||
0.0,
|
||||
);
|
||||
|
||||
let mut buttons = Vec::new();
|
||||
for (index, response) in info.responses.iter().enumerate() {
|
||||
let button_x = button_spacing * index as f32 + button_spacing2 - style.width / 2.0;
|
||||
|
||||
for response in info.responses.iter() {
|
||||
let color = response.get_color();
|
||||
let radius = style.footer.button_radius;
|
||||
let button_style = RoundButtonStyle {
|
||||
|
@ -177,7 +138,7 @@ impl Dialog {
|
|||
thickness: radius * 0.05,
|
||||
body_color: color,
|
||||
ring_color: color,
|
||||
icon_color: Color::WHITE,
|
||||
icon_color: style.footer.button_fg,
|
||||
};
|
||||
|
||||
let text = LabelText {
|
||||
|
@ -186,7 +147,7 @@ impl Dialog {
|
|||
};
|
||||
|
||||
let button = RoundButton::new(button_style, Some(text));
|
||||
let button = Offset::new(button, Vec2::new(button_x, button_y));
|
||||
let button = Offset::new(button, Vec2::ZERO);
|
||||
|
||||
buttons.push(DialogButton {
|
||||
response: *response,
|
||||
|
@ -194,11 +155,67 @@ impl Dialog {
|
|||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
let title_scale = style.header.height * style.header.text_scale_factor;
|
||||
let title = Label::new(
|
||||
LabelText {
|
||||
font: style.header.text_font,
|
||||
text: info.title.to_string(),
|
||||
},
|
||||
HorizontalAlignment::Center,
|
||||
title_scale,
|
||||
style.header.text_color,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
);
|
||||
|
||||
let content = Label::new(
|
||||
LabelText {
|
||||
font: style.body.text_font,
|
||||
text: info.content.to_string(),
|
||||
},
|
||||
HorizontalAlignment::Center,
|
||||
style.body.text_size,
|
||||
style.body.text_color,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
);
|
||||
|
||||
let mut dialog = Self {
|
||||
style,
|
||||
title,
|
||||
content,
|
||||
title: Offset::new(title, Vec2::ZERO),
|
||||
content: Offset::new(content, Vec2::ZERO),
|
||||
content_size: Vec2::ONE,
|
||||
buttons,
|
||||
};
|
||||
|
||||
dialog.resize(Vec2::splat(100.0));
|
||||
|
||||
dialog
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: Vec2) {
|
||||
let style = &self.style;
|
||||
let width = size.x;
|
||||
let width2 = width / 2.0;
|
||||
let body_height = size.y - style.header.height - style.footer.height;
|
||||
let body_height = body_height.max(0.0);
|
||||
let title_baseline = style.header.height * (1.0 - style.header.text_baseline);
|
||||
let content_baseline = style.header.height + body_height / 2.0;
|
||||
let button_baseline = style.header.height + body_height + style.footer.height / 2.0;
|
||||
let button_spacing = width / self.buttons.len() as f32;
|
||||
let button_spacing2 = button_spacing / 2.0;
|
||||
|
||||
self.content_size = Vec2::new(width, body_height);
|
||||
self.title.set_offset(Vec2::new(width2, title_baseline));
|
||||
self.content.set_offset(Vec2::new(width2, content_baseline));
|
||||
|
||||
for (index, button) in self.buttons.iter_mut().enumerate() {
|
||||
let button_x = button_spacing * index as f32 + button_spacing2;
|
||||
button
|
||||
.button
|
||||
.set_offset(Vec2::new(button_x, button_baseline));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -215,24 +232,15 @@ impl Container for Dialog {
|
|||
|
||||
fn draw(&mut self, ctx: &DrawContext) {
|
||||
let style = &self.style;
|
||||
let width = style.width;
|
||||
let width2 = width / 2.0;
|
||||
let width = self.content_size.x;
|
||||
let rounding = style.rounding;
|
||||
|
||||
let header_xy = Vec2::new(-width2, style.body.height / 2.0);
|
||||
let header_size = Vec2::new(width, style.header.height);
|
||||
let header_rect = Rect::from_xy_size(header_xy, header_size);
|
||||
let header_rect = Rect::from_xy_size(Vec2::ZERO, header_size);
|
||||
let header_corners = CornerFlags::TOP;
|
||||
|
||||
let body_tr = Vec2::new(width2, style.body.height / 2.0);
|
||||
let body_rect = Rect {
|
||||
bl: -body_tr,
|
||||
tr: body_tr,
|
||||
};
|
||||
|
||||
let footer_xy = Vec2::new(-width2, -style.body.height / 2.0 - style.footer.height);
|
||||
let body_rect = Rect::from_xy_size(header_rect.bl(), self.content_size);
|
||||
let footer_size = Vec2::new(width, style.footer.height);
|
||||
let footer_rect = Rect::from_xy_size(footer_xy, footer_size);
|
||||
let footer_rect = Rect::from_xy_size(body_rect.bl(), footer_size);
|
||||
let footer_corners = CornerFlags::BOTTOM;
|
||||
|
||||
ctx.draw_rect(body_rect, style.body.color);
|
||||
|
|
|
@ -52,7 +52,7 @@ impl<T> SlotMenu<T> {
|
|||
let out_delay = i as f32 * inter_button_delay;
|
||||
let in_delay = max_delay - out_delay;
|
||||
|
||||
let mut slide_anim = Animation::new(EaseOut, duration, 0.25, 0.0);
|
||||
let mut slide_anim = Animation::new(EaseOut, duration, -50.0, 0.0);
|
||||
slide_anim.set_in_delay(in_delay);
|
||||
slide_anim.set_out_delay(out_delay);
|
||||
slide_anim.ease_in();
|
||||
|
@ -108,7 +108,7 @@ impl<T> SlotMenu<T> {
|
|||
|
||||
pub fn for_buttons(&mut self, mut cb: impl FnMut(&mut SlotMenuButton<T>, usize, f32)) {
|
||||
for (i, button) in self.buttons.iter_mut().enumerate() {
|
||||
let y = -(i as f32 - self.scroll_anim.get()) * self.spacing + button.slide_anim.get();
|
||||
let y = (i as f32 - self.scroll_anim.get()) * self.spacing + button.slide_anim.get();
|
||||
cb(button, i, y);
|
||||
}
|
||||
}
|
||||
|
@ -222,22 +222,23 @@ pub struct TabMenu {
|
|||
}
|
||||
|
||||
impl TabMenu {
|
||||
const HEAD_RADIUS: f32 = 0.05;
|
||||
const HEAD_HEIGHT: f32 = 0.1;
|
||||
const TAB_WIDTH: f32 = 0.1;
|
||||
const TAB_HEIGHT: f32 = 0.15;
|
||||
const HEAD_RADIUS: f32 = 5.0;
|
||||
const HEAD_HEIGHT: f32 = 15.0;
|
||||
const HEAD_COLOR: Color = THEME.palette.surface;
|
||||
const TAB_WIDTH: f32 = 15.0;
|
||||
const TAB_HEIGHT: f32 = 25.0;
|
||||
const TAB_NUM: usize = 6;
|
||||
const SEPARATOR_WIDTH: f32 = 0.02;
|
||||
const INNER_RADIUS: f32 = 0.01;
|
||||
const CONTENT_WIDTH: f32 = 0.64;
|
||||
const SEPARATOR_WIDTH: f32 = 5.0;
|
||||
const INNER_RADIUS: f32 = 5.0;
|
||||
const CONTENT_WIDTH: f32 = 100.0;
|
||||
|
||||
const HEAD_BUTTON_STYLE: RoundButtonStyle = RoundButtonStyle {
|
||||
radius: Self::HEAD_RADIUS * 0.5,
|
||||
spacing: Self::HEAD_RADIUS * 0.2,
|
||||
thickness: Self::HEAD_RADIUS * 0.1,
|
||||
body_color: Color::WHITE,
|
||||
ring_color: Color::BLACK,
|
||||
icon_color: Color::BLACK,
|
||||
radius: Self::HEAD_HEIGHT * 0.25,
|
||||
spacing: Self::HEAD_HEIGHT * 0.1,
|
||||
thickness: Self::HEAD_HEIGHT * 0.05,
|
||||
body_color: Self::HEAD_COLOR,
|
||||
ring_color: THEME.palette.black,
|
||||
icon_color: THEME.palette.black,
|
||||
};
|
||||
|
||||
const HEAD_BUTTON_MARGIN: f32 = Self::HEAD_HEIGHT / 2.0;
|
||||
|
@ -248,8 +249,8 @@ impl TabMenu {
|
|||
|
||||
let mut tabs = Vec::new();
|
||||
for i in 0..Self::TAB_NUM {
|
||||
let y = (i + 1) as f32 * Self::TAB_HEIGHT;
|
||||
let pos = Vec2::new(0.0, -y);
|
||||
let y = i as f32 * Self::TAB_HEIGHT;
|
||||
let pos = Vec2::new(0.0, y);
|
||||
|
||||
let mut style = RectButtonStyle::default();
|
||||
style.radius = Self::HEAD_RADIUS;
|
||||
|
@ -280,7 +281,7 @@ impl TabMenu {
|
|||
}),
|
||||
);
|
||||
|
||||
let head_button_y = Self::HEAD_HEIGHT / 2.0;
|
||||
let head_button_y = -Self::HEAD_HEIGHT / 2.0;
|
||||
|
||||
let pop_out_x = Self::HEAD_BUTTON_MARGIN;
|
||||
let pop_out = Offset::new(pop_out, Vec2::new(pop_out_x, head_button_y));
|
||||
|
@ -295,8 +296,8 @@ impl TabMenu {
|
|||
let scroll_bar = Offset::new(scroll_bar, Vec2::new(scroll_x, -tab_list_height));
|
||||
|
||||
let separator_rect = Rect {
|
||||
bl: Vec2::new(Self::TAB_WIDTH, -tab_list_height),
|
||||
tr: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, 0.0),
|
||||
tl: Vec2::new(Self::TAB_WIDTH, 0.0),
|
||||
br: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, tab_list_height),
|
||||
};
|
||||
|
||||
let head_width = Self::TAB_WIDTH
|
||||
|
@ -306,13 +307,13 @@ impl TabMenu {
|
|||
+ scroll_bar.style.margin.x * 2.0;
|
||||
|
||||
let head_rect = Rect {
|
||||
bl: Vec2::ZERO,
|
||||
tr: Vec2::new(head_width, Self::HEAD_HEIGHT),
|
||||
tl: Vec2::new(0.0, -Self::HEAD_HEIGHT),
|
||||
br: Vec2::new(head_width, 0.0),
|
||||
};
|
||||
|
||||
let view_rect = Rect {
|
||||
bl: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, -tab_list_height),
|
||||
tr: Vec2::new(head_rect.tr.x, 0.0),
|
||||
tl: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, 0.0),
|
||||
br: Vec2::new(head_rect.br.x, tab_list_height),
|
||||
};
|
||||
|
||||
let view = ScrollView::new(
|
||||
|
@ -322,7 +323,7 @@ impl TabMenu {
|
|||
|available_width: f32| Inventory::new(available_width),
|
||||
);
|
||||
|
||||
let view = Offset::new(view, view_rect.bl);
|
||||
let view = Offset::new(view, view_rect.tl);
|
||||
|
||||
Self {
|
||||
pop_out,
|
||||
|
@ -348,20 +349,18 @@ impl Container for TabMenu {
|
|||
}
|
||||
|
||||
fn draw(&mut self, ctx: &DrawContext) {
|
||||
let head_color = Color::WHITE;
|
||||
|
||||
ctx.draw_partially_rounded_rect(
|
||||
CornerFlags::BOTTOM_RIGHT,
|
||||
self.separator_rect,
|
||||
Self::INNER_RADIUS,
|
||||
head_color,
|
||||
Self::HEAD_COLOR,
|
||||
);
|
||||
|
||||
ctx.draw_partially_rounded_rect(
|
||||
CornerFlags::TOP_LEFT | CornerFlags::TOP_RIGHT,
|
||||
CornerFlags::TOP,
|
||||
self.head_rect,
|
||||
Self::HEAD_RADIUS,
|
||||
head_color,
|
||||
Self::HEAD_COLOR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,9 @@ pub mod button;
|
|||
pub mod dialog;
|
||||
pub mod flex;
|
||||
pub mod menu;
|
||||
pub mod palette;
|
||||
pub mod scroll;
|
||||
pub mod slider;
|
||||
pub mod shell;
|
||||
pub mod text;
|
||||
|
||||
|
@ -75,6 +77,7 @@ impl<T: Container> Widget for T {
|
|||
pub mod prelude {
|
||||
pub use super::*;
|
||||
pub use crate::anim::Animation;
|
||||
pub use crate::style::{self, THEME};
|
||||
pub use canary_script::{*, api::*};
|
||||
pub use keyframe::functions::*;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,14 +18,14 @@ pub struct ScrollBarStyle {
|
|||
impl Default for ScrollBarStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
margin: Vec2::new(0.01, 0.01),
|
||||
body_radius: 0.005,
|
||||
body_width: 0.015,
|
||||
body_idle_color: Color(0x7f7f7fff),
|
||||
body_hover_color: Color(0xb0b0b0ff),
|
||||
body_selected_color: Color::MAGENTA,
|
||||
rail_width: 0.005,
|
||||
rail_color: Color(0xa0a0a07f),
|
||||
margin: Vec2::splat(2.0),
|
||||
body_radius: 1.0,
|
||||
body_width: 3.0,
|
||||
body_idle_color: THEME.palette.base,
|
||||
body_hover_color: THEME.palette.base_hover,
|
||||
body_selected_color: THEME.palette.base_active,
|
||||
rail_width: 1.0,
|
||||
rail_color: THEME.palette.base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,8 +50,8 @@ impl ScrollBar {
|
|||
pub fn new(height: f32, content_height: f32, style: ScrollBarStyle) -> Self {
|
||||
let center_x = style.body_width / 2.0 + style.margin.x;
|
||||
let rail_rect = Rect {
|
||||
bl: Vec2::new(center_x - style.rail_width / 2.0, style.margin.y),
|
||||
tr: Vec2::new(center_x + style.rail_width / 2.0, height - style.margin.y),
|
||||
tl: Vec2::new(center_x - style.rail_width / 2.0, style.margin.y),
|
||||
br: Vec2::new(center_x + style.rail_width / 2.0, height - style.margin.y),
|
||||
};
|
||||
|
||||
let body_color_anim = Animation::new(
|
||||
|
@ -80,7 +80,7 @@ impl ScrollBar {
|
|||
let style = &self.style;
|
||||
let rail_height = self.rail_rect.height();
|
||||
let body_height = (self.height / self.content_height) * rail_height;
|
||||
let body_y = rail_height - (self.scroll / self.content_height) * rail_height - body_height;
|
||||
let body_y = (self.scroll / self.content_height) * rail_height;
|
||||
let body_xy = Vec2::new(style.margin.x, body_y + style.margin.y);
|
||||
let body_size = Vec2::new(style.body_width, body_height);
|
||||
Rect::from_xy_size(body_xy, body_size)
|
||||
|
@ -128,7 +128,7 @@ impl Widget for ScrollBar {
|
|||
}
|
||||
|
||||
if kind == CursorEventKind::Drag && self.is_selected {
|
||||
self.scroll = ((self.grab_coord - at.y) / self.rail_rect.height())
|
||||
self.scroll = ((at.y - self.grab_coord) / self.rail_rect.height())
|
||||
* self.content_height
|
||||
+ self.grab_scroll;
|
||||
|
||||
|
@ -203,7 +203,7 @@ impl<T: Widget> ScrollView<T> {
|
|||
impl<T: Widget> Widget for ScrollView<T> {
|
||||
fn update(&mut self, dt: f32) {
|
||||
if self.scroll_bar.is_dirty() {
|
||||
let yoff = self.scroll_bar.get_scroll() - self.content_height + self.height;
|
||||
let yoff = -self.scroll_bar.get_scroll();
|
||||
self.inner.set_offset(Vec2::new(0.0, yoff));
|
||||
}
|
||||
|
||||
|
|
|
@ -132,8 +132,8 @@ impl<T: RectBounds> Offset<T> {
|
|||
vert_align: OffsetAlignment,
|
||||
) -> Self {
|
||||
let bounds = inner.get_bounds();
|
||||
let x = hori_align.align(bounds.bl.x, bounds.tr.x);
|
||||
let y = vert_align.align(bounds.tr.y, bounds.bl.y);
|
||||
let x = hori_align.align(bounds.tl.x, bounds.br.x);
|
||||
let y = vert_align.align(bounds.br.y, bounds.tl.y);
|
||||
let offset = anchor - Vec2::new(x, y);
|
||||
Self { inner, offset }
|
||||
}
|
||||
|
@ -180,8 +180,8 @@ impl<T: RectBounds> Popup<T> {
|
|||
vert_align: OffsetAlignment,
|
||||
) -> Self {
|
||||
let bounds = inner.get_bounds();
|
||||
let x = hori_align.align(bounds.bl.x, bounds.tr.x);
|
||||
let y = vert_align.align(bounds.tr.y, bounds.bl.y);
|
||||
let x = hori_align.align(bounds.tl.x, bounds.br.x);
|
||||
let y = vert_align.align(bounds.br.y, bounds.tl.y);
|
||||
let offset = anchor - Vec2::new(x, y);
|
||||
Self { inner, offset }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -53,6 +53,26 @@ impl Label {
|
|||
offset: Vec2::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_centered(text: LabelText, scale: f32, color: Color) -> Self {
|
||||
Self::new(
|
||||
text,
|
||||
HorizontalAlignment::Center,
|
||||
scale,
|
||||
color,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: &str) {
|
||||
if self.text.text != text {
|
||||
self.text.text = text.to_string();
|
||||
self.layout = None;
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Label {
|
||||
|
@ -62,14 +82,14 @@ impl Widget for Label {
|
|||
let bounds = Rect::from(layout.get_bounds()).scale(self.scale);
|
||||
self.bounds = bounds;
|
||||
let xoff = match self.alignment {
|
||||
HorizontalAlignment::Left => self.left - bounds.bl.x,
|
||||
HorizontalAlignment::Right => self.right - bounds.tr.x,
|
||||
HorizontalAlignment::Left => self.left - bounds.tl.x,
|
||||
HorizontalAlignment::Right => self.right - bounds.br.x,
|
||||
HorizontalAlignment::Center => {
|
||||
let available = self.right - self.left;
|
||||
let halfway = available / 2.0 + self.left;
|
||||
let width = bounds.tr.x - bounds.bl.x;
|
||||
let width = bounds.br.x - bounds.tl.x;
|
||||
let left = halfway - width / 2.0;
|
||||
left - bounds.bl.x
|
||||
left - bounds.tl.x
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -108,6 +128,14 @@ impl Icon {
|
|||
offset: Vec2::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: &str) {
|
||||
if self.text.text != text {
|
||||
self.text.text = text.to_string();
|
||||
self.layout = None;
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Icon {
|
||||
|
@ -117,7 +145,7 @@ impl Widget for Icon {
|
|||
let bounds = Rect::from(layout.get_bounds()).scale(self.scale);
|
||||
self.bounds = bounds;
|
||||
self.offset = self.center - bounds.size() / 2.0;
|
||||
self.offset.y -= bounds.bl.y;
|
||||
self.offset.y -= bounds.tl.y;
|
||||
self.dirty = false;
|
||||
self.layout = Some(layout);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
//! This module defines backends for WebAssembly execution.
|
||||
//!
|
||||
//! Canary is designed to support multiple WebAssembly runtimes for different
|
||||
|
@ -5,6 +8,8 @@
|
|||
//! implemented, but in the future, [wasm3](https://github.com/wasm3/wasm3)
|
||||
//! will also be provided.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub mod wasmtime;
|
||||
|
@ -19,7 +24,7 @@ pub fn make_default_backend() -> anyhow::Result<Box<dyn Backend>> {
|
|||
|
||||
/// A WebAssembly runtime backend.
|
||||
pub trait Backend {
|
||||
fn load_module(&self, 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.
|
||||
|
@ -33,10 +38,11 @@ pub trait Instance {
|
|||
/// Binds script data to a Canary panel.
|
||||
///
|
||||
/// To "bind" a Canary panel to a Canary script, this function must be
|
||||
/// called. It passes the ID of a panel to the script, plus an
|
||||
/// initialization message, and the script returns an integer as
|
||||
/// userdata. All panel events will be identified to the script with this
|
||||
/// userdata as the first argument.
|
||||
/// called. It passes the ID of a panel to the script, the name of the
|
||||
/// protocol that this panel will be using, plus an initialization
|
||||
/// message, and the script returns an integer as userdata. All panel
|
||||
/// events will be identified to the script with this userdata as the first
|
||||
/// argument.
|
||||
///
|
||||
/// The intended usecase for this userdata is to contain a pointer. A
|
||||
/// Canary script can allocate some high-level object in memory, and when
|
||||
|
@ -44,13 +50,140 @@ pub trait Instance {
|
|||
/// userdata. Then, when the runtime calls back into the script, the
|
||||
/// userdata will be reinterpreted as a pointer and a method can be called
|
||||
/// on that object in memory.
|
||||
fn bind_panel(&self, panel: PanelId, msg: Vec<u8>) -> u32;
|
||||
fn bind_panel(&self, panel: PanelId, protocol: &str, msg: Vec<u8>) -> u32;
|
||||
|
||||
fn update(&self, panel_ud: u32, dt: f32);
|
||||
|
||||
fn draw(&self, panel_ud: u32) -> Vec<DrawCommand>;
|
||||
|
||||
fn on_resize(&self, panel_ud: u32, new_size: Vec2);
|
||||
|
||||
fn on_cursor_event(&self, panel_ud: u32, kind: CursorEventKind, at: Vec2);
|
||||
|
||||
fn on_message(&self, panel_ud: u32, msg: Vec<u8>);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ScriptAbi {
|
||||
draw_cmds: Mutex<Vec<DrawCommand>>,
|
||||
font_store: Arc<text::FontStore>,
|
||||
font_families: Mutex<HashMap<String, u32>>,
|
||||
loaded_fonts: RwLock<Vec<Arc<text::Font>>>,
|
||||
text_layouts: RwLock<Slab<text::TextLayout>>,
|
||||
message_store: RwLock<Slab<Vec<u8>>>,
|
||||
panels: RwLock<Slab<PanelAbi>>,
|
||||
}
|
||||
|
||||
impl ScriptAbi {
|
||||
pub fn new(font_store: Arc<text::FontStore>) -> Self {
|
||||
Self {
|
||||
font_store,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
let mut lock = self.draw_cmds.lock();
|
||||
lock.clear();
|
||||
}
|
||||
|
||||
pub fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]) {
|
||||
self.draw_cmds.lock().push(DrawCommand::Mesh {
|
||||
vertices: vertices.to_vec(),
|
||||
indices: indices.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn draw_text_layout(&self, id: u32, offset: Vec2, scale: f32, color: Color) {
|
||||
// TODO multiple fonts per layout
|
||||
let layouts = self.text_layouts.read();
|
||||
let layout = layouts.get(id as usize).unwrap();
|
||||
let glyphs = layout.glyphs.as_slice();
|
||||
let loaded = self.loaded_fonts.read();
|
||||
let font = loaded.get(layout.font_id as usize).unwrap();
|
||||
let cmds = font.draw(glyphs, offset, scale, color);
|
||||
self.draw_cmds.lock().extend(cmds.into_iter());
|
||||
}
|
||||
|
||||
pub fn with_draw_commands(&self, f: impl FnOnce(&[DrawCommand])) {
|
||||
f(self.draw_cmds.lock().as_slice());
|
||||
}
|
||||
|
||||
pub fn font_load(&self, family: &str) -> u32 {
|
||||
let mut family_cache = self.font_families.lock();
|
||||
|
||||
if let Some(cached) = family_cache.get(family) {
|
||||
return *cached;
|
||||
}
|
||||
|
||||
let font = self.font_store.load_font(family);
|
||||
let mut loaded = self.loaded_fonts.write();
|
||||
let id = loaded.len() as u32;
|
||||
family_cache.insert(family.to_string(), id);
|
||||
loaded.push(font);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn text_layout_new(&self, font_id: u32, text: &str) -> u32 {
|
||||
let loaded = self.loaded_fonts.read();
|
||||
let font = loaded.get(font_id as usize).unwrap();
|
||||
let layout = font.shape(text);
|
||||
self.text_layouts.write().insert(layout) as u32
|
||||
}
|
||||
|
||||
pub fn text_layout_delete(&self, id: u32) {
|
||||
self.text_layouts.write().remove(id as usize);
|
||||
}
|
||||
|
||||
pub fn text_layout_get_bounds(&self, id: u32, dst: &mut Rect) {
|
||||
let src = self.text_layouts.read().get(id as usize).unwrap().bounds;
|
||||
let _ = std::mem::replace(dst, src);
|
||||
}
|
||||
|
||||
pub fn message_new(&self, data: Vec<u8>) -> u32 {
|
||||
let mut store = self.message_store.write();
|
||||
let id = store.insert(data) as u32;
|
||||
id
|
||||
}
|
||||
|
||||
pub fn message_free(&self, id: u32) {
|
||||
let mut store = self.message_store.write();
|
||||
store.remove(id as usize);
|
||||
}
|
||||
|
||||
pub fn message_get_len(&self, id: u32) -> u32 {
|
||||
self.message_store.read().get(id as usize).unwrap().len() as u32
|
||||
}
|
||||
|
||||
pub fn message_get_data(&self, id: u32, dst: &mut [u8]) {
|
||||
let store = self.message_store.read();
|
||||
let src = store.get(id as usize).unwrap();
|
||||
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,17 +1,20 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
use std::collections::{hash_map::DefaultHasher, HashMap};
|
||||
use std::hash::Hasher;
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use super::{Arc, Backend, Instance, PanelId};
|
||||
use crate::{DrawCommand, ScriptAbi, ScriptAbiImpl};
|
||||
use super::{Arc, Backend, Instance, PanelId, ScriptAbi};
|
||||
use crate::DrawCommand;
|
||||
|
||||
use canary_script::{Color, CursorEventKind, Rect, Vec2};
|
||||
use parking_lot::Mutex;
|
||||
use prehash::{DefaultPrehasher, Prehashed, Prehasher};
|
||||
|
||||
type Caller<'a> = wasmtime::Caller<'a, ScriptAbiImpl>;
|
||||
type Store = wasmtime::Store<ScriptAbiImpl>;
|
||||
type Linker = wasmtime::Linker<ScriptAbiImpl>;
|
||||
type Caller<'a> = wasmtime::Caller<'a, Arc<ScriptAbi>>;
|
||||
type Store = wasmtime::Store<Arc<ScriptAbi>>;
|
||||
type Linker = wasmtime::Linker<Arc<ScriptAbi>>;
|
||||
type ModuleCache = Mutex<HashMap<Prehashed<u64>, wasmtime::Module, DefaultPrehasher>>;
|
||||
|
||||
pub struct WasmtimeBackend {
|
||||
|
@ -37,7 +40,7 @@ impl WasmtimeBackend {
|
|||
}
|
||||
|
||||
impl Backend for WasmtimeBackend {
|
||||
fn load_module(&self, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>> {
|
||||
fn load_module(&self, abi: Arc<ScriptAbi>, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>> {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(module);
|
||||
let hashed = hasher.finish();
|
||||
|
@ -54,7 +57,6 @@ impl Backend for WasmtimeBackend {
|
|||
cache.get(&prehashed).unwrap()
|
||||
};
|
||||
|
||||
let abi = ScriptAbiImpl::default();
|
||||
let mut store = wasmtime::Store::new(&self.engine, abi);
|
||||
let mut linker = Linker::new(&self.engine);
|
||||
WasmtimeInstance::link(&mut linker)?;
|
||||
|
@ -62,6 +64,7 @@ impl Backend for WasmtimeBackend {
|
|||
let bind_panel = instance.get_typed_func(&mut store, "bind_panel")?;
|
||||
let update = instance.get_typed_func(&mut store, "update")?;
|
||||
let draw = instance.get_typed_func(&mut store, "draw")?;
|
||||
let on_resize = instance.get_typed_func(&mut store, "on_resize")?;
|
||||
let on_cursor_event = instance.get_typed_func(&mut store, "on_cursor_event")?;
|
||||
let on_message = instance.get_typed_func(&mut store, "on_message")?;
|
||||
|
||||
|
@ -70,6 +73,7 @@ impl Backend for WasmtimeBackend {
|
|||
bind_panel,
|
||||
update,
|
||||
draw,
|
||||
on_resize,
|
||||
on_cursor_event,
|
||||
on_message,
|
||||
};
|
||||
|
@ -82,9 +86,10 @@ impl Backend for WasmtimeBackend {
|
|||
|
||||
pub struct WasmtimeInstance {
|
||||
store: Mutex<Store>,
|
||||
bind_panel: wasmtime::TypedFunc<(u32, u32), u32>,
|
||||
bind_panel: wasmtime::TypedFunc<(u32, u32, u32), u32>,
|
||||
update: wasmtime::TypedFunc<(u32, f32), ()>,
|
||||
draw: wasmtime::TypedFunc<u32, ()>,
|
||||
on_resize: wasmtime::TypedFunc<(u32, f32, f32), ()>,
|
||||
on_cursor_event: wasmtime::TypedFunc<(u32, u32, f32, f32), ()>,
|
||||
on_message: wasmtime::TypedFunc<(u32, u32), ()>,
|
||||
}
|
||||
|
@ -167,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(())
|
||||
}
|
||||
|
||||
|
@ -209,11 +223,13 @@ impl WasmtimeInstance {
|
|||
}
|
||||
|
||||
impl Instance for WasmtimeInstance {
|
||||
fn bind_panel(&self, panel: PanelId, msg: Vec<u8>) -> u32 {
|
||||
fn bind_panel(&self, panel: PanelId, protocol: &str, msg: Vec<u8>) -> u32 {
|
||||
let mut store = self.store.lock();
|
||||
let protocol = store.data().message_new(protocol.as_bytes().to_vec());
|
||||
let msg = store.data().message_new(msg);
|
||||
let args = (panel.0 as u32, msg);
|
||||
let args = (panel.0 as u32, protocol, msg);
|
||||
let data = self.bind_panel.call(store.deref_mut(), args).unwrap();
|
||||
store.data().message_free(protocol);
|
||||
store.data().message_free(msg);
|
||||
data
|
||||
}
|
||||
|
@ -234,6 +250,13 @@ impl Instance for WasmtimeInstance {
|
|||
cmds
|
||||
}
|
||||
|
||||
fn on_resize(&self, panel_ud: u32, new_size: Vec2) {
|
||||
let mut store = self.store.lock();
|
||||
self.on_resize
|
||||
.call(store.deref_mut(), (panel_ud, new_size.x, new_size.y))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn on_cursor_event(&self, panel_ud: u32, kind: CursorEventKind, at: Vec2) {
|
||||
let mut store = self.store.lock();
|
||||
self.on_cursor_event
|
||||
|
|
150
src/lib.rs
150
src/lib.rs
|
@ -1,5 +1,5 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SDPX-License-Identifier: LGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
pub use canary_script::*;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
|
@ -10,24 +10,31 @@ use std::sync::Arc;
|
|||
pub mod backend;
|
||||
pub mod text;
|
||||
|
||||
use backend::{Backend, Instance};
|
||||
use backend::{Backend, Instance, ScriptAbi};
|
||||
use text::FontStore;
|
||||
|
||||
/// The main interface to Canary.
|
||||
pub struct Runtime {
|
||||
backend: Box<dyn Backend>,
|
||||
font_store: Arc<FontStore>,
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
pub fn new(backend: Box<dyn Backend>) -> anyhow::Result<Self> {
|
||||
Ok(Self { backend })
|
||||
Ok(Self {
|
||||
backend,
|
||||
font_store: Arc::new(FontStore::new()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_module(&self, module: &[u8]) -> anyhow::Result<Script> {
|
||||
let instance = self.backend.load_module(module)?;
|
||||
let abi = ScriptAbi::new(self.font_store.to_owned());
|
||||
let abi = Arc::new(abi);
|
||||
let instance = self.backend.load_module(abi.to_owned(), module)?;
|
||||
|
||||
Ok(Script {
|
||||
instance,
|
||||
next_panel: 0,
|
||||
abi,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -35,16 +42,16 @@ impl Runtime {
|
|||
/// A loaded instance of a Canary script.
|
||||
pub struct Script {
|
||||
instance: Arc<dyn Instance>,
|
||||
next_panel: usize,
|
||||
abi: Arc<ScriptAbi>,
|
||||
}
|
||||
|
||||
impl Script {
|
||||
pub fn create_panel(&mut self, msg: Vec<u8>) -> anyhow::Result<Panel> {
|
||||
let id = PanelId(self.next_panel);
|
||||
self.next_panel += 1;
|
||||
let userdata = self.instance.bind_panel(id, msg);
|
||||
pub fn create_panel(&mut self, protocol: &str, msg: Vec<u8>) -> anyhow::Result<Panel> {
|
||||
let id = self.abi.create_panel();
|
||||
let userdata = self.instance.bind_panel(id, protocol, msg);
|
||||
Ok(Panel {
|
||||
instance: self.instance.clone(),
|
||||
abi: self.abi.clone(),
|
||||
id,
|
||||
userdata,
|
||||
})
|
||||
|
@ -54,6 +61,7 @@ impl Script {
|
|||
/// A Canary panel.
|
||||
pub struct Panel {
|
||||
instance: Arc<dyn Instance>,
|
||||
abi: Arc<ScriptAbi>,
|
||||
id: PanelId,
|
||||
userdata: u32,
|
||||
}
|
||||
|
@ -67,6 +75,10 @@ impl Panel {
|
|||
self.instance.draw(self.userdata)
|
||||
}
|
||||
|
||||
pub fn on_resize(&self, new_size: Vec2) {
|
||||
self.instance.on_resize(self.userdata, new_size);
|
||||
}
|
||||
|
||||
pub fn on_cursor_event(&self, kind: CursorEventKind, at: Vec2) {
|
||||
self.instance.on_cursor_event(self.userdata, kind, at);
|
||||
}
|
||||
|
@ -74,33 +86,15 @@ impl Panel {
|
|||
pub fn on_message(&self, msg: Vec<u8>) {
|
||||
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).
|
||||
pub const PX_PER_MM: f32 = 25.4 / 96.0;
|
||||
|
||||
/// Low-level script API callbacks.
|
||||
///
|
||||
/// If you're a casual user of Canary the struct you're looking for is
|
||||
/// [ScriptAbiImpl]. This trait exists to help with making mocks for testing.
|
||||
pub trait ScriptAbi {
|
||||
fn start_draw(&self);
|
||||
fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]);
|
||||
fn draw_text_layout(&self, id: u32, offset: Vec2, scale: f32, color: Color);
|
||||
fn with_draw_commands(&self, f: impl FnOnce(&[DrawCommand]));
|
||||
|
||||
fn font_load(&self, family: &str) -> u32;
|
||||
|
||||
fn text_layout_new(&self, font_id: u32, text: &str) -> u32;
|
||||
fn text_layout_delete(&self, id: u32);
|
||||
fn text_layout_get_bounds(&self, id: u32, rect: &mut Rect);
|
||||
|
||||
fn message_new(&self, data: Vec<u8>) -> u32;
|
||||
fn message_free(&self, id: u32);
|
||||
fn message_get_len(&self, id: u32) -> u32;
|
||||
fn message_get_data(&self, id: u32, dst: &mut [u8]);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct PanelId(pub(crate) usize);
|
||||
|
||||
|
@ -112,95 +106,3 @@ pub enum DrawCommand {
|
|||
indices: Vec<MeshIndex>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The standard [ScriptAbi] implementation to use.
|
||||
#[derive(Default)]
|
||||
pub struct ScriptAbiImpl {
|
||||
draw_cmds: Mutex<Vec<DrawCommand>>,
|
||||
font_store: text::FontStore,
|
||||
font_families: Mutex<HashMap<String, u32>>,
|
||||
loaded_fonts: RwLock<Vec<Arc<text::Font>>>,
|
||||
text_layouts: RwLock<Slab<text::TextLayout>>,
|
||||
message_store: RwLock<Slab<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl ScriptAbi for ScriptAbiImpl {
|
||||
fn start_draw(&self) {
|
||||
let mut lock = self.draw_cmds.lock();
|
||||
lock.clear();
|
||||
}
|
||||
|
||||
fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]) {
|
||||
self.draw_cmds.lock().push(DrawCommand::Mesh {
|
||||
vertices: vertices.to_vec(),
|
||||
indices: indices.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
fn draw_text_layout(&self, id: u32, offset: Vec2, scale: f32, color: Color) {
|
||||
// TODO multiple fonts per layout
|
||||
let layouts = self.text_layouts.read();
|
||||
let layout = layouts.get(id as usize).unwrap();
|
||||
let glyphs = layout.glyphs.as_slice();
|
||||
let loaded = self.loaded_fonts.read();
|
||||
let font = loaded.get(layout.font_id as usize).unwrap();
|
||||
let cmds = font.draw(glyphs, offset, scale, color);
|
||||
self.draw_cmds.lock().extend(cmds.into_iter());
|
||||
}
|
||||
|
||||
fn with_draw_commands(&self, f: impl FnOnce(&[DrawCommand])) {
|
||||
f(self.draw_cmds.lock().as_slice());
|
||||
}
|
||||
|
||||
fn font_load(&self, family: &str) -> u32 {
|
||||
let mut family_cache = self.font_families.lock();
|
||||
|
||||
if let Some(cached) = family_cache.get(family) {
|
||||
return *cached;
|
||||
}
|
||||
|
||||
let font = self.font_store.load_font(family);
|
||||
let mut loaded = self.loaded_fonts.write();
|
||||
let id = loaded.len() as u32;
|
||||
family_cache.insert(family.to_string(), id);
|
||||
loaded.push(font);
|
||||
id
|
||||
}
|
||||
|
||||
fn text_layout_new(&self, font_id: u32, text: &str) -> u32 {
|
||||
let loaded = self.loaded_fonts.read();
|
||||
let font = loaded.get(font_id as usize).unwrap();
|
||||
let layout = font.shape(text);
|
||||
self.text_layouts.write().insert(layout) as u32
|
||||
}
|
||||
|
||||
fn text_layout_delete(&self, id: u32) {
|
||||
self.text_layouts.write().remove(id as usize);
|
||||
}
|
||||
|
||||
fn text_layout_get_bounds(&self, id: u32, dst: &mut Rect) {
|
||||
let src = self.text_layouts.read().get(id as usize).unwrap().bounds;
|
||||
let _ = std::mem::replace(dst, src);
|
||||
}
|
||||
|
||||
fn message_new(&self, data: Vec<u8>) -> u32 {
|
||||
let mut store = self.message_store.write();
|
||||
let id = store.insert(data) as u32;
|
||||
id
|
||||
}
|
||||
|
||||
fn message_free(&self, id: u32) {
|
||||
let mut store = self.message_store.write();
|
||||
store.remove(id as usize);
|
||||
}
|
||||
|
||||
fn message_get_len(&self, id: u32) -> u32 {
|
||||
self.message_store.read().get(id as usize).unwrap().len() as u32
|
||||
}
|
||||
|
||||
fn message_get_data(&self, id: u32, dst: &mut [u8]) {
|
||||
let store = self.message_store.read();
|
||||
let src = store.get(id as usize).unwrap();
|
||||
dst.copy_from_slice(src);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SDPX-License-Identifier: LGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
use super::{AllsortsFont, Rect, Vec2};
|
||||
|
||||
|
@ -133,26 +133,8 @@ impl OutlineSink {
|
|||
|
||||
fn pf_vector_to_lyon(&mut self, v: &Vector2F) -> lyon::geom::Point<f32> {
|
||||
let point = lyon::geom::Point::<f32>::new(v.x(), -v.y()) / self.units_per_em;
|
||||
|
||||
// TODO clean this up with helper math methods?
|
||||
let bb = &mut self.bounding_box;
|
||||
|
||||
if point.x < bb.bl.x {
|
||||
bb.bl.x = point.x;
|
||||
}
|
||||
|
||||
if point.x > bb.tr.x {
|
||||
bb.tr.x = point.x;
|
||||
}
|
||||
|
||||
if point.y < bb.tr.y {
|
||||
bb.tr.y = point.y;
|
||||
}
|
||||
|
||||
if point.y > bb.bl.y {
|
||||
bb.bl.y = point.y;
|
||||
}
|
||||
|
||||
let glam_point = Vec2::new(point.x, point.y);
|
||||
self.bounding_box = self.bounding_box.union_point(glam_point);
|
||||
point
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SDPX-License-Identifier: LGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
use super::{Color, DrawCommand, MeshIndex, MeshVertex, Rect, Vec2};
|
||||
|
||||
|
@ -73,31 +73,9 @@ impl Font {
|
|||
xcur += position.hori_advance;
|
||||
ycur += position.vert_advance;
|
||||
|
||||
let xpos = xpos as f32 / units_per_em;
|
||||
let ypos = ypos as f32 / units_per_em;
|
||||
|
||||
let mut bb = glyphs.get(position.index as usize).unwrap().bounding_box;
|
||||
bb.bl.x = bb.bl.x + xpos;
|
||||
bb.bl.y = bb.bl.y + ypos;
|
||||
bb.tr.x = bb.tr.x + xpos;
|
||||
bb.tr.y = bb.tr.y + ypos;
|
||||
|
||||
// TODO use euclid instead
|
||||
if bounds.bl.x > bb.bl.x {
|
||||
bounds.bl.x = bb.bl.x;
|
||||
}
|
||||
|
||||
if bounds.bl.y > bb.bl.y {
|
||||
bounds.bl.y = bb.bl.y;
|
||||
}
|
||||
|
||||
if bounds.tr.x < bb.tr.x {
|
||||
bounds.tr.x = bb.tr.x;
|
||||
}
|
||||
|
||||
if bounds.tr.y < bb.tr.y {
|
||||
bounds.tr.y = bb.tr.y;
|
||||
}
|
||||
let pos = Vec2::new(xpos as f32, ypos as f32) / units_per_em;
|
||||
let bb = glyphs.get(position.index as usize).unwrap().bounding_box;
|
||||
bounds = bounds.union(&bb.offset(pos));
|
||||
}
|
||||
|
||||
TextLayout {
|
||||
|
|
Loading…
Reference in New Issue