Compare commits

..

13 Commits

80 changed files with 1499 additions and 2931 deletions

View File

@ -2,10 +2,9 @@
members = [
"apps/magpie",
"apps/music-player",
"apps/notifications",
"apps/sandbox",
"crates/script",
"crates/textwrap",
"scripts/force-directed-graph",
"scripts/music-player",
"scripts/sao-ui",
]
@ -21,13 +20,11 @@ allsorts = "0.10"
anyhow = "1"
bytemuck = "1"
canary-script = { path = "crates/script" }
log = "0.4"
lyon = "1"
ouroboros = "^0.15"
parking_lot = "0.12"
prehash = "0.3.3"
slab = "0.4"
wasmtime = "3"
wasmtime = "0.38"
[dependencies.font-kit]
version = "*"

View File

@ -2,7 +2,6 @@
name = "canary-magpie"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-or-later"
[[bin]]
name = "magpie"
@ -13,10 +12,7 @@ required-features = ["service"]
anyhow = { version = "1", optional = true }
byteorder = "1.4"
canary = { path = "../..", optional = true }
env_logger = { version = "0.10", optional = true }
futures-util = { version = "0.3", optional = true, features = ["io"] }
glium = { version = "0.32", optional = true}
log = "0.4"
mio = { version = "0.8", features = ["net", "os-poll"], optional = true }
mio-signals = { version = "0.2", optional = true }
parking_lot = { version = "0.12", optional = true}
@ -25,5 +21,4 @@ serde_json = "1"
slab = { version = "0.4", optional = true}
[features]
async = ["dep:futures-util"]
service = ["dep:anyhow", "dep:canary", "dep:env_logger", "dep:glium", "dep:mio", "dep:mio-signals", "dep:parking_lot", "dep:slab"]
service = ["dep:anyhow", "dep:canary", "dep:glium", "dep:mio", "dep:mio-signals", "dep:parking_lot", "dep:slab"]

41
apps/magpie/src/client.rs Normal file
View File

@ -0,0 +1,41 @@
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));
}
}

View File

@ -1,6 +1,4 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod client;
pub mod protocol;
#[cfg(feature = "service")]

View File

@ -1,6 +1,3 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use glium::glutin::event_loop::EventLoopBuilder;
use canary_magpie::service::*;
@ -8,12 +5,6 @@ use ipc::Ipc;
use window::{WindowMessage, WindowStore};
fn main() -> std::io::Result<()> {
env_logger::Builder::new()
.filter(None, log::LevelFilter::Info) // By default logs all info messages.
.parse_default_env()
.init();
log::info!("Initializing Magpie...");
let event_loop = EventLoopBuilder::<WindowMessage>::with_user_event().build();
let window_sender = event_loop.create_proxy();
let (ipc, ipc_sender) = Ipc::new(window_sender)?;

View File

@ -1,10 +1,7 @@
// 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;
use std::path::{PathBuf, Path};
use std::path::PathBuf;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
@ -24,15 +21,7 @@ pub type PanelId = u32;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CreatePanel {
pub id: PanelId,
pub protocol: String,
pub script: PathBuf,
pub init_msg: Vec<u8>,
}
/// Closes a Magpie panel with a given ID.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ClosePanel {
pub id: PanelId,
}
/// Sends a panel a message.
@ -47,61 +36,74 @@ pub struct SendMessage {
#[serde(tag = "kind")]
pub enum MagpieServerMsg {
CreatePanel(CreatePanel),
ClosePanel(ClosePanel),
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 {
RecvMessage(RecvMessage),
}
pub enum MagpieClientMsg {}
/// 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();
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>;
/// Piecewise packet assembler for [Messenger].
pub struct MessageQueue<I> {
/// Bidirectional, transport-agnostic Magpie IO wrapper struct.
pub struct Messenger<T, I, O> {
pub transport: T,
expected_len: Option<usize>,
received_buf: VecDeque<u8>,
received_queue: VecDeque<I>,
closed: bool,
_output: PhantomData<O>,
}
impl<I> Default for MessageQueue<I> {
fn default() -> Self {
impl<T: Read + Write, I: DeserializeOwned, O: Serialize> Messenger<T, I, O> {
pub fn new(transport: T) -> Self {
Self {
transport,
expected_len: None,
received_buf: Default::default(),
received_queue: Default::default(),
closed: false,
_output: PhantomData,
}
}
}
impl<I: DeserializeOwned> MessageQueue<I> {
pub fn on_data(&mut self, data: &[u8]) -> std::io::Result<()> {
self.received_buf.write_all(data)?;
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),
}
}
loop {
if let Some(expected_len) = self.expected_len {
@ -133,146 +135,8 @@ impl<I: DeserializeOwned> MessageQueue<I> {
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()
}
}
/// Acquires the path to the Magpie socket.
///
/// Currently only joins XDG_RUNTIME_DIR with [MAGPIE_SOCK].
pub fn find_socket() -> PathBuf {
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);
sock_path
}
#[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();
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])?;
}
}
}
}

View File

