Compare commits
37 Commits
Author | SHA1 | Date |
---|---|---|
mars | 73d524cddf | |
mars | f842ae608b | |
mars | 3a778c0aa6 | |
Iris Pupo | 38fc390e10 | |
mars | 4b89e781b0 | |
mars | 25eedc0a0d | |
mars | ad0feb2b31 | |
mars | 508abcb0cc | |
mars | 9660ebb4dd | |
mars | e944caa358 | |
mars | 7adc356c41 | |
mars | 482a0de030 | |
mars | a6b675dd11 | |
mars | d073dde8ca | |
mars | 2d098f8dea | |
mars | c06cd2d999 | |
mars | 780f13a015 | |
mars | c951c95650 | |
mars | c58ca051f4 | |
mars | ae7271a479 | |
mars | 4b46142f90 | |
mars | 03e596b219 | |
mars | b9416b922f | |
mars | 14f077ff97 | |
mars | 109af8793b | |
mars | 38674c2580 | |
mars | 70f9ca4405 | |
mars | c7981b5064 | |
mars | e4a279c230 | |
mars | 9920fea900 | |
mars | feec4de657 | |
mars | a5f279dfd1 | |
mars | 72d7e703c1 | |
mars | ce187a0381 | |
mars | 4007c40ba6 | |
mars | 0d4eb5d188 | |
mars | 756238feab |
|
@ -2,8 +2,10 @@
|
|||
members = [
|
||||
"apps/magpie",
|
||||
"apps/music-player",
|
||||
"apps/notifications",
|
||||
"apps/sandbox",
|
||||
"crates/script",
|
||||
"crates/textwrap",
|
||||
"scripts/music-player",
|
||||
"scripts/sao-ui",
|
||||
]
|
||||
|
@ -19,12 +21,13 @@ 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 = "0.38"
|
||||
wasmtime = "3"
|
||||
|
||||
[dependencies.font-kit]
|
||||
version = "*"
|
||||
|
|
|
@ -1,13 +1,39 @@
|
|||
# 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`
|
||||
|
||||
[`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.
|
||||
This repository (`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 even the source code for the documentation that you're
|
||||
currently reading.
|
||||
authored in Rust, and 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
|
||||
|
@ -16,13 +42,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),
|
||||
|
@ -46,6 +72,8 @@ 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,
|
||||
|
@ -71,7 +99,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
|
|
@ -13,8 +13,10 @@ 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}
|
||||
|
@ -24,4 +26,4 @@ slab = { version = "0.4", optional = true}
|
|||
|
||||
[features]
|
||||
async = ["dep:futures-util"]
|
||||
service = ["dep:anyhow", "dep:canary", "dep:glium", "dep:mio", "dep:mio-signals", "dep:parking_lot", "dep:slab"]
|
||||
service = ["dep:anyhow", "dep:canary", "dep:env_logger", "dep:glium", "dep:mio", "dep:mio-signals", "dep:parking_lot", "dep:slab"]
|
||||
|
|
|
@ -8,6 +8,12 @@ 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)?;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::io::{Read, Write};
|
||||
use std::marker::PhantomData;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{PathBuf, Path};
|
||||
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
|
@ -26,6 +26,13 @@ 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.
|
||||
|
@ -40,6 +47,7 @@ pub struct SendMessage {
|
|||
#[serde(tag = "kind")]
|
||||
pub enum MagpieServerMsg {
|
||||
CreatePanel(CreatePanel),
|
||||
ClosePanel(ClosePanel),
|
||||
SendMessage(SendMessage),
|
||||
}
|
||||
|
||||
|
@ -63,7 +71,6 @@ pub type ClientMessenger<T> = Messenger<T, MagpieClientMsg, MagpieServerMsg>;
|
|||
impl<T: Write> ClientMessenger<T> {
|
||||
pub fn send_panel_json<O: Serialize>(&mut self, id: PanelId, msg: &O) {
|
||||
let msg = serde_json::to_string(msg).unwrap();
|
||||
eprintln!("Sending message: {:?}", msg);
|
||||
|
||||
let _ = self.send(&MagpieServerMsg::SendMessage(SendMessage {
|
||||
id,
|
||||
|
@ -211,6 +218,16 @@ impl<T: Read, I: DeserializeOwned, O> Messenger<T, I, O> {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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::*;
|
||||
|
@ -220,7 +237,6 @@ mod async_messages {
|
|||
impl<T: AsyncWriteExt + Unpin> ClientMessenger<T> {
|
||||
pub async fn send_panel_json_async<O: Serialize>(&mut self, id: PanelId, msg: &O) {
|
||||
let msg = serde_json::to_string(msg).unwrap();
|
||||
eprintln!("Sending message: {:?}", msg);
|
||||
|
||||
let _ = self
|
||||
.send_async(&MagpieServerMsg::SendMessage(SendMessage {
|
||||
|
|
|
@ -47,7 +47,7 @@ impl Drop for Listener {
|
|||
fn drop(&mut self) {
|
||||
match std::fs::remove_file(&self.path) {
|
||||
Ok(_) => {}
|
||||
Err(e) => eprintln!("Could not delete UnixListener {:?}", e),
|
||||
Err(e) => log::error!("Could not delete UnixListener {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -75,20 +75,20 @@ impl Listener {
|
|||
use std::io::{Error, ErrorKind};
|
||||
match UnixStream::connect(&sock_path) {
|
||||
Ok(_) => {
|
||||
eprintln!("Socket is already in use. Another instance of Magpie may be running.");
|
||||
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 => {
|
||||
eprintln!("Found leftover socket; removing.");
|
||||
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),
|
||||
}
|
||||
|
||||
eprintln!("Making socket at: {:?}", sock_path);
|
||||
log::info!("Making socket at: {:?}", sock_path);
|
||||
let uds = UnixListener::bind(&sock_path)?;
|
||||
let path = sock_path.to_path_buf();
|
||||
Ok(Self { uds, path })
|
||||
|
@ -120,16 +120,17 @@ pub struct Client {
|
|||
impl Client {
|
||||
pub fn on_readable(&mut self) -> std::io::Result<bool> {
|
||||
if let Err(err) = self.messenger.flush_recv() {
|
||||
eprintln!("flush_recv() error: {:?}", err);
|
||||
log::error!("flush_recv() error: {:?}", err);
|
||||
}
|
||||
|
||||
while let Some(msg) = self.messenger.try_recv() {
|
||||
println!("Client #{}: {:?}", self.token.0, msg);
|
||||
log::debug!("Client #{}: {:?}", self.token.0, msg);
|
||||
match msg {
|
||||
MagpieServerMsg::CreatePanel(CreatePanel {
|
||||
id,
|
||||
protocol,
|
||||
script,
|
||||
init_msg,
|
||||
}) => {
|
||||
let mut data = self.data.write();
|
||||
|
||||
|
@ -146,9 +147,16 @@ impl Client {
|
|||
id: window,
|
||||
protocol,
|
||||
script,
|
||||
init_msg,
|
||||
};
|
||||
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 };
|
||||
|
@ -162,7 +170,7 @@ impl Client {
|
|||
}
|
||||
|
||||
pub fn disconnect(mut self) {
|
||||
println!("Client #{} disconnected", self.token.0);
|
||||
log::info!("Client #{} disconnected", self.token.0);
|
||||
|
||||
let mut transport = self.messenger.into_transport();
|
||||
let mut data = self.data.write();
|
||||
|
@ -262,9 +270,10 @@ impl Ipc {
|
|||
match self.listener.accept() {
|
||||
Ok((mut connection, address)) => {
|
||||
let token = Token(self.clients.vacant_key());
|
||||
println!(
|
||||
log::info!(
|
||||
"Accepting connection (Client #{}) from {:?}",
|
||||
token.0, address
|
||||
token.0,
|
||||
address
|
||||
);
|
||||
|
||||
let interest = Interest::READABLE;
|
||||
|
@ -288,7 +297,7 @@ impl Ipc {
|
|||
}
|
||||
} else if event.token() == self.signals_token {
|
||||
while let Some(received) = self.signals.receive()? {
|
||||
eprintln!("Received {:?} signal; exiting...", received);
|
||||
log::info!("Received {:?} signal; exiting...", received);
|
||||
let _ = self.window_sender.send_event(WindowMessage::Quit);
|
||||
self.quit = true;
|
||||
}
|
||||
|
@ -302,7 +311,7 @@ impl Ipc {
|
|||
self.clients.remove(event.token().0).disconnect();
|
||||
}
|
||||
} else {
|
||||
eprintln!("Unrecognized event token: {:?}", event);
|
||||
log::error!("Unrecognized event token: {:?}", event);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -316,7 +325,7 @@ impl Ipc {
|
|||
match self.poll(&mut events, Some(wait)) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
eprintln!("IPC poll error: {:?}", e);
|
||||
log::error!("IPC poll error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ pub enum WindowMessage {
|
|||
id: usize,
|
||||
protocol: String,
|
||||
script: PathBuf,
|
||||
init_msg: Vec<u8>,
|
||||
},
|
||||
CloseWindow {
|
||||
id: usize,
|
||||
|
@ -194,15 +195,20 @@ impl WindowStore {
|
|||
id,
|
||||
protocol,
|
||||
script,
|
||||
init_msg,
|
||||
} => {
|
||||
println!("Opening window {} with script {:?}", id, script);
|
||||
log::debug!("Opening window {} with script {:?}...", id, script);
|
||||
let start = std::time::Instant::now();
|
||||
let module = std::fs::read(script)?;
|
||||
let mut script = self.runtime.load_module(&module)?;
|
||||
let panel = script.create_panel(&protocol, vec![])?;
|
||||
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 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) {
|
||||
|
@ -242,7 +248,7 @@ impl WindowStore {
|
|||
Ok(false) => {}
|
||||
Ok(true) => *control_flow = ControlFlow::Exit,
|
||||
Err(err) => {
|
||||
eprintln!("Error while handling message {:?}:\n{}", event, err);
|
||||
log::error!("Error while handling message {:?}:\n{}", event, err);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
|
|
|
@ -10,12 +10,12 @@ 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 }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
smol = { version = "1.2", optional = true }
|
||||
zbus = { version = "3.5", optional = true }
|
||||
|
||||
[features]
|
||||
bin = ["dep:canary-magpie", "dep:futures-util", "dep:smol", "dep:zbus"]
|
||||
bin = ["dep:async-std", "dep:canary-magpie", "dep:futures-util", "dep:zbus"]
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use canary_magpie::protocol::{
|
||||
ClientMessenger, CreatePanel, MagpieClientMsg, MagpieServerMsg, RecvMessage, MAGPIE_SOCK,
|
||||
};
|
||||
use canary_music_player::*;
|
||||
use smol::net::unix::UnixStream;
|
||||
|
||||
use async_std::os::unix::net::UnixStream;
|
||||
|
||||
pub type MagpieClient = ClientMessenger<UnixStream>;
|
||||
|
||||
|
@ -125,13 +124,11 @@ async fn player_main(
|
|||
player: &PlayerProxy<'_>,
|
||||
magpie: &mut MagpieClient,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use futures_util::StreamExt;
|
||||
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;
|
||||
use futures_util::FutureExt;
|
||||
|
||||
loop {
|
||||
futures_util::select! {
|
||||
|
@ -188,78 +185,76 @@ async fn player_main(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[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();
|
||||
|
||||
smol::block_on(async {
|
||||
let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set");
|
||||
let sock_dir = Path::new(&sock_dir);
|
||||
let sock_path = sock_dir.join(MAGPIE_SOCK);
|
||||
let socket = UnixStream::connect(sock_path).await.unwrap();
|
||||
let mut magpie = MagpieClient::new(socket);
|
||||
let protocol = "tebibyte-media.desktop.music-player-controller".to_string();
|
||||
let script = std::path::PathBuf::from(&module_path);
|
||||
let msg = CreatePanel {
|
||||
id: 0,
|
||||
protocol,
|
||||
script,
|
||||
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 script = std::path::PathBuf::from(&module_path);
|
||||
let msg = CreatePanel {
|
||||
id: 0,
|
||||
protocol,
|
||||
script,
|
||||
init_msg: vec![],
|
||||
};
|
||||
|
||||
let msg = MagpieServerMsg::CreatePanel(msg);
|
||||
magpie.send_async(&msg).await.unwrap();
|
||||
|
||||
let dbus = zbus::Connection::session().await.unwrap();
|
||||
|
||||
let mut first_loop = true;
|
||||
let mut connected = false;
|
||||
|
||||
loop {
|
||||
if !first_loop {
|
||||
let wait = std::time::Duration::from_secs(1);
|
||||
async_std::task::sleep(wait).await;
|
||||
}
|
||||
|
||||
first_loop = false;
|
||||
|
||||
if connected {
|
||||
println!("Disconnected from MPRIS");
|
||||
let msg = InMsg::Disconnected;
|
||||
magpie.send_panel_json_async(0, &msg).await;
|
||||
connected = false;
|
||||
}
|
||||
|
||||
println!("Connecting to MPRIS...");
|
||||
|
||||
let player = match find_player(&dbus).await {
|
||||
Ok(Some(player)) => player,
|
||||
Ok(None) => {
|
||||
eprintln!("Couldn't find player");
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("D-Bus error while finding player: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let msg = MagpieServerMsg::CreatePanel(msg);
|
||||
magpie.send_async(&msg).await.unwrap();
|
||||
println!(
|
||||
"Connected to \"{}\" ({})",
|
||||
player.path().as_str(),
|
||||
player.destination().as_str()
|
||||
);
|
||||
connected = true;
|
||||
magpie.send_panel_json_async(0, &InMsg::Connected).await;
|
||||
|
||||
let dbus = zbus::Connection::session().await.unwrap();
|
||||
|
||||
let mut first_loop = true;
|
||||
let mut connected = false;
|
||||
|
||||
loop {
|
||||
if !first_loop {
|
||||
let wait = std::time::Duration::from_secs(1);
|
||||
std::thread::sleep(wait);
|
||||
}
|
||||
|
||||
first_loop = false;
|
||||
|
||||
if connected {
|
||||
println!("Disconnected from MPRIS");
|
||||
let msg = InMsg::Disconnected;
|
||||
magpie.send_panel_json_async(0, &msg).await;
|
||||
connected = false;
|
||||
}
|
||||
|
||||
println!("Connecting to MPRIS...");
|
||||
|
||||
let player = match find_player(&dbus).await {
|
||||
Ok(Some(player)) => player,
|
||||
Ok(None) => {
|
||||
eprintln!("Couldn't find player");
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("D-Bus error while finding player: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!(
|
||||
"Connected to \"{}\" ({})",
|
||||
player.path().as_str(),
|
||||
player.destination().as_str()
|
||||
);
|
||||
connected = true;
|
||||
magpie.send_panel_json_async(0, &InMsg::Connected).await;
|
||||
|
||||
match player_main(&player, &mut magpie).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
eprintln!("D-Bus error while connected to player: {:?}", err);
|
||||
}
|
||||
match player_main(&player, &mut magpie).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
eprintln!("D-Bus error while connected to player: {:?}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
[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"]
|
|
@ -0,0 +1,27 @@
|
|||
// 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 {}
|
|
@ -0,0 +1,143 @@
|
|||
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;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
book
|
|
@ -1,6 +0,0 @@
|
|||
[book]
|
||||
authors = ["marceline-cramer"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "Canary GUI Book"
|
|
@ -1,30 +0,0 @@
|
|||
# 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 +0,0 @@
|
|||
# Credits
|
|
@ -1 +0,0 @@
|
|||
# Ecosystem
|
|
@ -1 +0,0 @@
|
|||
# Finding Scripts
|
|
@ -1 +0,0 @@
|
|||
# Fonts
|
|
@ -1 +0,0 @@
|
|||
# Localization
|
|
@ -1 +0,0 @@
|
|||
# Messages
|
|
@ -1 +0,0 @@
|
|||
# Protocols
|
|
@ -1 +0,0 @@
|
|||
# Glossary
|
|
@ -1,16 +0,0 @@
|
|||
# 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.
|
|
@ -1 +0,0 @@
|
|||
# Backends
|
|
@ -1 +0,0 @@
|
|||
# Examples
|
|
@ -1,23 +0,0 @@
|
|||
# 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.
|
|
@ -1,4 +0,0 @@
|
|||
# Implementation
|
||||
|
||||
This chapter discusses the design and usage of [canary-rs](https://git.tebibyte.media/canary/canary-rs),
|
||||
the canonical implementation of Canary.
|
|
@ -1 +0,0 @@
|
|||
# Input
|
|
@ -1 +0,0 @@
|
|||
# Pointer
|
|
@ -1 +0,0 @@
|
|||
# Text
|
|
@ -1 +0,0 @@
|
|||
# Introduction
|
|
@ -1 +0,0 @@
|
|||
# Rendering
|
|
@ -1 +0,0 @@
|
|||
# Canvases
|
|
@ -1 +0,0 @@
|
|||
# Primitives
|
|
@ -1 +0,0 @@
|
|||
# Graphics State
|
|
@ -1 +0,0 @@
|
|||
# Tessellation
|
|
@ -1 +0,0 @@
|
|||
# Text
|
|
@ -101,16 +101,22 @@ impl Panel {
|
|||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
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 {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "canary-textwrap"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
canary-script = { path = "../script" }
|
||||
textwrap = "0.16"
|
|
@ -0,0 +1,248 @@
|
|||
// 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.
After Width: | Height: | Size: 68 KiB |
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
|
@ -11,7 +11,9 @@ crate-type = ["cdylib"]
|
|||
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"
|
||||
|
|
|
@ -7,6 +7,7 @@ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
|||
pub mod anim;
|
||||
pub mod main_menu;
|
||||
pub mod music_player;
|
||||
pub mod notifications;
|
||||
pub mod style;
|
||||
pub mod widgets;
|
||||
|
||||
|
@ -14,6 +15,7 @@ use api::*;
|
|||
use canary_script::*;
|
||||
use main_menu::MainMenuPanel;
|
||||
use music_player::MusicPlayerPanel;
|
||||
use notifications::NotificationPanel;
|
||||
use widgets::Widget;
|
||||
|
||||
export_abi!(bind_panel_impl);
|
||||
|
@ -24,6 +26,7 @@ fn bind_panel_impl(panel: Panel, protocol: Message, msg: Message) -> Box<dyn Pan
|
|||
|
||||
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),
|
||||
}
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
// 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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -166,7 +166,7 @@ pub struct Theme {
|
|||
|
||||
/// The global theme.
|
||||
pub const THEME: Theme = Theme {
|
||||
palette: ARCTICA_PALETTE,
|
||||
palette: ROSE_PINE_MOON_PALETTE,
|
||||
metrics: Metrics {
|
||||
surface_rounding: 5.0,
|
||||
},
|
||||
|
|
|
@ -19,11 +19,13 @@ 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>>;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,20 +2,21 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
use std::collections::{hash_map::DefaultHasher, HashMap};
|
||||
use std::hash::Hasher;
|
||||
use std::hash::{Hasher, BuildHasherDefault};
|
||||
use std::ops::DerefMut;
|
||||
use std::time::Instant;
|
||||
|
||||
use super::{Arc, Backend, Instance, PanelId, ScriptAbi};
|
||||
use crate::DrawCommand;
|
||||
|
||||
use canary_script::{Color, CursorEventKind, Rect, Vec2};
|
||||
use parking_lot::Mutex;
|
||||
use prehash::{DefaultPrehasher, Prehashed, Prehasher};
|
||||
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<Prehashed<u64>, wasmtime::Module, DefaultPrehasher>>;
|
||||
type ModuleCache = Mutex<HashMap<u64, wasmtime::Module, BuildHasherDefault<Passthru>>>;
|
||||
|
||||
pub struct WasmtimeBackend {
|
||||
engine: wasmtime::Engine,
|
||||
|
@ -24,6 +25,8 @@ pub struct WasmtimeBackend {
|
|||
|
||||
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);
|
||||
|
@ -40,21 +43,31 @@ impl WasmtimeBackend {
|
|||
}
|
||||
|
||||
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 hashed = hasher.finish();
|
||||
let hash = hasher.finish();
|
||||
let fmt_hash = format!("{:x}", hash);
|
||||
|
||||
let prehasher = DefaultPrehasher::new();
|
||||
let prehashed = prehasher.prehash(hashed);
|
||||
log::debug!("Loading module (hash: {})", fmt_hash);
|
||||
let mut cache = self.module_cache.lock();
|
||||
|
||||
let module = if let Some(module) = cache.get(&prehashed) {
|
||||
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(prehashed, module);
|
||||
cache.get(&prehashed).unwrap()
|
||||
cache.insert(hash, module);
|
||||
log::debug!("Built module in {:?} (hash: {})", start.elapsed(), fmt_hash);
|
||||
cache.get(&hash).unwrap()
|
||||
};
|
||||
|
||||
let mut store = wasmtime::Store::new(&self.engine, abi);
|
||||
|
@ -80,6 +93,12 @@ impl Backend for WasmtimeBackend {
|
|||
|
||||
let instance = Arc::new(instance);
|
||||
|
||||
log::debug!(
|
||||
"Loaded module in {:?} (hash: {})",
|
||||
start.elapsed(),
|
||||
fmt_hash
|
||||
);
|
||||
|
||||
Ok(instance)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ pub struct Runtime {
|
|||
|
||||
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()),
|
||||
|
|
|
@ -220,6 +220,7 @@ 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);
|
||||
|
||||
|
@ -241,14 +242,14 @@ impl FontStore {
|
|||
use font_kit::handle::Handle;
|
||||
use font_kit::properties::Properties;
|
||||
|
||||
println!("loading font family {}", title);
|
||||
log::info!("Finding font by family: {}", title);
|
||||
|
||||
let font_handle = self
|
||||
.source
|
||||
.select_best_match(&[FamilyName::Title(title.to_string())], &Properties::new())
|
||||
.unwrap();
|
||||
|
||||
println!("loading font file: {:?}", font_handle);
|
||||
log::info!("Loading font file: {:?}", font_handle);
|
||||
|
||||
let font_data = if let Handle::Path {
|
||||
path,
|
||||
|
|
Loading…
Reference in New Issue