@ -1,8 +1,5 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use canary::{DrawCommand, Vec2, PX_PER_MM};
use glium::{program::ProgramCreationInput, Surface};
use glium::Surface;
#[derive(Copy, Clone)]
pub struct Vertex {
@ -59,21 +56,9 @@ pub struct Graphics {
impl Graphics {
pub fn new(display: glium::Display) -> Self {
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();
let program =
glium::Program::from_source(&display, VERTEX_SHADER_SRC, FRAGMENT_SHADER_SRC, None)
.unwrap();
Self { display, program }
}
@ -117,7 +102,7 @@ impl Graphics {
};
let mut target = self.display.draw();
target.clear_color(0.0, 0.0, 0.0, 0.0);
target.clear_color(0.0, 0.0, 0.0, 1.0);
target
.draw(
&vertex_buffer,

View File

@ -1,6 +1,3 @@
// 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};
@ -14,29 +11,18 @@ use mio_signals::{Signal, Signals};
use parking_lot::RwLock;
use slab::Slab;
use crate::protocol::*;
use crate::protocol::{CreatePanel, MagpieServerMsg, SendMessage, ServerMessenger};
use crate::service::window::{WindowMessage, WindowMessageSender};
const SOCK_NAME: &str = "magpie.sock";
#[derive(Debug)]
pub enum IpcMessage {
PanelMessage { window: usize, message: Vec<u8> },
}
pub enum IpcMessage {}
#[derive(Clone)]
pub struct IpcMessageSender {
waker: Arc<Waker>,
waker: 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,
@ -47,7 +33,7 @@ impl Drop for Listener {
fn drop(&mut self) {
match std::fs::remove_file(&self.path) {
Ok(_) => {}
Err(e) => log::error!("Could not delete UnixListener {:?}", e),
Err(e) => eprintln!("Could not delete UnixListener {:?}", e),
}
}
}
@ -66,38 +52,8 @@ impl DerefMut for Listener {
}
}
impl Listener {
fn new() -> std::io::Result<Self> {
let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set");
let sock_dir = Path::new(&sock_dir);
let sock_path = sock_dir.join(SOCK_NAME);
use std::io::{Error, ErrorKind};
match UnixStream::connect(&sock_path) {
Ok(_) => {
log::warn!("Socket is already in use. Another instance of Magpie may be running.");
let kind = ErrorKind::AddrInUse;
let error = Error::new(kind, "Socket is already in use.");
return Err(error);
}
Err(ref err) if err.kind() == ErrorKind::ConnectionRefused => {
log::warn!("Found leftover socket; removing.");
std::fs::remove_file(&sock_path)?;
}
Err(ref err) if err.kind() == ErrorKind::NotFound => {}
Err(err) => return Err(err),
}
log::info!("Making socket at: {:?}", sock_path);
let uds = UnixListener::bind(&sock_path)?;
let path = sock_path.to_path_buf();
Ok(Self { uds, path })
}
}
pub struct IpcData {
poll: Poll,
window_to_client_panel: HashMap<usize, (usize, PanelId)>,
next_window_id: usize,
}
@ -117,46 +73,40 @@ 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> {
if let Err(err) = self.messenger.flush_recv() {
log::error!("flush_recv() error: {:?}", err);
}
self.messenger.flush_recv()?;
while let Some(msg) = self.messenger.try_recv() {
log::debug!("Client #{}: {:?}", self.token.0, msg);
while let Some(msg) = self.messenger.recv() {
println!("Client #{}: {:?}", self.token.0, msg);
match msg {
MagpieServerMsg::CreatePanel(CreatePanel {
id,
protocol,
script,
init_msg,
}) => {
let mut data = self.data.write();
let window = data.new_window_id();
data.window_to_client_panel
.insert(window, (self.token.0, id));
MagpieServerMsg::CreatePanel(CreatePanel { id, script }) => {
let window = self.data.write().new_window_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,
protocol,
script,
init_msg,
};
let msg = WindowMessage::OpenWindow { id: window, script };
let _ = self.window_sender.send_event(msg);
}
MagpieServerMsg::ClosePanel(ClosePanel { id }) => {
if let Some(id) = self.id_to_window.get(&id).copied() {
let msg = WindowMessage::CloseWindow { id };
let _ = self.window_sender.send_event(msg);
}
}
MagpieServerMsg::SendMessage(SendMessage { id, msg }) => {
if let Some(id) = self.id_to_window.get(&id).cloned() {
let msg = WindowMessage::SendMessage { id, msg };
@ -168,26 +118,13 @@ impl Client {
Ok(self.messenger.is_closed())
}
pub fn disconnect(mut self) {
log::info!("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,
@ -199,10 +136,19 @@ pub struct Ipc {
impl Ipc {
pub fn new(window_sender: WindowMessageSender) -> std::io::Result<(Self, IpcMessageSender)> {
let mut listener = Listener::new()?;
let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set");
let sock_dir = Path::new(&sock_dir);
let sock_path = sock_dir.join(SOCK_NAME);
eprintln!("Making socket at: {:?}", sock_path);
let mut listener = Listener {
uds: UnixListener::bind(&sock_path)?,
path: sock_path.to_path_buf(),
};
let mut 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);
@ -216,13 +162,12 @@ impl Ipc {
let (sender, message_recv) = channel();
let sender = IpcMessageSender {
waker: Arc::new(Waker::new(registry, message_recv_token)?),
waker: Waker::new(registry, message_recv_token)?,
sender,
};
let data = IpcData {
poll,
window_to_client_panel: HashMap::new(),
next_window_id: 0,
};
@ -230,6 +175,7 @@ impl Ipc {
data: Arc::new(RwLock::new(data)),
window_sender,
message_recv,
events,
quit: false,
listener,
signals,
@ -242,38 +188,18 @@ impl Ipc {
Ok((ipc, sender))
}
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))?;
}
}
pub fn poll(&mut self, timeout: Option<Duration>) -> std::io::Result<()> {
self.data.write().poll.poll(&mut self.events, timeout)?;
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() {
for event in self.events.iter() {
if event.token() == self.listener_token {
loop {
match self.listener.accept() {
Ok((mut connection, address)) => {
let token = Token(self.clients.vacant_key());
log::info!(
println!(
"Accepting connection (Client #{}) from {:?}",
token.0,
address
token.0, address
);
let interest = Interest::READABLE;
@ -297,21 +223,17 @@ impl Ipc {
}
} else if event.token() == self.signals_token {
while let Some(received) = self.signals.receive()? {
log::info!("Received {:?} signal; exiting...", received);
eprintln!("Received {:?} signal; exiting...", received);
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).disconnect();
self.clients.remove(event.token().0);
}
} else {
log::error!("Unrecognized event token: {:?}", event);
panic!("Unrecognized event token: {:?}", event);
}
}
@ -319,13 +241,12 @@ 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(&mut events, Some(wait)) {
match self.poll(Some(wait)) {
Ok(_) => {}
Err(e) => {
log::error!("IPC poll error: {:?}", e);
eprintln!("IPC poll error: {:?}", e);
}
}
}

View File

@ -1,6 +1,3 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod gl;
pub mod ipc;
pub mod window;

View File

@ -1,6 +1,3 @@
// 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;
@ -17,58 +14,36 @@ use crate::service::ipc::{IpcMessage, IpcMessageSender};
#[derive(Clone, Debug)]
pub enum WindowMessage {
OpenWindow {
id: usize,
protocol: String,
script: PathBuf,
init_msg: Vec<u8>,
},
CloseWindow {
id: usize,
},
OpenWindow { id: usize, 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()
.with_transparent(true)
.with_decorations(false);
let cb = glutin::ContextBuilder::new()
.with_vsync(true)
.with_multisampling(4);
let wb = glutin::window::WindowBuilder::new();
let cb = glutin::ContextBuilder::new();
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,
})
}
@ -80,38 +55,24 @@ 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) {
@ -124,15 +85,6 @@ impl Window {
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,
@ -140,18 +92,11 @@ impl Window {
..
} => {
let event = match state {
ElementState::Pressed => {
self.cursor_down = true;
CursorEventKind::Select
}
ElementState::Released => {
self.cursor_down = false;
CursorEventKind::Deselect
}
ElementState::Pressed => CursorEventKind::Select,
ElementState::Released => CursorEventKind::Deselect,
};
self.panel.on_cursor_event(event, self.cursor_pos);
self.recv_messages();
}
_ => {}
}
@ -191,24 +136,15 @@ impl WindowStore {
message: WindowMessage,
) -> anyhow::Result<bool> {
match message {
WindowMessage::OpenWindow {
id,
protocol,
script,
init_msg,
} => {
log::debug!("Opening window {} with script {:?}...", id, script);
let start = std::time::Instant::now();
WindowMessage::OpenWindow { id, script } => {
println!("Opening window {} with script {:?}", id, script);
let module = std::fs::read(script)?;
let mut script = self.runtime.load_module(&module)?;
log::debug!("Instantiated window {} script in {:?}", id, start.elapsed());
let panel = script.create_panel(&protocol, init_msg)?;
log::debug!("Created window {} panel in {:?}", id, start.elapsed());
let window = Window::new(self.ipc_sender.to_owned(), id, panel, &event_loop)?;
let panel = script.create_panel(vec![])?;
let window = Window::new(panel, &event_loop)?;
let window_id = window.get_id();
self.windows.insert(window_id, window);
self.ipc_to_window.insert(id, window_id);
log::debug!("Opened window {} in {:?}", id, start.elapsed());
}
WindowMessage::CloseWindow { id } => {
if let Some(window_id) = self.ipc_to_window.remove(&id) {
@ -248,7 +184,7 @@ impl WindowStore {
Ok(false) => {}
Ok(true) => *control_flow = ControlFlow::Exit,
Err(err) => {
log::error!("Error while handling message {:?}:\n{}", event, err);
eprintln!("Error while handling message {:?}:\n{}", event, err);
}
},
_ => {}

View File

@ -2,7 +2,6 @@
name = "canary-music-player"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-or-later"
[[bin]]
name = "canary-music-player"
@ -10,12 +9,10 @@ path = "src/main.rs"
required-features = ["bin"]
[dependencies]
async-std = { version = "1.12", optional = true, features = ["attributes"] }
canary-magpie = { path = "../magpie", optional = true, features = ["async"] }
futures-util = { version = "0.3", optional = true }
canary-magpie = { path = "../magpie", optional = true }
mpris = { version = "2.0.0-rc3", optional = true }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
zbus = { version = "3.5", optional = true }
[features]
bin = ["dep:async-std", "dep:canary-magpie", "dep:futures-util", "dep:zbus"]
bin = ["dep:canary-magpie", "dep:mpris"]

View File

@ -1,12 +1,9 @@
// 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(Copy, Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum PlaybackStatus {
/// A track is currently playing.
Playing,
@ -18,7 +15,7 @@ pub enum PlaybackStatus {
Stopped,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum LoopStatus {
/// The playback will stop when there are no more tracks to play.
None,
@ -34,6 +31,9 @@ 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)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct TrackInfo {
/// The title of the current track.
pub title: Option<String>,
@ -55,9 +55,6 @@ 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)]
@ -104,6 +101,6 @@ pub enum OutMsg {
/// Sets the volume. Values are clamped to 0.0 to 1.0.
SetVolume { volume: f32 },
/// Seeks the current track's position in seconds.
Seek { offset: f32 },
/// Set the current track position in seconds.
SetPosition { position: f32 },
}

View File

@ -1,214 +1,77 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use canary_magpie::protocol::{
ClientMessenger, CreatePanel, MagpieClientMsg, MagpieServerMsg, RecvMessage, MAGPIE_SOCK,
};
use canary_music_player::*;
use canary_magpie::client::MagpieClient;
use canary_magpie::protocol::{CreatePanel, MagpieServerMsg};
use mpris::PlayerFinder;
use async_std::os::unix::net::UnixStream;
pub type MagpieClient = ClientMessenger<UnixStream>;
pub mod mpris;
use mpris::*;
#[derive(Debug)]
pub struct Metadata {
pub struct MetadataTracker {
pub album: AlbumInfo,
pub track: TrackInfo,
}
impl<'a> From<MetadataMap<'a>> for Metadata {
fn from(map: MetadataMap<'a>) -> Self {
impl From<mpris::Metadata> for MetadataTracker {
fn from(metadata: mpris::Metadata) -> Self {
let album = AlbumInfo {
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()),
title: metadata.album_name().map(ToString::to_string),
artists: metadata
.album_artists()
.unwrap_or(Vec::new())
.iter()
.map(ToString::to_string)
.collect(),
};
let track = TrackInfo {
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
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(),
};
Self { album, track }
}
}
impl Metadata {
pub async fn update_new(magpie: &mut MagpieClient, metadata: MetadataMap<'_>) -> Self {
impl MetadataTracker {
pub fn new(magpie: &mut MagpieClient, metadata: mpris::Metadata) -> Self {
let new: Self = metadata.into();
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;
magpie.send_json_message(0, &InMsg::AlbumChanged(new.album.clone()));
magpie.send_json_message(0, &InMsg::TrackChanged(new.track.clone()));
new
}
pub async fn update_diff(&mut self, messenger: &mut MagpieClient, metadata: MetadataMap<'_>) {
pub fn update(&mut self, messenger: &mut MagpieClient, metadata: mpris::Metadata) {
let new: Self = metadata.into();
if self.album != new.album {
let msg = InMsg::AlbumChanged(new.album.clone());
messenger.send_panel_json_async(0, &msg).await;
messenger.send_json_message(0, &InMsg::AlbumChanged(new.album.clone()));
}
if self.track != new.track {
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;
messenger.send_json_message(0, &InMsg::TrackChanged(new.track.clone()));
}
*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::{FutureExt, 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;
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(())
}
#[async_std::main]
async fn main() {
fn main() {
let args: Vec<String> = std::env::args().collect();
let module_path = args
.get(1)
.expect("Please pass a path to a Canary script!")
.to_owned();
let sock_path = canary_magpie::protocol::find_socket();
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 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,
protocol,
script,
init_msg: vec![],
};
let msg = CreatePanel { id: 0, script };
let msg = MagpieServerMsg::CreatePanel(msg);
magpie.send_async(&msg).await.unwrap();
let dbus = zbus::Connection::session().await.unwrap();
magpie.messenger.send(&msg).unwrap();
let mut first_loop = true;
let mut connected = false;
@ -216,7 +79,7 @@ async fn main() {
loop {
if !first_loop {
let wait = std::time::Duration::from_secs(1);
async_std::task::sleep(wait).await;
std::thread::sleep(wait);
}
first_loop = false;
@ -224,36 +87,81 @@ async fn main() {
if connected {
println!("Disconnected from MPRIS");
let msg = InMsg::Disconnected;
magpie.send_panel_json_async(0, &msg).await;
magpie.send_json_message(0, &msg);
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;
}
let player = match player_finder.find_active() {
Ok(player) => player,
Err(err) => {
eprintln!("D-Bus error while finding player: {:?}", err);
return;
eprintln!("Couldn't find player: {:?}", err);
continue;
}
};
println!(
"Connected to \"{}\" ({})",
player.path().as_str(),
player.destination().as_str()
player.identity(),
player.bus_name()
);
connected = true;
magpie.send_panel_json_async(0, &InMsg::Connected).await;
magpie.send_json_message(0, &InMsg::Connected);
match player_main(&player, &mut magpie).await {
Ok(()) => {}
let metadata = player.get_metadata().unwrap();
let mut metadata_tracker = MetadataTracker::new(&mut magpie, metadata);
let mut events = match player.events() {
Ok(events) => events,
Err(err) => {
eprintln!("D-Bus error while connected to player: {:?}", err);
eprintln!("Player events D-Bus error: {:?}", err);
continue;
}
};
loop {
let event = match events.next() {
None => break,
Some(Ok(e)) => e,
Some(Err(err)) => {
eprintln!("D-Bus error while reading player events: {:?}", err);
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
}
};
if let Some(msg) = in_msg {
magpie.send_json_message(0, &msg);
}
}
}

View File

@ -1,51 +0,0 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::collections::HashMap;
use zbus::fdo::DBusProxy;
use zbus::zvariant::Value;
use zbus::{dbus_proxy, Connection, Result};
pub type MetadataMap<'a> = HashMap<String, Value<'a>>;
#[dbus_proxy(
interface = "org.mpris.MediaPlayer2.Player",
default_path = "/org/mpris/MediaPlayer2"
)]
trait Player {
fn next(&self) -> Result<()>;
fn previous(&self) -> Result<()>;
fn pause(&self) -> Result<()>;
fn play_pause(&self) -> Result<()>;
fn stop(&self) -> Result<()>;
fn play(&self) -> Result<()>;
fn seek(&self, offset: i64) -> Result<()>;
#[dbus_proxy(property)]
fn playback_status(&self) -> Result<String>;
#[dbus_proxy(property)]
fn position(&self) -> Result<i64>;
#[dbus_proxy(property)]
fn metadata(&self) -> Result<MetadataMap>;
}
pub async fn find_player(connection: &Connection) -> Result<Option<PlayerProxy>> {
let dbus = DBusProxy::new(connection).await?;
let names = dbus.list_names().await?;
for name in names {
let name = name.as_str().to_string();
if name.starts_with("org.mpris.MediaPlayer2") {
let player = PlayerProxy::builder(connection)
.destination(name)?
.build()
.await?;
return Ok(Some(player));
}
}
Ok(None)
}

View File

@ -1,19 +0,0 @@
[package]
name = "canary-notifications"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "canary-notifications"
path = "src/main.rs"
required-features = ["bin"]
[dependencies]
async-std = { version = "1.12", optional = true, features = ["attributes"] }
canary-magpie = { path = "../magpie", optional = true, features = ["async"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
zbus = { version = "3.5", optional = true }
[features]
bin = ["dep:async-std", "dep:canary-magpie", "dep:zbus"]

View File

@ -1,27 +0,0 @@
// 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)]
pub struct Contents {
/// The optional name of the application sending the notification.
pub app_name: Option<String>,
/// The summary text briefly describing the notification.
pub summary: String,
/// The optional detailed body text.
pub body: Option<String>,
/// The timeout time in milliseconds since the display of the notification
/// at which the notification should automatically close.
pub timeout: Option<i32>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum OutMsg {}

View File

@ -1,143 +0,0 @@
use std::collections::HashMap;
use std::future::pending;
use std::path::PathBuf;
use async_std::channel::{unbounded, Sender};
use async_std::os::unix::net::UnixStream;
use canary_magpie::protocol::*;
use canary_notifications::Contents;
use zbus::{dbus_interface, zvariant::Value, ConnectionBuilder, SignalContext};
pub type MagpieClient = ClientMessenger<UnixStream>;
pub struct Notifications {
module_path: PathBuf,
magpie_sender: Sender<MagpieServerMsg>,
next_id: u32,
}
#[dbus_interface(name = "org.freedesktop.Notifications")]
impl Notifications {
fn get_capabilities(&self) -> Vec<String> {
vec!["body", "body-markup", "actions", "icon-static"]
.into_iter()
.map(ToString::to_string)
.collect()
}
#[dbus_interface(out_args("name", "vendor", "version", "spec_version"))]
fn get_server_information(&self) -> zbus::fdo::Result<(String, String, String, String)> {
Ok((
"canary-notifications".to_string(),
"Canary Development Team".to_string(),
"0.1.0".to_string(),
"1.2".to_string(),
))
}
async fn notify(
&mut self,
app_name: String,
replaces_id: u32,
app_icon: String,
summary: String,
body: String,
actions: Vec<String>,
hints: HashMap<String, Value<'_>>,
timeout: i32,
) -> u32 {
let timeout = match timeout {
-1 => Some(5000), // default timeout
0 => None,
t => Some(t),
};
let contents = Contents {
app_name: Some(app_name).filter(|s| !s.is_empty()),
summary,
body: Some(body).filter(|s| !s.is_empty()),
timeout,
};
let id = self.next_id;
self.next_id += 1;
let msg = CreatePanel {
id,
protocol: "tebibyte-media.desktop.notification".to_string(),
script: self.module_path.to_owned(),
init_msg: serde_json::to_vec(&contents).unwrap(),
};
if let Some(delay_ms) = contents.timeout.clone() {
let delay = std::time::Duration::from_millis(delay_ms as _);
let magpie_sender = self.magpie_sender.to_owned();
async_std::task::spawn(async move {
async_std::task::sleep(delay).await;
magpie_sender
.send(MagpieServerMsg::ClosePanel(ClosePanel { id }))
.await
.unwrap();
});
}
self.magpie_sender
.send(MagpieServerMsg::CreatePanel(msg))
.await
.unwrap();
id
}
fn close_notification(&self, id: u32) {}
#[dbus_interface(signal)]
async fn notification_closed(ctx: &SignalContext<'_>, id: u32, reason: u32)
-> zbus::Result<()>;
#[dbus_interface(signal)]
async fn action_invoked(
ctx: &SignalContext<'_>,
id: u32,
action_key: String,
) -> zbus::Result<()>;
}
#[async_std::main]
async fn main() {
let args: Vec<String> = std::env::args().collect();
let module_path = args
.get(1)
.expect("Please pass a path to a Canary script!")
.to_owned()
.into();
let sock_path = find_socket();
let socket = UnixStream::connect(sock_path).await.unwrap();
let mut magpie = MagpieClient::new(socket);
let (magpie_sender, magpie_receiver) = unbounded();
let notifications = Notifications {
magpie_sender,
next_id: 0,
module_path,
};
let _ = ConnectionBuilder::session()
.unwrap()
.name("org.freedesktop.Notifications")
.unwrap()
.serve_at("/org/freedesktop/Notifications", notifications)
.unwrap()
.build()
.await
.unwrap();
async_std::task::spawn(async move {
while let Ok(msg) = magpie_receiver.recv().await {
magpie.send_async(&msg).await.unwrap();
}
});
pending::<()>().await;
}

View File

@ -1,7 +1,7 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use canary::{CursorEventKind, Panel, Runtime, Script, PX_PER_MM};
use canary::{CursorEventKind, Panel, Runtime, Script};
use eframe::egui;
use std::time::Instant;
@ -31,9 +31,7 @@ struct App {
panels: Vec<PanelWindow>,
next_idx: usize,
last_update: Instant,
protocol_buf: String,
bind_message_buf: String,
panel_bg: egui::Color32,
}
impl App {
@ -48,9 +46,7 @@ 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,
}
}
}
@ -60,18 +56,12 @@ 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(&self.protocol_buf, msg).unwrap();
let panel = self.script.create_panel(msg).unwrap();
let index = self.next_idx;
self.next_idx += 1;
@ -80,19 +70,10 @@ 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();
@ -100,7 +81,7 @@ impl eframe::App for App {
for panel in self.panels.iter_mut() {
panel.panel.update(dt);
panel.show(self.panel_bg, ctx);
panel.show(ctx);
}
}
}
@ -110,91 +91,80 @@ 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, bg: egui::Color32, ctx: &egui::Context) {
let frame = egui::Frame::window(&ctx.style()).fill(bg);
pub fn show(&mut self, ctx: &egui::Context) {
let window_id = egui::Id::new(format!("panel_{}", self.index));
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");
});
egui::Window::new("Panel").id(window_id).show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.checkbox(&mut self.show_msg, "Show Message Editor");
});
let sense = egui::Sense {
click: true,
drag: true,
focusable: true,
let 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 desired_size = ui.available_size();
let response = ui.allocate_response(desired_size, sense);
let rect = response.rect;
self.panel.on_cursor_event(kind, pos);
}
if rect.size() != self.current_size {
let size = rect.size();
self.current_size = size;
let texture = egui::TextureId::Managed(0);
let uv = egui::pos2(0.0, 0.0);
let mut mesh = egui::Mesh::with_texture(texture);
let size = canary::Vec2::new(size.x, size.y);
self.panel.on_resize(size * PX_PER_MM);
}
let commands = self.panel.draw();
for command in commands.into_iter() {
let voff = mesh.vertices.len() as u32;
if let Some(hover_pos) = response.hover_pos() {
let local = (hover_pos - rect.left_top()) * PX_PER_MM;
let pos = canary::Vec2::new(local.x, local.y);
let kind = if response.drag_started() {
CursorEventKind::Select
} else if response.drag_released() {
CursorEventKind::Deselect
} else if response.dragged() {
CursorEventKind::Drag
} else {
CursorEventKind::Hover
};
self.panel.on_cursor_event(kind, pos);
}
let 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);
}
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!(),
}
_ => unimplemented!(),
}
}
let painter = ui.painter_at(rect);
let shape = egui::Shape::mesh(mesh);
painter.add(shape);
let painter = ui.painter_at(rect);
let shape = egui::Shape::mesh(mesh);
painter.add(shape);
response
});
response
});
let msg_edit_id = egui::Id::new(format!("msg_edit_{}", self.index));
egui::Window::new("Message Editor")

1
book/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
book

6
book/book.toml Normal file
View File

@ -0,0 +1,6 @@
[book]
authors = ["marceline-cramer"]
language = "en"
multilingual = false
src = "src"
title = "Canary GUI Book"

30
book/src/SUMMARY.md Normal file
View File

@ -0,0 +1,30 @@
# Summary
[Introduction](introduction.md)
[History](history.md)
- [Implementation](impl/README.md)
- [Development Process](impl-rs/process.md)
- [Usage](impl-rs/usage.md)
- [Backends](impl-rs/backends.md)
- [Examples](impl-rs/examples.md)
- [Ecosystem](ecosystem/README.md)
- [Messages](ecosystem/messages.md)
- [Protocols](ecosystem/protocols.md)
- [Finding Scripts](ecosystem/finding-scripts.md)
- [Fonts](ecosystem/fonts.md)
- [Localization](ecosystem/localization.md)
- [Rendering](rendering/README.md)
- [Graphics State](rendering/state.md)
- [Primitives](rendering/primitives.md)
- [Canvases](rendering/canvases.md)
- [Tessellation](rendering/tessellation.md)
- [Text](rendering/text.md)
- [Input](input/README.md)
- [Pointer](input/pointer.md)
- [Text](input/text.md)
---
[Glossary](glossary.md)
[Credits](credits.md)

1
book/src/credits.md Normal file
View File

@ -0,0 +1 @@
# Credits

View File

@ -0,0 +1 @@
# Ecosystem

View File

@ -0,0 +1 @@
# Finding Scripts

View File

@ -0,0 +1 @@
# Fonts

View File

@ -0,0 +1 @@
# Localization

View File

@ -0,0 +1 @@
# Messages

View File

@ -0,0 +1 @@
# Protocols

1
book/src/glossary.md Normal file
View File

@ -0,0 +1 @@
# Glossary

16
book/src/history.md Normal file
View File

@ -0,0 +1,16 @@
# History
Canary was originally conceived in early 2021 as a WebAssembly-based,
minimalistic UI framework during the development of [Mondradiko](https://mondradiko.github.io),
where it was a dedicated subsystem of a larger game engine. When the new UI
system turned out to be even more powerful than originally expected, it was
decided that the UI code would be broken out into a separate project. The
Mondradiko community voted to name it "Canary" (the other contenders were
"Robin" and "Magpie"), and it was given [a new repository](https://github.com/mondradiko/canary).
However, before Canary could be fully fleshed-out, development on Mondradiko
was ceased and there was no reason to continue working on Canary.
In mid-2022, development was started back up, as a member project of
[Tebibyte Media](https://tebibyte.media). This new community of free software
enthusiasts had new interest in Canary apart from its usage in a larger game
engine, so development was restarted.

View File

@ -0,0 +1 @@
# Backends

View File

@ -0,0 +1 @@
# Examples

View File

@ -0,0 +1,23 @@
# Development Process
# Adding New Features
To keep Canary as minimal as possible we adopt a conservative policy for what
features are added to its specification. This is to avoid the feature-creep that
plagues large UI and UX frameworks in the long run. The following reasons are
general guidelines for what features should and should not be added to Canary.
## Reasons to add a feature
The feature provides a clear benefit to a cultural class of users. For example,
Arabic speakers will require that text can be rendered right-to-left.
The feature reduces the resource usage of scripts.
## Reasons NOT to add a feature
The feature adds more complexity to the host than is removed from scripts.
The feature only applies to certain host configurations.
The feature can be effectively emulated in a script.

View File

@ -1,39 +1,13 @@
# Canary
Canary is a post-structuralist graphical user interface (GUI) framework that
uses standardized message-passing protocols to represent UI state instead of a
typical DOM-based GUI workflow.
Canary scripts (executed as WebAssembly) implement all of the rendering and
layout of the GUIs, so the host has little to no involvement in their
appearance. This allows virtually unlimited customization of Canary GUIs, as
scripts can be reloaded by applications with no impact on application behavior.
Canary's development has been documented on Tebibyte Media's blog:
https://tebibyte.media/blog/project/canary/
# Screenshots
<div align="center">
<figure>
<img src="./resources/sandbox-screenshot.jpg"/>
<figcaption>A screenshot of the Canary sandbox and Sword Art Online script.</figcaption>
</figure>
<figure>
<img src="./resources/music-player-screenshot.jpg"/>
<figcaption>A screenshot of the desktop music player controller using the Sword Art Online script.</figcaption>
</figure>
</div>
# Using `canary-rs`
This repository (`canary-rs`) is the reference implementation for Canary. It is
written in Rust, and is licensed under the LGPLv3.
[`canary-rs`](https://git.tebibyte.media/canary/canary-rs) is the reference
implementation for Canary. It is written in Rust, and is licensed under the
LGPLv3.
`canary-rs` is the central hub for Canary's development. It includes host-side
Rust code, helper crates for Canary hosts, wrapper crates for scripts
authored in Rust, and the documentation that you're currently reading.
authored in Rust, and even the source code for the documentation that you're
currently reading.
`canary-rs` provides a graphical "sandbox" that embeds the Canary runtime
into a lightweight graphical app. It has two purposes: first, to give
@ -42,13 +16,13 @@ benchmark, and experiment with their scripts, and second, to give Canary
embedders a live, functioning example of how Canary can be integrated into their
applications.
## Running the `canary-rs` sandbox
# Running the `canary-rs` sandbox
The sandbox requires a Canary script to run. If you don't already have one,
you can follow [these instructions](optional-building-the-sword-art-online-demonstration-ui-script)
to build the example script provided by `canary-rs`.
### Building the sandbox
## Building the sandbox
To build the sandbox from source, first make sure that you have
[installed the standard Rust toolchain](https://www.rustlang.org/tools/install),
@ -72,8 +46,6 @@ Now, the sandbox can be ran with a script:
$ cargo run --release -p canary_sandbox -- <path-to-script>
```
## Running Magpie
## (Optional) Building the Sword Art Online demonstration UI script
`canary-rs` provides an example of a fully-functioning script which, optionally,
@ -99,7 +71,7 @@ Now it can be run using the sandbox:
$ cargo run --release -p canary_sandbox -- target/wasm32-unknown-unknown/release/sao_ui_rs.wasm
```
## Using `canary-rs` as a Rust library
# Using `canary-rs` as a Rust library
***WARNING***: `canary-rs` is still in alpha development so both its API and its
version number are unstable. It is not recommended to use it in your own

4
book/src/impl/README.md Normal file
View File

@ -0,0 +1,4 @@
# Implementation
This chapter discusses the design and usage of [canary-rs](https://git.tebibyte.media/canary/canary-rs),
the canonical implementation of Canary.

1
book/src/input/README.md Normal file
View File

@ -0,0 +1 @@
# Input

View File

@ -0,0 +1 @@
# Pointer

1
book/src/input/text.md Normal file
View File

@ -0,0 +1 @@
# Text

1
book/src/introduction.md Normal file
View File

@ -0,0 +1 @@
# Introduction

View File

@ -0,0 +1 @@
# Rendering

View File

@ -0,0 +1 @@
# Canvases

View File

@ -0,0 +1 @@
# Primitives

View File

@ -0,0 +1 @@
# Graphics State

View File

@ -0,0 +1 @@
# Tessellation

View File

@ -0,0 +1 @@
# Text

View File

@ -1,38 +1,4 @@
#!/bin/sh
# Depends on: `rg` (ripgrep)
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")"
toml_lic="$(tomcat package.license "$toml")"
if ! test -n "$toml_lic"; then
printf "%s: Missing license information\n" "$(printf "%s\n" "$toml" |\
sed "s/^.\+$dir\///g")"
continue 2
fi
for file in $(find "$(printf "%s\n" "$toml" |\
sed 's/Cargo\.toml/src/g')" -name "*.rs")
do
info="$(head -n 2 "$file")"
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
! rg --multiline --files-without-match --glob '*.rs' --pcre2 '(?<!\n)((//)|(#)) Copyright \(c\) \d+ [A-z, ]+\n((//)|(#)) SPDX-License-Identifier: .*\n'

View File

@ -7,17 +7,11 @@ use super::*;
static mut PANEL_IMPLS: Vec<Box<dyn PanelImpl>> = Vec::new();
pub fn bind_panel(
cb: impl Fn(Panel, Message, Message) -> Box<dyn PanelImpl>,
panel: u32,
protocol: u32,
msg: u32,
) -> u32 {
pub fn bind_panel<T: BindPanel>(panel: u32, msg: u32) -> u32 {
unsafe {
let panel = Panel(panel);
let protocol = Message(protocol);
let msg = Message(msg);
let panel_impl = cb(panel, protocol, msg);
let panel_impl = T::bind(panel, msg);
let id = PANEL_IMPLS.len() as u32;
PANEL_IMPLS.push(panel_impl);
id

View File

@ -9,10 +9,10 @@ pub mod abi;
#[macro_export]
macro_rules! export_abi {
($bind_panel: ident) => {
($panel_impl: ident) => {
#[no_mangle]
pub extern "C" fn bind_panel(panel: u32, protocol: u32, msg: u32) -> u32 {
::canary_script::api::abi::bind_panel($bind_panel, panel, protocol, msg)
pub extern "C" fn bind_panel(panel: u32, msg: u32) -> u32 {
::canary_script::api::abi::bind_panel::<$panel_impl>(panel, msg)
}
#[no_mangle]
@ -42,6 +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);
@ -94,29 +98,19 @@ 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)]
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone)]
pub struct Font(u32);
impl Font {
pub fn new(family: &str) -> Self {
unsafe { Self(font_load(family.as_ptr() as u32, family.len() as u32)) }
}
/// Retrieves the script-local identifier of this font.
pub fn get_id(&self) -> u32 {
self.0
}
}
#[repr(transparent)]
#[derive(Debug)]
pub struct TextLayout(u32);
impl TextLayout {
@ -261,10 +255,10 @@ impl DrawContext {
let delta = PI / 4.0 / spoke_num;
let (mut theta, limit) = match corner {
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),
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),
};
let mut last_spoke = Vec2::from_angle(theta) * radius + center;
@ -311,10 +305,10 @@ impl DrawContext {
rect
};
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;
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;
self.draw_triangle_noclip(v1, v2, v3, color);
self.draw_triangle_noclip(v2, v3, v4, color);
@ -339,48 +333,48 @@ impl DrawContext {
let mut inner_rect = rect;
let inset = rect.inset(radius);
if corners.intersects(CornerFlags::TOP) {
inner_rect.tl.y += radius;
let mut top_edge = Rect {
tl: rect.tl,
br: Vec2::new(rect.br.x, rect.tl.y + radius),
};
if corners.contains(CornerFlags::TOP_LEFT) {
top_edge.tl.x += radius;
self.draw_quarter_circle(Corner::TopLeft, inset.tl, radius, color);
}
if corners.contains(CornerFlags::TOP_RIGHT) {
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;
inner_rect.bl.y += radius;
let mut bottom_edge = Rect {
tl: Vec2::new(rect.tl.x, rect.br.y - radius),
br: rect.br,
bl: rect.bl,
tr: Vec2::new(rect.tr.x, rect.bl.y + radius),
};
if corners.contains(CornerFlags::BOTTOM_LEFT) {
bottom_edge.tl.x += radius;
self.draw_quarter_circle(Corner::BottomLeft, inset.bl(), radius, color);
bottom_edge.bl.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);
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;
let mut top_edge = Rect {
bl: Vec2::new(rect.bl.x, rect.tr.y - radius),
tr: rect.tr,
};
if corners.contains(CornerFlags::TOP_LEFT) {
top_edge.bl.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);
}
self.draw_rect(top_edge, color);
}
self.draw_rect(inner_rect, color);
}
@ -453,6 +447,4 @@ 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);
}

View File

@ -11,78 +11,78 @@ pub mod api;
#[repr(C)]
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
pub struct Rect {
pub tl: Vec2,
pub br: Vec2,
pub bl: Vec2,
pub tr: Vec2,
}
impl Rect {
pub const NEG_INFINITY: Self = Self {
tl: Vec2::splat(f32::INFINITY),
br: Vec2::splat(f32::NEG_INFINITY),
bl: Vec2::splat(f32::INFINITY),
tr: Vec2::splat(f32::NEG_INFINITY),
};
pub fn from_xy_size(xy: Vec2, size: Vec2) -> Self {
Self {
tl: xy,
br: xy + size,
bl: xy,
tr: xy + size,
}
}
pub fn from_circle_bounds(center: Vec2, radius: f32) -> Self {
Self {
tl: center - radius,
br: center + radius,
bl: center - radius,
tr: center + radius,
}
}
pub fn from_triangle_bounds(tri: &ColoredTriangle) -> Self {
Self {
tl: tri.v1.min(tri.v2).min(tri.v3),
br: tri.v1.max(tri.v2).max(tri.v3),
bl: tri.v1.min(tri.v2).min(tri.v3),
tr: tri.v1.max(tri.v2).max(tri.v3),
}
}
pub fn inset(&self, d: f32) -> Self {
Self {
tl: self.tl + d,
br: self.br - d,
bl: self.bl + d,
tr: self.tr - d,
}
}
pub fn bl(&self) -> Vec2 {
Vec2::new(self.tl.x, self.br.y)
pub fn tl(&self) -> Vec2 {
Vec2::new(self.bl.x, self.tr.y)
}
pub fn tr(&self) -> Vec2 {
Vec2::new(self.br.x, self.tl.y)
pub fn br(&self) -> Vec2 {
Vec2::new(self.tr.x, self.bl.y)
}
pub fn offset(&self, offset: Vec2) -> Self {
Self {
tl: self.tl + offset,
br: self.br + offset,
bl: self.bl + offset,
tr: self.tr + offset,
}
}
pub fn scale(&self, scale: f32) -> Self {
Self {
tl: self.tl * scale,
br: self.br * scale,
bl: self.bl * scale,
tr: self.tr * scale,
}
}
pub fn is_valid(&self) -> bool {
self.tl.cmplt(self.br).all()
self.bl.cmplt(self.tr).all()
}
pub fn intersects_rect(&self, other: &Self) -> bool {
self.tl.cmple(other.br).all() && self.br.cmpge(other.tl).all()
self.bl.cmple(other.tr).all() && self.tr.cmpge(other.bl).all()
}
pub fn intersection(&self, other: &Self) -> Option<Self> {
let clipped = Self {
tl: self.tl.max(other.tl),
br: self.br.min(other.br),
bl: self.bl.max(other.bl),
tr: self.tr.min(other.tr),
};
if clipped.is_valid() {
@ -92,41 +92,27 @@ 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.tl.x < other.tl.x
&& self.tl.y < other.tl.y
&& self.br.x > other.br.x
&& self.br.y > other.br.y
self.bl.x < other.bl.x
&& self.bl.y < other.bl.y
&& self.tr.x > other.tr.x
&& self.tr.y > other.tr.y
}
pub fn contains_point(&self, xy: Vec2) -> bool {
self.tl.x < xy.x && self.tl.y < xy.y && self.br.x > xy.x && self.br.y > xy.y
self.bl.x < xy.x && self.bl.y < xy.y && self.tr.x > xy.x && self.tr.y > xy.y
}
pub fn size(&self) -> Vec2 {
self.br - self.tl
self.tr - self.bl
}
pub fn width(&self) -> f32 {
self.br.x - self.tl.x
self.tr.x - self.bl.x
}
pub fn height(&self) -> f32 {
self.br.y - self.tl.y
self.tr.y - self.bl.y
}
}
@ -173,21 +159,15 @@ impl Color {
)
}
pub const fn alpha_multiply(&self, mul: u8) -> Self {
pub 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 const fn with_alpha(&self, alpha: u8) -> Self {
pub 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)]

View File

@ -1,9 +0,0 @@
[package]
name = "canary-textwrap"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
[dependencies]
canary-script = { path = "../script" }
textwrap = "0.16"

View File

@ -1,248 +0,0 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: Apache-2.0
use std::collections::HashMap;
use canary_script::api::{DrawContext, Font, TextLayout};
use canary_script::{Color, Vec2};
#[derive(Default)]
pub struct TextCache {
layouts: Vec<OwnedText>,
fonts: Vec<HashMap<String, usize>>,
}
impl TextCache {
pub fn insert(&mut self, font: Font, text: &str) -> (usize, f32) {
let font_idx = font.get_id() as usize;
if let Some(font_cache) = self.fonts.get(font_idx) {
if let Some(layout_idx) = font_cache.get(text) {
return (*layout_idx, self.get(*layout_idx).width);
}
} else {
let new_size = font_idx + 1;
self.fonts.resize_with(new_size, Default::default);
}
let index = self.layouts.len();
let layout = TextLayout::new(&font, text);
let width = layout.get_bounds().width().max(0.0);
self.layouts.push(OwnedText {
font,
layout,
width,
});
self.fonts[font_idx].insert(text.to_string(), index);
(index, width)
}
pub fn get(&self, index: usize) -> Text<'_> {
self.layouts[index].borrow()
}
}
/// Processed text that can be laid out.
pub struct Content {
pub words: Vec<Word>,
}
impl Content {
pub fn from_plain(cache: &mut TextCache, font: Font, text: &str) -> Self {
use textwrap::word_splitters::{split_words, WordSplitter};
use textwrap::WordSeparator;
let separator = WordSeparator::new();
let words = separator.find_words(text);
// TODO: crate feature to enable hyphenation support?
let splitter = WordSplitter::NoHyphenation;
let split_words = split_words(words, &splitter);
let mut words = Vec::new();
for split_word in split_words {
let (word, word_width) = cache.insert(font, split_word.word);
let (whitespace, whitespace_width) = cache.insert(font, "_");
let (penalty, penalty_width) = cache.insert(font, split_word.penalty);
words.push(Word {
word,
word_width,
whitespace,
whitespace_width,
penalty,
penalty_width,
});
}
Self { words }
}
pub fn layout(&self, cache: &TextCache, width: f32) -> Layout {
use textwrap::wrap_algorithms::wrap_optimal_fit;
let fragments = self.words.as_slice();
let line_widths = &[width as f64];
let penalties = Default::default();
// Should never fail with reasonable input. Check [wrap_optimal_fit] docs for more info.
let wrapped_lines = wrap_optimal_fit(fragments, line_widths, &penalties).unwrap();
let mut lines = Vec::new();
for line in wrapped_lines {
lines.push(Line::from_word_line(cache, line));
}
Layout { lines }
}
}
/// An atomic fragment of processed text that is ready to be laid out.
///
/// May or may not correspond to a single English "word".
///
/// Please see [textwrap::core::Word] and [textwrap::core::Fragment] for more information.
#[derive(Debug)]
pub struct Word {
pub word: usize,
pub word_width: f32,
pub whitespace: usize,
pub whitespace_width: f32,
pub penalty: usize,
pub penalty_width: f32,
}
impl textwrap::core::Fragment for Word {
fn width(&self) -> f64 {
self.word_width as f64
}
fn whitespace_width(&self) -> f64 {
self.whitespace_width as f64
}
fn penalty_width(&self) -> f64 {
self.penalty_width as f64
}
}
#[derive(Debug)]
pub struct OwnedText {
pub font: Font,
pub layout: TextLayout,
pub width: f32,
}
impl OwnedText {
pub fn borrow(&self) -> Text<'_> {
Text {
font: self.font,
layout: &self.layout,
width: self.width,
}
}
}
/// A single piece of renderable text.
#[derive(Debug)]
pub struct Text<'a> {
/// The font that this fragment has been laid out with.
pub font: Font,
/// The draw-ready [TextLayout] of this fragment.
pub layout: &'a TextLayout,
/// The width of this text.
pub width: f32,
}
/// A finished, wrapped text layout.
pub struct Layout {
pub lines: Vec<Line>,
}
impl Layout {
pub fn draw(
&self,
cache: &TextCache,
ctx: &DrawContext,
scale: f32,
line_height: f32,
color: Color,
) {
let mut cursor = Vec2::ZERO;
for line in self.lines.iter() {
let ctx = ctx.with_offset(cursor);
line.draw(cache, &ctx, scale, color);
cursor.y += line_height;
}
}
}
/// A finished line of a layout.
pub struct Line {
pub fragments: Vec<Fragment>,
}
impl Line {
pub fn from_word_line(cache: &TextCache, words: &[Word]) -> Self {
let last_idx = words.len() - 1;
let mut fragments = Vec::new();
let mut add_word = |index: usize, hidden: bool| {
let text = cache.get(index);
fragments.push(Fragment {
font: text.font,
text: index,
offset: Vec2::ZERO,
advance: text.width,
hidden,
});
};
for (idx, word) in words.iter().enumerate() {
add_word(word.word, false);
if idx == last_idx {
add_word(word.penalty, false);
} else {
add_word(word.whitespace, true);
}
}
Self { fragments }
}
pub fn draw(&self, cache: &TextCache, ctx: &DrawContext, scale: f32, color: Color) {
let mut cursor = Vec2::ZERO;
for fragment in self.fragments.iter() {
if !fragment.hidden {
let text = cache.get(fragment.text);
let offset = cursor + fragment.offset;
ctx.draw_text_layout(text.layout, offset, scale, color);
}
cursor.x += fragment.advance * scale;
}
}
}
/// A finished fragment in a layout.
pub struct Fragment {
/// The font of this fragment.
pub font: Font,
/// The index into the [TextCache] of the content of this fragment.
pub text: usize,
/// The offset for drawing the text layout.
pub offset: Vec2,
/// The horizontal advance to draw the next fragment.
pub advance: f32,
/// Whether this fragment should be skipped while drawing.
pub hidden: bool,
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

View File

@ -0,0 +1,10 @@
[package]
name = "canary-force-directed-graph-script"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
canary-script = { path = "../../crates/script" }

View File

@ -0,0 +1,566 @@
use canary_script::api::{BindPanel, DrawContext, Font, Message, Panel, PanelImpl, TextLayout};
use canary_script::{export_abi, Color, CursorEventKind, MeshVertex, Vec2};
static LABEL_FONT: &str = "Liberation Sans";
pub struct Body {
pub position: Vec2,
pub velocity: Vec2,
pub acceleration: Vec2,
pub mass: f32,
pub friction: f32,
pub fixed: bool,
}
impl Default for Body {
fn default() -> Self {
Self {
position: Vec2::ZERO,
velocity: Vec2::ZERO,
acceleration: Vec2::ZERO,
friction: 0.001,
mass: 1.0,
fixed: false,
}
}
}
impl Body {
pub fn apply_force(&mut self, force: Vec2) {
self.acceleration += force / self.mass; // F = ma
}
pub fn apply_velocity(&mut self, vel: Vec2) {
self.velocity += vel;
}
pub fn update(&mut self, dt: f32) {
if self.fixed {
self.velocity = Vec2::ZERO;
return;
}
let pos = self.position;
let vel = self.velocity;
let acc = self.acceleration;
let new_pos = pos + vel * dt + acc * (dt * dt * 0.5);
let new_vel = vel + acc * (dt * 0.5);
self.position = new_pos;
self.velocity = new_vel * self.friction.powf(dt);
self.acceleration = Vec2::ZERO;
}
}
pub struct NodeInfo {
pub label: TextLayout,
pub label_offset: Vec2,
pub text_size: f32,
pub hidden: bool,
pub parent: Option<usize>,
pub unhide: Vec<usize>,
}
impl NodeInfo {
pub fn new(label: &str) -> Self {
let text_size = 5.0;
let label = TextLayout::new(&Font::new(LABEL_FONT), &label);
let bounds = label.get_bounds();
let label_offset = Vec2::new(-bounds.width() / 2.0, text_size / 2.0);
Self {
label,
label_offset,
text_size,
hidden: false,
parent: None,
unhide: Vec::new(),
}
}
}
pub struct Node {
pub body: Body,
pub info: NodeInfo,
}
#[derive(Debug, Clone)]
pub struct MenuNode {
pub label: String,
pub children: Vec<MenuNode>,
}
impl MenuNode {
pub fn make_test() -> Self {
Self {
label: "Menu".to_string(),
children: vec![
Self {
label: "File".to_string(),
children: vec![
Self {
label: "Open...".to_string(),
children: vec![],
},
Self {
label: "Save as...".to_string(),
children: vec![],
},
Self {
label: "Save".to_string(),
children: vec![],
},
Self {
label: "Quit".to_string(),
children: vec![],
},
],
},
Self {
label: "Edit".to_string(),
children: vec![
Self {
label: "Undo".to_string(),
children: vec![],
},
Self {
label: "Redo".to_string(),
children: vec![],
},
],
},
Self {
label: "View".to_string(),
children: vec![
Self {
label: "Zoom in".to_string(),
children: vec![],
},
Self {
label: "Zoom out".to_string(),
children: vec![],
},
Self {
label: "Reset zoom".to_string(),
children: vec![],
},
Self {
label: "Fullscreen".to_string(),
children: vec![],
},
],
},
Self {
label: "Help".to_string(),
children: vec![Self {
label: "About".to_string(),
children: vec![],
}],
},
],
}
}
pub fn make_force_graph(&self) -> ForceGraph {
let mut graph = ForceGraph::default();
let root = Node {
body: Body {
fixed: true,
..Default::default()
},
info: NodeInfo::new(&self.label),
};
let parent = graph.nodes.len();
graph.nodes.push(root);
self.build_force_graph(&mut graph, parent);
graph.unhide(parent);
graph
}
pub fn build_force_graph(&self, graph: &mut ForceGraph, parent: usize) {
let mut siblings = Vec::new();
let mut first_sibling = None;
let mut last_sibling = None;
const SIBLING_LENGTH: f32 = 60.0;
const SIBLING_STRENGTH: f32 = 0.1;
for child in self.children.iter() {
let id = graph.nodes.len();
let _ = first_sibling.get_or_insert(id);
graph.nodes.push(Node {
body: Body {
position: Vec2::from_angle(id as f32),
..Default::default()
},
info: NodeInfo {
hidden: true,
parent: Some(parent),
..NodeInfo::new(&child.label)
},
});
graph.springs.push(Constraint {
from: parent,
to: id,
length: 30.0,
strength: 1.0,
});
if let Some(last) = last_sibling {
graph.springs.push(Constraint {
from: last,
to: id,
length: SIBLING_LENGTH,
strength: SIBLING_STRENGTH,
});
}
siblings.push(id);
last_sibling = Some(id);
graph.connections.push(Connection {
from: parent,
to: id,
width: 1.0,
color: Color::WHITE,
});
child.build_force_graph(graph, id);
}
if let (Some(first), Some(last)) = (first_sibling, last_sibling) {
graph.springs.push(Constraint {
from: first,
to: last,
length: SIBLING_LENGTH,
strength: SIBLING_STRENGTH,
});
}
graph.nodes[parent].info.unhide.extend_from_slice(&siblings);
}
}
#[derive(Clone)]
pub struct Constraint {
pub from: usize,
pub to: usize,
pub length: f32,
pub strength: f32,
}
impl Constraint {
pub fn apply(&self, mut delta: Vec2) -> Vec2 {
const ALPHA: f32 = 0.99;
let distance = delta.length();
let displacement = distance - self.length;
delta *= displacement / distance * self.strength * ALPHA;
delta / 2.0
}
}
#[derive(Clone)]
pub struct Connection {
pub from: usize,
pub to: usize,
pub width: f32,
pub color: Color,
}
#[derive(Default)]
pub struct ForceGraph {
pub nodes: Vec<Node>,
pub springs: Vec<Constraint>,
pub connections: Vec<Connection>,
}
impl ForceGraph {
pub fn update(&mut self, dt: f32) {
self.apply_springs(dt);
self.apply_repulsion();
for node in self.nodes.iter_mut() {
if !node.info.hidden {
node.body.update(dt);
}
}
}
pub fn reaction(&self, from: usize, to: usize, delta: Vec2) -> (Vec2, Vec2) {
let from_fixed = self.nodes[from].body.fixed;
let to_fixed = self.nodes[to].body.fixed;
if from_fixed {
let from = Vec2::ZERO;
let to = if to_fixed { Vec2::ZERO } else { -delta };
(from, to)
} else if to_fixed {
let to = Vec2::ZERO;
let from = if from_fixed { Vec2::ZERO } else { delta };
(from, to)
} else {
let half_delta = delta / 2.0;
(half_delta, -half_delta)
}
}
pub fn with_unhidden<R>(
&self,
from: usize,
to: usize,
f: impl FnOnce(&Node, &Node) -> R,
) -> Option<R> {
let from = &self.nodes[from];
let to = &self.nodes[to];
if from.info.hidden || to.info.hidden {
None
} else {
Some(f(from, to))
}
}
pub fn apply_springs(&mut self, dt: f32) {
for spring in self.springs.iter() {
let from = spring.from;
let to = spring.to;
let vel = self.with_unhidden(from, to, |from, to| {
let from = from.body.position + from.body.velocity * dt;
let to = to.body.position + to.body.velocity * dt;
let delta = to - from;
spring.apply(delta)
});
if let Some(vel) = vel {
let (from_vel, to_vel) = self.reaction(from, to, vel);
self.nodes[from].body.apply_velocity(from_vel);
self.nodes[to].body.apply_velocity(to_vel);
}
}
}
pub fn apply_repulsion(&mut self) {
for i in 1..self.nodes.len() {
for j in i..self.nodes.len() {
let from = i - 1;
let to = j;
let force = self.with_unhidden(from, to, |from, to| {
const REPULSION_CONSTANT: f32 = 1000.0;
let delta = to.body.position - from.body.position;
let proximity = delta.length().max(0.1);
let force = -(REPULSION_CONSTANT / (proximity * proximity));
delta * force
});
if let Some(force) = force {
let (from_force, to_force) = self.reaction(from, to, force);
self.nodes[from].body.apply_force(from_force);
self.nodes[to].body.apply_force(to_force);
}
}
}
}
pub fn unhide(&mut self, node: usize) {
let node = &mut self.nodes[node];
node.info.hidden = false;
let position = node.body.position;
let children = node.info.unhide.to_owned();
// Avoid divide-by-zero
if children.is_empty() {
return;
}
let mut neighbor_num = children.len();
let mut rotate_by = Vec2::new(1.0, 0.0);
if let Some(parent) = node.info.parent {
neighbor_num += 1;
rotate_by = self.nodes[parent].body.position - position;
rotate_by = rotate_by.normalize();
}
let theta = std::f32::consts::TAU / neighbor_num as f32;
for (index, child) in children.into_iter().enumerate() {
let angle = Vec2::from_angle((index + 1) as f32 * theta).rotate(rotate_by);
let child = &mut self.nodes[child];
child.info.hidden = false;
child.body.fixed = false;
child.body.position = position + angle;
child.body.velocity = Vec2::ZERO;
}
}
pub fn hide(&mut self, node: usize) {
let node = &mut self.nodes[node];
node.info.hidden = true;
let children = node.info.unhide.to_owned();
for child in children {
self.hide(child);
}
}
pub fn on_select(&mut self, id: usize) {
let node = &mut self.nodes[id];
if !node.body.fixed {
node.body.fixed = true;
self.unhide(id);
} else {
node.body.fixed = false;
let children = node.info.unhide.to_owned();
for child in children {
self.hide(child);
}
}
}
pub fn draw(&self, ctx: &DrawContext) {
let radius = 5.0;
let thickness = 1.0;
let color = Color::WHITE;
let label_color = Color::WHITE;
for node in self.nodes.iter() {
if node.info.hidden {
continue;
}
ctx.draw_ring(node.body.position, radius, thickness, color);
let offset = node.body.position + node.info.label_offset;
ctx.draw_text_layout(&node.info.label, offset, node.info.text_size, label_color);
}
for connection in self.connections.iter() {
self.with_unhidden(connection.from, connection.to, |from, to| {
let from = from.body.position;
let to = to.body.position;
let delta = to - from;
if delta.length() < radius * 2.0 {
return;
}
let delta_norm = delta.normalize();
let from = from + delta_norm * radius;
let to = to - delta_norm * radius;
let delta_cross = Vec2::new(delta_norm.y, -delta_norm.x);
let delta_side = delta_cross * connection.width / 2.0;
let vertices = [
MeshVertex {
position: from + delta_side,
color,
},
MeshVertex {
position: from - delta_side,
color,
},
MeshVertex {
position: to + delta_side,
color,
},
MeshVertex {
position: to - delta_side,
color,
},
];
let indices = [0, 1, 2, 1, 2, 3];
ctx.draw_indexed(&vertices, &indices);
});
}
}
}
export_abi!(ForceDirectedGraphPanel);
pub struct ForceDirectedGraphPanel {
panel: Panel,
graph: ForceGraph,
center: Vec2,
last_focus: Vec2,
focus_anim: f32,
focus_target: usize,
size: Vec2,
}
impl BindPanel for ForceDirectedGraphPanel {
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
let menu = MenuNode::make_test();
let graph = menu.make_force_graph();
let panel = Self {
panel,
graph,
last_focus: Vec2::ZERO,
center: Vec2::ZERO,
focus_anim: 1.0,
focus_target: 0,
size: Vec2::splat(100.0),
};
Box::new(panel)
}
}
impl PanelImpl for ForceDirectedGraphPanel {
fn update(&mut self, dt: f32) {
self.graph.update(dt);
self.focus_anim += dt;
let target_pos = self.graph.nodes[self.focus_target].body.position;
let anim = self.focus_anim / 0.5;
if anim < 0.0 {
self.center = self.last_focus;
} else if anim < 1.0 {
let anim = -(anim * (anim - 2.0));
self.center = target_pos * anim + self.last_focus * (1.0 - anim);
} else {
self.center = target_pos;
}
}
fn draw(&mut self) {
let offset = self.size / 2.0 - self.center;
let ctx = DrawContext::new(self.panel).with_offset(offset);
self.graph.draw(&ctx);
}
fn on_resize(&mut self, new_size: Vec2) {
self.size = new_size;
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
let offset = self.size / 2.0 - self.center;
let at = at - offset;
if let CursorEventKind::Deselect = kind {
let node = self
.graph
.nodes
.iter()
.position(|node| !node.info.hidden && node.body.position.distance(at) < 6.0);
if let Some(node) = node {
self.graph.on_select(node);
self.last_focus = self.center;
self.focus_target = node;
self.focus_anim = 0.0;
}
}
}
fn on_message(&mut self, msg: Message) {}
}

View File

@ -2,7 +2,6 @@
name = "canary-music-player-script"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-or-later"
[lib]
crate-type = ["cdylib"]

View File

@ -1,17 +1,10 @@
// 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 api::*;
use canary_script::*;
use api::*;
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)
}
canary_script::export_abi!(MusicPlayerPanel);
const DISPLAY_FONT: &str = "Liberation Sans";
@ -21,6 +14,19 @@ 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) {}
@ -33,31 +39,20 @@ impl PanelImpl for MusicPlayerPanel {
self.label.draw(&ctx, offset, size, color);
}
fn on_resize(&mut self, new_size: Vec2) {}
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::{serde_json, InMsg};
use canary_music_player::{InMsg, serde_json};
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,

View File

@ -10,10 +10,7 @@ crate-type = ["cdylib"]
[dependencies]
glam = "^0.21"
keyframe = "1"
canary-music-player = { path = "../../apps/music-player" }
canary-notifications = { path = "../../apps/notifications" }
canary-script = { path = "../../crates/script" }
canary-textwrap = { path = "../../crates/textwrap" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
wee_alloc = "^0.4"

View File

@ -1,6 +1,3 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::Color;
use keyframe::EasingFunction;

View File

@ -0,0 +1 @@

View File

@ -1,36 +1,18 @@
// 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 music_player;
pub mod notifications;
pub mod style;
pub mod panel;
pub mod widgets;
use api::*;
use canary_script::*;
use main_menu::MainMenuPanel;
use music_player::MusicPlayerPanel;
use notifications::NotificationPanel;
use api::*;
use widgets::Widget;
use main_menu::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),
"tebibyte-media.desktop.notification" => NotificationPanel::bind(panel, msg),
"wip-dialog" => ConfirmationDialogPanel::bind(panel, msg),
_ => MainMenuPanel::bind(panel, msg),
}
}
export_abi!(MainMenuPanel);
pub const ICON_FONT: &str = "Iosevka Nerd Font";
pub const DISPLAY_FONT: &str = "Homenaje";
@ -41,6 +23,18 @@ 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);
@ -51,31 +45,9 @@ impl PanelImpl for ConfirmationDialogPanel {
self.dialog.draw(&ctx);
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
fn on_cursor_event(&mut self, kind: CursorEventKind, at: canary_script::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 })
}
}

View File

@ -1,13 +1,9 @@
// 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;
@ -16,6 +12,15 @@ 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);
@ -26,35 +31,23 @@ impl PanelImpl for MainMenuPanel {
Widget::draw(&mut self.menu, &ctx);
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
fn on_cursor_event(&mut self, kind: CursorEventKind, at: canary_script::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 ANCHOR: Vec2 = Vec2::new(100.0, 100.0);
pub const SUBMENU_SPACING: f32 = 15.0;
pub const POSITION_X: f32 = -0.40;
pub const SUBMENU_SPACING: f32 = 0.1;
}
impl Default for MainMenu {
@ -63,12 +56,12 @@ impl Default for MainMenu {
let icons = ["", "", "", "", "", ""];
let button_style = RoundButtonStyle {
radius: 7.5,
spacing: 1.5,
thickness: 0.4,
body_color: THEME.palette.surface,
ring_color: THEME.palette.surface,
icon_color: THEME.palette.text,
radius: 0.05,
spacing: 0.01,
thickness: 0.002,
body_color: Color::WHITE,
ring_color: Color::WHITE,
icon_color: Color::BLACK,
};
let mut buttons = Vec::new();
@ -82,13 +75,12 @@ impl Default for MainMenu {
buttons.push(button);
}
let menu = SlotMenu::new(buttons, 30.0);
let menu = Offset::new(menu, Self::ANCHOR);
let menu = SlotMenu::new(buttons, 0.18);
let menu = Offset::new(menu, Vec2::new(Self::POSITION_X, 0.0));
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 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 reveal_duration = 0.1;
let player_info = PlayerInfo::new();
@ -104,15 +96,6 @@ 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);
@ -121,7 +104,6 @@ impl Default for MainMenu {
menu,
player_info,
inventory,
palette,
settings,
}
}
@ -132,7 +114,6 @@ 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);
}
@ -152,14 +133,8 @@ impl Container for MainMenu {
self.player_info.hide();
self.inventory.hide();
}
SlotMenuEvent::SubmenuOpen(4) => {
self.palette.show();
self.settings.show();
}
SlotMenuEvent::SubmenuClose(4) => {
self.palette.hide();
self.settings.hide();
}
SlotMenuEvent::SubmenuOpen(4) => self.settings.show(),
SlotMenuEvent::SubmenuClose(4) => self.settings.hide(),
_ => {}
};
}
@ -169,16 +144,14 @@ pub struct PlayerInfo {
width: f32,
height: f32,
rounding: f32,
color: Color,
}
impl PlayerInfo {
pub fn new() -> Self {
Self {
width: 70.0,
height: 120.0,
rounding: 5.0,
color: THEME.palette.surface,
width: 0.5,
height: 0.9,
rounding: 0.02,
}
}
}
@ -191,7 +164,7 @@ impl RectBounds for PlayerInfo {
impl Widget for PlayerInfo {
fn draw(&mut self, ctx: &DrawContext) {
ctx.draw_rounded_rect(self.get_bounds(), self.rounding, self.color);
ctx.draw_rounded_rect(self.get_bounds(), self.rounding, Color::WHITE);
}
}
@ -202,7 +175,7 @@ pub struct Inventory {
impl Inventory {
pub fn new(available_width: f32) -> (Self, f32) {
let height = 1024.0;
let height = 1.28;
(
Self {
@ -216,8 +189,8 @@ impl Inventory {
impl Widget for Inventory {
fn draw(&mut self, ctx: &DrawContext) {
let box_size = 12.0;
let box_margin = 4.0;
let box_size = 0.06;
let box_margin = 0.02;
let box_stride = box_size + box_margin;
let grid_width = (self.width / box_stride).floor() as usize;
@ -254,7 +227,7 @@ impl SettingsMenu {
("Log Out", ""),
];
let button_size = Vec2::new(90.0, 20.0);
let button_size = Vec2::new(0.4, 0.1);
let button_rect = Rect::from_xy_size(Vec2::new(0.0, -button_size.y / 2.0), button_size);
let mut buttons = Vec::new();
@ -275,7 +248,7 @@ impl SettingsMenu {
buttons.push(button);
}
let menu = SlotMenu::new(buttons, 25.0);
let menu = SlotMenu::new(buttons, 0.12);
Self {
menu,

View File

@ -1,430 +0,0 @@
// Copyright (c) 2022 Marceline Crmaer
// SPDX-License-Identifier: AGPL-3.0-or-later
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);
}
}

View File

@ -1,130 +0,0 @@
// Copyright (c) 2022 Marceline Crmaer
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::widgets::prelude::*;
use api::*;
use canary_script::*;
use canary_textwrap::{Content, Layout, TextCache};
use dialog::{DialogBodyStyle, DialogHeaderStyle};
use shell::Offset;
use text::{Label, LabelText};
pub struct NotificationStyle {
pub header: DialogHeaderStyle,
pub body: DialogBodyStyle,
pub rounding: f32,
}
impl Default for NotificationStyle {
fn default() -> Self {
Self {
header: DialogHeaderStyle {
height: 12.0,
..Default::default()
},
body: Default::default(),
rounding: THEME.metrics.surface_rounding,
}
}
}
pub struct NotificationPanel {
panel: Panel,
style: NotificationStyle,
summary: Label,
text_cache: TextCache,
body: Content,
body_layout: Layout,
header_rect: Rect,
body_rect: Rect,
}
impl PanelImpl for NotificationPanel {
fn update(&mut self, dt: f32) {}
fn draw(&mut self) {
let ctx = DrawContext::new(self.panel);
ctx.draw_partially_rounded_rect(
CornerFlags::TOP,
self.header_rect,
self.style.rounding,
self.style.header.color,
);
ctx.draw_partially_rounded_rect(
CornerFlags::BOTTOM,
self.body_rect,
self.style.rounding,
self.style.body.color,
);
self.summary.draw(&ctx);
let ctx = ctx.with_offset(Vec2::new(5.0, 20.0));
self.body_layout
.draw(&self.text_cache, &ctx, 5.0, 8.0, THEME.palette.text);
}
fn on_resize(&mut self, new_size: Vec2) {
let style = &self.style;
let width = new_size.x;
let body_height = new_size.y - style.header.height;
let body_height = body_height.max(0.0);
let header_size = Vec2::new(width, style.header.height);
let body_size = Vec2::new(width, body_height);
self.header_rect = Rect::from_xy_size(Vec2::ZERO, header_size);
self.body_rect = Rect::from_xy_size(self.header_rect.bl(), body_size);
let width = (new_size.x - 10.0) / 5.0;
self.body_layout = self.body.layout(&mut self.text_cache, width);
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {}
fn on_message(&mut self, msg: Message) {}
}
impl NotificationPanel {
pub fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
let msg = msg.to_vec();
let msg: canary_notifications::Contents = serde_json::from_slice(&msg).unwrap();
let style = NotificationStyle::default();
let font = style.header.text_font;
let text = msg.summary;
let text = LabelText { font, text };
let scale = style.header.height * style.header.text_scale_factor;
let summary = Label::new(
text,
text::HorizontalAlignment::Left,
scale,
style.header.text_color,
5.0,
5.0,
style.header.height * (1.0 - style.header.text_baseline),
);
let font = Font::new(crate::CONTENT_FONT);
let text = msg.body.unwrap_or(String::new());
let mut text_cache = TextCache::default();
let body = Content::from_plain(&mut text_cache, font, &text);
let body_layout = body.layout(&text_cache, 0.0);
let header_rect = Default::default();
let body_rect = Default::default();
Box::new(Self {
style,
panel,
summary,
text_cache,
body,
body_layout,
header_rect,
body_rect,
})
}
}

View File

@ -0,0 +1 @@

View File

@ -1,173 +0,0 @@
// Copyright (c) 2022 Marceline Crmaer
// SPDX-License-Identifier: AGPL-3.0-or-later
use canary_script::Color;
/// A reusable set of colors. Used by default widget styles.
pub struct Palette {
pub base: Color,
pub base_hover: Color,
pub base_active: Color,
pub surface: Color,
pub overlay: Color,
pub text: Color,
pub black: Color,
pub red: Color,
pub green: Color,
pub yellow: Color,
pub blue: Color,
pub magenta: Color,
pub cyan: Color,
pub white: Color,
}
impl Palette {
pub fn make_label_pairs(&self) -> Vec<(&'static str, Color)> {
vec![
("Base", self.base),
("Base Hover", self.base_hover),
("Base Active", self.base_active),
("Surface", self.surface),
("Overlay", self.overlay),
("Text", self.text),
("Black", self.black),
("Red", self.red),
("Green", self.green),
("Yellow", self.yellow),
("Blue", self.blue),
("Magenta", self.magenta),
("Cyan", self.cyan),
("White", self.white),
]
}
}
/// The common base color alpha shared between all themes.
pub const BASE_ALPHA: u8 = 230;
/// The common base_hover color alpha shared between all themes.
pub const BASE_HOVER_ALPHA: u8 = 242;
/// Converts 0xrrggbb hex to an opaque [Color].
pub const fn hex(rgb: u32) -> Color {
Color((rgb << 8) | 0xff)
}
/// Sword Art Online color palette.
pub const SAO_PALETTE: Palette = Palette {
base: Color::WHITE.with_alpha(BASE_ALPHA),
base_hover: Color::WHITE.with_alpha(BASE_HOVER_ALPHA),
base_active: Color::YELLOW,
surface: Color::WHITE,
overlay: Color::WHITE,
text: Color::BLACK,
black: Color::BLACK,
red: Color::RED,
green: Color::GREEN,
yellow: Color::YELLOW,
blue: Color::BLUE,
magenta: Color::MAGENTA,
cyan: Color::CYAN,
white: Color::WHITE,
};
/// Rose Pine color palette.
pub const ROSE_PINE_PALETTE: Palette = Palette {
base: hex(0x191724).with_alpha(BASE_ALPHA),
base_hover: hex(0x21202ee0).with_alpha(BASE_HOVER_ALPHA), // Highlight Low
base_active: hex(0x403d52), // Highlight Med
surface: hex(0x1f1d2e),
overlay: hex(0x26233a),
text: hex(0xe0def4),
black: hex(0x6e6a86), // Muted
red: hex(0xeb6f92), // Love
green: hex(0x7fb59f), // ??? (not in Rose Pine?)
yellow: hex(0xf6c177), // Gold
blue: hex(0x31748f), // Pine
magenta: hex(0xc4a7e7), // Iris
cyan: hex(0x9ccfd8), // Foam
white: hex(0xe0def4), // Text
};
/// Rose Pine Moon color palette.
pub const ROSE_PINE_MOON_PALETTE: Palette = Palette {
base: hex(0x232136).with_alpha(BASE_ALPHA),
base_hover: hex(0x2a283e).with_alpha(BASE_HOVER_ALPHA), // Highlight Low
base_active: hex(0x44415a), // Highlight Med
surface: hex(0x2a273f),
overlay: hex(0x393552),
text: hex(0xe0def4),
black: hex(0x6e6a86), // Muted
red: hex(0xeb6f92), // Love
green: hex(0x7fb59f), // ??? (not in Rose Pine?)
yellow: hex(0xf6c177), // Gold
blue: hex(0x3e8fb0), // Pine
magenta: hex(0xc4a7e7), // Iris
cyan: hex(0x9ccfd8), // Foam
white: hex(0xe0def4), // Text
};
/// [Arctica](https://github.com/sashakoshka/arctica) indexable color theme.
pub const ARCTICA: [Color; 24] = [
hex(0x242933),
hex(0x2e3440),
hex(0x3b4252),
hex(0x4c566a),
hex(0xeceff4),
hex(0xd8dee9),
hex(0xc2c9d6),
hex(0xaeb7c6),
hex(0xa8555d),
hex(0xb77763),
hex(0xcdb179),
hex(0x8ba277),
hex(0x769b9b),
hex(0x72a1ae),
hex(0x5e81ac),
hex(0x92738c),
hex(0xbf616a),
hex(0xd08770),
hex(0xebcb8b),
hex(0xa3be8c),
hex(0x8fbcbb),
hex(0x88c0d0),
hex(0x81a1c1),
hex(0xb48ead),
];
/// [Arctica](https://github.com/sashakoshka/arctica) color palette.
pub const ARCTICA_PALETTE: Palette = Palette {
base: ARCTICA[0].with_alpha(BASE_ALPHA),
base_hover: ARCTICA[1].with_alpha(BASE_HOVER_ALPHA),
base_active: ARCTICA[13],
surface: ARCTICA[2],
overlay: ARCTICA[3],
text: ARCTICA[5],
black: ARCTICA[3],
red: ARCTICA[8],
green: ARCTICA[11],
yellow: ARCTICA[10],
blue: ARCTICA[14],
magenta: ARCTICA[15],
cyan: ARCTICA[13],
white: ARCTICA[7],
};
/// Common measurements for widget shapes.
pub struct Metrics {
pub surface_rounding: f32,
}
/// Common default parameters for widget styles.
pub struct Theme {
pub palette: Palette,
pub metrics: Metrics,
}
/// The global theme.
pub const THEME: Theme = Theme {
palette: ROSE_PINE_MOON_PALETTE,
metrics: Metrics {
surface_rounding: 5.0,
},
};

View File

@ -48,12 +48,6 @@ 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 {
@ -118,8 +112,6 @@ 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 {
@ -131,11 +123,9 @@ impl Default for RectButtonStyle {
label_baseline: 0.25,
icon_scale_factor: 0.8,
icon_margin_factor: 1.1,
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,
inactive_color: Color::WHITE.with_alpha(0x40),
hover_color: Color::WHITE.with_alpha(0xb0),
selected_color: Color::YELLOW,
}
}
}
@ -164,7 +154,7 @@ impl RectButton {
label: Option<LabelText>,
icon: Option<LabelText>,
) -> Self {
let mut label_left = rect.tl.x;
let mut label_left = rect.bl.x;
let mut alignment = HorizontalAlignment::Center;
let icon = icon.map(|text| {
@ -172,9 +162,9 @@ impl RectButton {
label_left += margin;
alignment = HorizontalAlignment::Left;
let scale = rect.height() * style.icon_scale_factor;
let color = style.icon_color;
let cx = rect.tl.x + margin / 2.0;
let cy = rect.tl.y + rect.height() / 2.0;
let color = Color::BLACK;
let cx = rect.bl.x + margin / 2.0;
let cy = rect.bl.y + rect.height() / 2.0;
let center = Vec2::new(cx, cy);
Icon::new(text, scale, color, center)
@ -183,10 +173,10 @@ impl RectButton {
let label = label.map(|text| {
let scale = rect.height() * style.label_scale_factor;
let left = label_left;
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;
let right = rect.tr.x;
let baseline = rect.bl.y;
let baseline = (rect.height() * style.label_baseline) + baseline;
let color = Color::BLACK;
Label::new(text, alignment, scale, color, left, right, baseline)
});

View File

@ -23,14 +23,15 @@ impl DialogResponse {
pub fn get_color(&self) -> Color {
match self {
DialogResponse::Yes => THEME.palette.blue,
DialogResponse::No => THEME.palette.red,
DialogResponse::Yes => Color::BLUE,
DialogResponse::No => Color::RED,
}
}
}
#[derive(Clone)]
pub struct DialogStyle {
pub width: f32,
pub rounding: f32,
pub header: DialogHeaderStyle,
pub body: DialogBodyStyle,
@ -40,7 +41,8 @@ pub struct DialogStyle {
impl Default for DialogStyle {
fn default() -> Self {
Self {
rounding: THEME.metrics.surface_rounding,
width: 1.2,
rounding: 0.02,
header: Default::default(),
body: Default::default(),
footer: Default::default(),
@ -61,10 +63,10 @@ pub struct DialogHeaderStyle {
impl Default for DialogHeaderStyle {
fn default() -> Self {
Self {
color: THEME.palette.surface,
height: 20.0,
color: Color::WHITE,
height: 0.3,
text_font: Font::new(crate::DISPLAY_FONT),
text_color: THEME.palette.text,
text_color: Color::BLACK,
text_scale_factor: 0.65,
text_baseline: 0.25,
}
@ -74,18 +76,20 @@ 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_size: f32,
pub text_scale_factor: f32,
}
impl Default for DialogBodyStyle {
fn default() -> Self {
Self {
color: THEME.palette.base,
color: Color::WHITE.with_alpha(0xb0),
height: 0.6,
text_font: Font::new(crate::CONTENT_FONT),
text_color: THEME.palette.text,
text_size: 5.0,
text_color: Color::BLACK,
text_scale_factor: 0.15,
}
}
}
@ -95,7 +99,6 @@ pub struct DialogFooterStyle {
pub icon_font: Font,
pub button_radius: f32,
pub color: Color,
pub button_fg: Color,
pub height: f32,
}
@ -103,10 +106,9 @@ impl Default for DialogFooterStyle {
fn default() -> Self {
Self {
icon_font: Font::new(crate::ICON_FONT),
button_radius: 7.5,
color: THEME.palette.surface,
button_fg: THEME.palette.white,
height: 15.0,
button_radius: 0.1,
color: Color::WHITE,
height: 0.25,
}
}
}
@ -120,42 +122,22 @@ pub struct DialogInfo {
pub struct Dialog {
style: DialogStyle,
title: Offset<Label>,
content: Offset<Label>,
content_size: Vec2,
title: Label,
content: Label,
buttons: Vec<DialogButton>,
}
impl Dialog {
pub fn new(style: DialogStyle, info: &DialogInfo) -> Self {
let mut buttons = Vec::new();
for response in info.responses.iter() {
let color = response.get_color();
let radius = style.footer.button_radius;
let button_style = RoundButtonStyle {
radius: radius * 0.8,
spacing: radius * 0.15,
thickness: radius * 0.05,
body_color: color,
ring_color: color,
icon_color: style.footer.button_fg,
};
let width2 = style.width / 2.0;
let text = LabelText {
font: style.footer.icon_font,
text: response.get_text().to_string(),
};
let button = RoundButton::new(button_style, Some(text));
let button = Offset::new(button, Vec2::ZERO);
buttons.push(DialogButton {
response: *response,
button,
});
}
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,
@ -164,58 +146,59 @@ impl Dialog {
HorizontalAlignment::Center,
title_scale,
style.header.text_color,
0.0,
0.0,
0.0,
-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,
style.body.text_size,
content_scale,
style.body.text_color,
0.0,
0.0,
-width2,
width2,
0.0,
);
let mut dialog = Self {
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;
let color = response.get_color();
let radius = style.footer.button_radius;
let button_style = RoundButtonStyle {
radius: radius * 0.8,
spacing: radius * 0.15,
thickness: radius * 0.05,
body_color: color,
ring_color: color,
icon_color: Color::WHITE,
};
let text = LabelText {
font: style.footer.icon_font,
text: response.get_text().to_string(),
};
let button = RoundButton::new(button_style, Some(text));
let button = Offset::new(button, Vec2::new(button_x, button_y));
buttons.push(DialogButton {
response: *response,
button,
});
}
Self {
style,
title: Offset::new(title, Vec2::ZERO),
content: Offset::new(content, Vec2::ZERO),
content_size: Vec2::ONE,
title,
content,
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));
}
}
}
@ -232,15 +215,24 @@ impl Container for Dialog {
fn draw(&mut self, ctx: &DrawContext) {
let style = &self.style;
let width = self.content_size.x;
let width = style.width;
let width2 = width / 2.0;
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(Vec2::ZERO, header_size);
let header_rect = Rect::from_xy_size(header_xy, header_size);
let header_corners = CornerFlags::TOP;
let body_rect = Rect::from_xy_size(header_rect.bl(), self.content_size);
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 footer_size = Vec2::new(width, style.footer.height);
let footer_rect = Rect::from_xy_size(body_rect.bl(), footer_size);
let footer_rect = Rect::from_xy_size(footer_xy, footer_size);
let footer_corners = CornerFlags::BOTTOM;
ctx.draw_rect(body_rect, style.body.color);

View File

@ -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, -50.0, 0.0);
let mut slide_anim = Animation::new(EaseOut, duration, 0.25, 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,23 +222,22 @@ pub struct TabMenu {
}
impl TabMenu {
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 HEAD_RADIUS: f32 = 0.05;
const HEAD_HEIGHT: f32 = 0.1;
const TAB_WIDTH: f32 = 0.1;
const TAB_HEIGHT: f32 = 0.15;
const TAB_NUM: usize = 6;
const SEPARATOR_WIDTH: f32 = 5.0;
const INNER_RADIUS: f32 = 5.0;
const CONTENT_WIDTH: f32 = 100.0;
const SEPARATOR_WIDTH: f32 = 0.02;
const INNER_RADIUS: f32 = 0.01;
const CONTENT_WIDTH: f32 = 0.64;
const HEAD_BUTTON_STYLE: RoundButtonStyle = RoundButtonStyle {
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,
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,
};
const HEAD_BUTTON_MARGIN: f32 = Self::HEAD_HEIGHT / 2.0;
@ -249,8 +248,8 @@ impl TabMenu {
let mut tabs = Vec::new();
for i in 0..Self::TAB_NUM {
let y = i as f32 * Self::TAB_HEIGHT;
let pos = Vec2::new(0.0, y);
let y = (i + 1) as f32 * Self::TAB_HEIGHT;
let pos = Vec2::new(0.0, -y);
let mut style = RectButtonStyle::default();
style.radius = Self::HEAD_RADIUS;
@ -281,7 +280,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));
@ -296,8 +295,8 @@ impl TabMenu {
let scroll_bar = Offset::new(scroll_bar, Vec2::new(scroll_x, -tab_list_height));
let separator_rect = Rect {
tl: Vec2::new(Self::TAB_WIDTH, 0.0),
br: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, tab_list_height),
bl: Vec2::new(Self::TAB_WIDTH, -tab_list_height),
tr: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, 0.0),
};
let head_width = Self::TAB_WIDTH
@ -307,13 +306,13 @@ impl TabMenu {
+ scroll_bar.style.margin.x * 2.0;
let head_rect = Rect {
tl: Vec2::new(0.0, -Self::HEAD_HEIGHT),
br: Vec2::new(head_width, 0.0),
bl: Vec2::ZERO,
tr: Vec2::new(head_width, Self::HEAD_HEIGHT),
};
let view_rect = Rect {
tl: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, 0.0),
br: Vec2::new(head_rect.br.x, tab_list_height),
bl: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, -tab_list_height),
tr: Vec2::new(head_rect.tr.x, 0.0),
};
let view = ScrollView::new(
@ -323,7 +322,7 @@ impl TabMenu {
|available_width: f32| Inventory::new(available_width),
);
let view = Offset::new(view, view_rect.tl);
let view = Offset::new(view, view_rect.bl);
Self {
pop_out,
@ -349,18 +348,20 @@ 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,
Self::HEAD_COLOR,
head_color,
);
ctx.draw_partially_rounded_rect(
CornerFlags::TOP,
CornerFlags::TOP_LEFT | CornerFlags::TOP_RIGHT,
self.head_rect,
Self::HEAD_RADIUS,
Self::HEAD_COLOR,
head_color,
);
}
}

View File

@ -8,9 +8,7 @@ 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;
@ -77,7 +75,6 @@ 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::*;
}

View File

@ -1,109 +0,0 @@
// Copyright (c) 2022 Marceline Crmaer
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::prelude::*;
use shell::Offset;
use text::{HorizontalAlignment, Label, LabelText};
pub struct PaletteStyle {
pub bg: Color,
pub text: Color,
pub rounding: f32,
pub text_size: f32,
pub line_spacing: f32,
pub color_radius: f32,
pub margin: Rect,
}
impl Default for PaletteStyle {
fn default() -> Self {
Self {
bg: THEME.palette.surface,
text: THEME.palette.text,
rounding: THEME.metrics.surface_rounding,
text_size: 5.0,
line_spacing: 8.0,
color_radius: 3.0,
margin: Rect::from_xy_size(Vec2::splat(10.0), Vec2::ZERO),
}
}
}
/// A widget that displays all the colors in the global palette.
pub struct Palette {
body: Rect,
style: PaletteStyle,
labels: Vec<Offset<Label>>,
colors: Vec<(Vec2, Color)>,
}
impl Palette {
pub fn new(style: PaletteStyle) -> Self {
let width = 70.0;
let pairs = THEME.palette.make_label_pairs();
let label_font = Font::new(crate::CONTENT_FONT);
let mut label_cursor = Vec2::new(0.0, style.line_spacing) + style.margin.tl;
let mut color_cursor = Vec2::new(
width - style.margin.br.x,
style.line_spacing / 2.0 + style.margin.tl.y,
);
let mut labels = Vec::new();
let mut colors = Vec::new();
for (text, color) in pairs {
let text = LabelText {
font: label_font,
text: text.to_string(),
};
let label = Label::new(
text,
HorizontalAlignment::Left,
style.text_size,
style.text,
0.0,
0.0,
0.0,
);
let label = Offset::new(label, label_cursor);
labels.push(label);
colors.push((color_cursor, color));
label_cursor.y += style.line_spacing;
color_cursor.y += style.line_spacing;
}
let height = label_cursor.y + style.margin.br.y;
Self {
body: Rect::from_xy_size(Vec2::ZERO, Vec2::new(width, height)),
style,
labels,
colors,
}
}
}
impl RectBounds for Palette {
fn get_bounds(&self) -> Rect {
self.body
}
}
impl Widget for Palette {
fn draw(&mut self, ctx: &DrawContext) {
ctx.draw_rounded_rect(self.body, self.style.rounding, self.style.bg);
for label in self.labels.iter_mut() {
label.draw(ctx);
}
for (center, color) in self.colors.iter() {
ctx.draw_circle(*center, self.style.color_radius, *color);
}
}
}

View File

@ -18,14 +18,14 @@ pub struct ScrollBarStyle {
impl Default for ScrollBarStyle {
fn default() -> Self {
Self {
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,
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),
}
}
}
@ -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 {
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),
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),
};
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 = (self.scroll / self.content_height) * rail_height;
let body_y = rail_height - (self.scroll / self.content_height) * rail_height - body_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 = ((at.y - self.grab_coord) / self.rail_rect.height())
self.scroll = ((self.grab_coord - at.y) / 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();
let yoff = self.scroll_bar.get_scroll() - self.content_height + self.height;
self.inner.set_offset(Vec2::new(0.0, yoff));
}

View File

@ -132,8 +132,8 @@ impl<T: RectBounds> Offset<T> {
vert_align: OffsetAlignment,
) -> Self {
let bounds = inner.get_bounds();
let x = hori_align.align(bounds.tl.x, bounds.br.x);
let y = vert_align.align(bounds.br.y, bounds.tl.y);
let x = hori_align.align(bounds.bl.x, bounds.tr.x);
let y = vert_align.align(bounds.tr.y, bounds.bl.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.tl.x, bounds.br.x);
let y = vert_align.align(bounds.br.y, bounds.tl.y);
let x = hori_align.align(bounds.bl.x, bounds.tr.x);
let y = vert_align.align(bounds.tr.y, bounds.bl.y);
let offset = anchor - Vec2::new(x, y);
Self { inner, offset }
}

View File

@ -1,112 +0,0 @@
// Copyright (c) 2022 Marceline Crmaer
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::prelude::*;
pub struct SliderStyle {
pub bg_color: Color,
pub bg_padding: f32,
pub bg_rounding: f32,
pub fg_color: Color,
}
impl Default for SliderStyle {
fn default() -> Self {
Self {
bg_color: THEME.palette.overlay,
bg_padding: 2.5,
bg_rounding: 2.5,
fg_color: THEME.palette.blue,
}
}
}
pub struct Slider {
style: SliderStyle,
bg_rect: Rect,
fg_rect: Rect,
position: f32,
dirty: bool,
updating: bool,
}
impl Slider {
pub fn new(style: SliderStyle, rect: Rect) -> Self {
Self {
style,
bg_rect: rect,
fg_rect: rect,
position: 0.5,
dirty: true,
updating: false,
}
}
pub fn set_position(&mut self, position: f32) {
if !self.updating {
self.position = position;
self.dirty = true;
}
}
pub fn has_update(&mut self) -> Option<f32> {
if self.updating {
Some(self.position)
} else {
None
}
}
pub fn set_rect(&mut self, rect: Rect) {
self.bg_rect = rect;
self.dirty = true;
}
fn undirty(&mut self) {
if !self.dirty {
return;
}
let mut fg_space = self.bg_rect.inset(self.style.bg_padding);
fg_space.br.x = (fg_space.width() * self.position) + fg_space.tl.x;
self.fg_rect = fg_space;
self.dirty = false;
}
pub fn get_position(&self) -> f32 {
self.position
}
}
impl Widget for Slider {
fn draw(&mut self, ctx: &DrawContext) {
self.undirty();
ctx.draw_rounded_rect(self.bg_rect, self.style.bg_rounding, self.style.bg_color);
ctx.draw_rect(self.fg_rect, self.style.fg_color);
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
if let CursorEventKind::Select = kind {
if self.bg_rect.contains_point(at) {
self.updating = true;
}
}
if !self.updating {
return;
}
match kind {
CursorEventKind::Hover => {}
CursorEventKind::Select | CursorEventKind::Drag => {
let offset = at.x - self.fg_rect.tl.x;
let range = self.bg_rect.inset(self.style.bg_padding).width();
self.position = (offset / range).clamp(0.0, 1.0);
self.dirty = true;
}
CursorEventKind::Deselect => {
self.updating = false;
}
}
}
}

View File

@ -53,26 +53,6 @@ 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 {
@ -82,14 +62,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.tl.x,
HorizontalAlignment::Right => self.right - bounds.br.x,
HorizontalAlignment::Left => self.left - bounds.bl.x,
HorizontalAlignment::Right => self.right - bounds.tr.x,
HorizontalAlignment::Center => {
let available = self.right - self.left;
let halfway = available / 2.0 + self.left;
let width = bounds.br.x - bounds.tl.x;
let width = bounds.tr.x - bounds.bl.x;
let left = halfway - width / 2.0;
left - bounds.tl.x
left - bounds.bl.x
}
};
@ -128,14 +108,6 @@ 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 {
@ -145,7 +117,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.tl.y;
self.offset.y -= bounds.bl.y;
self.dirty = false;
self.layout = Some(layout);
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: LGPL-3.0-or-later
// SDPX-License-Identifier: LGPL-3.0-or-later
//! This module defines backends for WebAssembly execution.
//!
@ -8,8 +8,6 @@
//! 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,14 +17,12 @@ pub mod wasmtime;
/// Currently, only ever creates [wasmtime::WasmtimeBackend].
pub fn make_default_backend() -> anyhow::Result<Box<dyn Backend>> {
let backend = wasmtime::WasmtimeBackend::new()?;
log::info!("Created default ({}) backend", backend.name());
Ok(Box::new(backend))
}
/// A WebAssembly runtime backend.
pub trait Backend {
fn name(&self) -> &'static str;
fn load_module(&self, abi: Arc<ScriptAbi>, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>>;
fn load_module(&self, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>>;
}
/// An instance of a WebAssembly module.
@ -40,11 +36,10 @@ 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, 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.
/// 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.
///
/// The intended usecase for this userdata is to contain a pointer. A
/// Canary script can allocate some high-level object in memory, and when
@ -52,7 +47,7 @@ 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, protocol: &str, msg: Vec<u8>) -> u32;
fn bind_panel(&self, panel: PanelId, msg: Vec<u8>) -> u32;
fn update(&self, panel_ud: u32, dt: f32);
@ -64,128 +59,3 @@ pub trait Instance {
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>>>,
}

View File

@ -1,79 +1,42 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: LGPL-3.0-or-later
// SDPX-License-Identifier: LGPL-3.0-or-later
use std::collections::{hash_map::DefaultHasher, HashMap};
use std::hash::{Hasher, BuildHasherDefault};
use std::ops::DerefMut;
use std::time::Instant;
use super::{Arc, Backend, Instance, PanelId, ScriptAbi};
use crate::DrawCommand;
use super::{Arc, Backend, Instance, PanelId};
use crate::{DrawCommand, ScriptAbi, ScriptAbiImpl};
use canary_script::{Color, CursorEventKind, Rect, Vec2};
use parking_lot::Mutex;
use prehash::Passthru;
type Caller<'a> = wasmtime::Caller<'a, Arc<ScriptAbi>>;
type Store = wasmtime::Store<Arc<ScriptAbi>>;
type Linker = wasmtime::Linker<Arc<ScriptAbi>>;
type ModuleCache = Mutex<HashMap<u64, wasmtime::Module, BuildHasherDefault<Passthru>>>;
type Caller<'a> = wasmtime::Caller<'a, ScriptAbiImpl>;
type Store = wasmtime::Store<ScriptAbiImpl>;
type Linker = wasmtime::Linker<ScriptAbiImpl>;
pub struct WasmtimeBackend {
engine: wasmtime::Engine,
module_cache: ModuleCache,
}
impl WasmtimeBackend {
pub fn new() -> anyhow::Result<Self> {
log::info!("Creating wasmtime backend");
let mut config = wasmtime::Config::new();
config.wasm_simd(true);
config.wasm_bulk_memory(true);
config.cranelift_opt_level(wasmtime::OptLevel::Speed);
config.cache_config_load_default()?;
let engine = wasmtime::Engine::new(&config)?;
let module_cache = Default::default();
Ok(Self {
engine,
module_cache,
})
Ok(Self { engine })
}
}
impl Backend for WasmtimeBackend {
fn name(&self) -> &'static str {
"wasmtime"
}
fn load_module(&self, abi: Arc<ScriptAbi>, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>> {
let start = Instant::now();
let mut hasher = DefaultHasher::new();
hasher.write(module);
let hash = hasher.finish();
let fmt_hash = format!("{:x}", hash);
log::debug!("Loading module (hash: {})", fmt_hash);
let mut cache = self.module_cache.lock();
let module = if let Some(module) = cache.get(&hash) {
log::debug!("Module load cache hit (hash: {})", fmt_hash);
module
} else {
log::debug!("Module load cache miss; building (hash: {})", fmt_hash);
let start = Instant::now();
let module = wasmtime::Module::new(&self.engine, module)?;
cache.insert(hash, module);
log::debug!("Built module in {:?} (hash: {})", start.elapsed(), fmt_hash);
cache.get(&hash).unwrap()
};
fn load_module(&self, module: &[u8]) -> anyhow::Result<Arc<dyn Instance>> {
let module = wasmtime::Module::new(&self.engine, module)?;
let abi = ScriptAbiImpl::default();
let mut store = wasmtime::Store::new(&self.engine, abi);
let mut linker = Linker::new(&self.engine);
WasmtimeInstance::link(&mut linker)?;
let instance = linker.instantiate(&mut store, module)?;
let instance = linker.instantiate(&mut store, &module)?;
let bind_panel = instance.get_typed_func(&mut store, "bind_panel")?;
let update = instance.get_typed_func(&mut store, "update")?;
let draw = instance.get_typed_func(&mut store, "draw")?;
@ -93,19 +56,13 @@ impl Backend for WasmtimeBackend {
let instance = Arc::new(instance);
log::debug!(
"Loaded module in {:?} (hash: {})",
start.elapsed(),
fmt_hash
);
Ok(instance)
}
}
pub struct WasmtimeInstance {
store: Mutex<Store>,
bind_panel: wasmtime::TypedFunc<(u32, u32, u32), u32>,
bind_panel: wasmtime::TypedFunc<(u32, u32), u32>,
update: wasmtime::TypedFunc<(u32, f32), ()>,
draw: wasmtime::TypedFunc<u32, ()>,
on_resize: wasmtime::TypedFunc<(u32, f32, f32), ()>,
@ -191,15 +148,6 @@ 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(())
}
@ -242,13 +190,11 @@ impl WasmtimeInstance {
}
impl Instance for WasmtimeInstance {
fn bind_panel(&self, panel: PanelId, protocol: &str, msg: Vec<u8>) -> u32 {
fn bind_panel(&self, panel: PanelId, 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, protocol, msg);
let args = (panel.0 as u32, msg);
let data = self.bind_panel.call(store.deref_mut(), args).unwrap();
store.data().message_free(protocol);
store.data().message_free(msg);
data
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: LGPL-3.0-or-later
// SDPX-License-Identifier: LGPL-3.0-or-later
pub use canary_script::*;
use parking_lot::{Mutex, RwLock};
@ -10,33 +10,24 @@ use std::sync::Arc;
pub mod backend;
pub mod text;
use backend::{Backend, Instance, ScriptAbi};
use text::FontStore;
use backend::{Backend, Instance};
/// 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> {
log::info!("Initializing runtime with {} backend", backend.name());
Ok(Self {
backend,
font_store: Arc::new(FontStore::new()),
})
Ok(Self { backend })
}
pub fn load_module(&self, module: &[u8]) -> anyhow::Result<Script> {
let abi = ScriptAbi::new(self.font_store.to_owned());
let abi = Arc::new(abi);
let instance = self.backend.load_module(abi.to_owned(), module)?;
let instance = self.backend.load_module(module)?;
Ok(Script {
instance,
abi,
next_panel: 0,
})
}
}
@ -44,16 +35,16 @@ impl Runtime {
/// A loaded instance of a Canary script.
pub struct Script {
instance: Arc<dyn Instance>,
abi: Arc<ScriptAbi>,
next_panel: usize,
}
impl Script {
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);
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);
Ok(Panel {
instance: self.instance.clone(),
abi: self.abi.clone(),
id,
userdata,
})
@ -63,7 +54,6 @@ impl Script {
/// A Canary panel.
pub struct Panel {
instance: Arc<dyn Instance>,
abi: Arc<ScriptAbi>,
id: PanelId,
userdata: u32,
}
@ -88,15 +78,33 @@ 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);
@ -108,3 +116,95 @@ 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);
}
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: LGPL-3.0-or-later
// SDPX-License-Identifier: LGPL-3.0-or-later
use super::{AllsortsFont, Rect, Vec2};
@ -133,8 +133,26 @@ 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;
let glam_point = Vec2::new(point.x, point.y);
self.bounding_box = self.bounding_box.union_point(glam_point);
// 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;
}
point
}
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: LGPL-3.0-or-later
// SDPX-License-Identifier: LGPL-3.0-or-later
use super::{Color, DrawCommand, MeshIndex, MeshVertex, Rect, Vec2};
@ -73,9 +73,31 @@ impl Font {
xcur += position.hori_advance;
ycur += position.vert_advance;
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));
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;
}
}
TextLayout {
@ -220,7 +242,6 @@ impl Default for FontStore {
impl FontStore {
pub fn new() -> Self {
log::info!("Initializing FontStore");
let source = font_kit::source::SystemSource::new();
let source = Box::new(source);
@ -242,14 +263,14 @@ impl FontStore {
use font_kit::handle::Handle;
use font_kit::properties::Properties;
log::info!("Finding font by family: {}", title);
println!("loading font family {}", title);
let font_handle = self
.source
.select_best_match(&[FamilyName::Title(title.to_string())], &Properties::new())
.unwrap();
log::info!("Loading font file: {:?}", font_handle);
println!("loading font file: {:?}", font_handle);
let font_data = if let Handle::Path {
path,