Compare commits
16 Commits
Author | SHA1 | Date |
---|---|---|
mars | 0b878ca74e | |
mars | 6ac82404e6 | |
mars | d5e9e229dc | |
mars | 0bf02ba93b | |
mars | 24196adc3a | |
mars | 4c5dc489ac | |
mars | 72ba4bbcb9 | |
mars | eb5e1c6080 | |
mars | 5dbe325ea9 | |
mars | 76c697d5ab | |
mars | 69f067b62a | |
mars | b4da999c24 | |
mars | f0a2e1b57c | |
mars | c25449cbf9 | |
mars | 51c8627755 | |
mars | 13c395c880 |
14
Cargo.toml
14
Cargo.toml
|
@ -1,11 +1,11 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"apps/android",
|
||||
"apps/magpie",
|
||||
"apps/music-player",
|
||||
"apps/notifications",
|
||||
"apps/sandbox",
|
||||
"crates/script",
|
||||
"crates/textwrap",
|
||||
"renderers/wgpu",
|
||||
"scripts/music-player",
|
||||
"scripts/sao-ui",
|
||||
]
|
||||
|
@ -21,13 +21,19 @@ 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 = "*"
|
||||
default-features = false
|
||||
|
||||
[profile.dev]
|
||||
opt-level = "z"
|
||||
debug = false
|
||||
lto = "fat"
|
||||
strip = true
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "canary-android-demo"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib", "cdylib"]
|
||||
|
||||
[dependencies]
|
||||
canary = { path = "../.." }
|
||||
canary-wgpu = { path = "../../renderers/wgpu" }
|
||||
pollster = "0.2"
|
||||
ndk-glue = "0.7"
|
||||
winit = { version = "0.27", default-features = false }
|
||||
wgpu = "0.14"
|
|
@ -0,0 +1,159 @@
|
|||
use std::borrow::Cow;
|
||||
use winit::{
|
||||
event::{Event, WindowEvent},
|
||||
event_loop::{ControlFlow, EventLoop},
|
||||
window::{Window, WindowBuilder},
|
||||
};
|
||||
|
||||
async fn run(event_loop: EventLoop<()>, window: Window) {
|
||||
println!("Waiting for NativeScreen");
|
||||
loop {
|
||||
match ndk_glue::native_window().as_ref() {
|
||||
Some(_) => {
|
||||
println!("NativeScreen Found:{:?}", ndk_glue::native_window());
|
||||
break;
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
let size = window.inner_size();
|
||||
|
||||
println!("Creating instance...");
|
||||
let instance = wgpu::Instance::new(wgpu::Backends::all());
|
||||
let surface = unsafe { instance.create_surface(&window) };
|
||||
println!("Requesting adapter...");
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::default(),
|
||||
force_fallback_adapter: false,
|
||||
// Request an adapter which can render to our surface
|
||||
compatible_surface: Some(&surface),
|
||||
})
|
||||
.await
|
||||
.expect("Failed to find an appropriate adapter");
|
||||
|
||||
// Create the logical device and command queue
|
||||
println!("Creating logical device...");
|
||||
let (device, queue) = adapter
|
||||
.request_device(
|
||||
&wgpu::DeviceDescriptor {
|
||||
label: None,
|
||||
features: wgpu::Features::empty(),
|
||||
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the swapchain.
|
||||
limits: wgpu::Limits::downlevel_webgl2_defaults()
|
||||
.using_resolution(adapter.limits()),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create device");
|
||||
|
||||
println!("No more async code!");
|
||||
|
||||
// Load the shaders from disk
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: None,
|
||||
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))),
|
||||
});
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: None,
|
||||
bind_group_layouts: &[],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let swapchain_format = wgpu::TextureFormat::Rgba8UnormSrgb;
|
||||
|
||||
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: "vs_main",
|
||||
buffers: &[],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: "fs_main",
|
||||
targets: &[Some(swapchain_format.into())],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview: None,
|
||||
});
|
||||
|
||||
let mut config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: swapchain_format,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: wgpu::PresentMode::Fifo,
|
||||
alpha_mode: wgpu::CompositeAlphaMode::Auto,
|
||||
};
|
||||
|
||||
surface.configure(&device, &config);
|
||||
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
// Have the closure take ownership of the resources.
|
||||
// `event_loop.run` never returns, therefore we must do this to ensure
|
||||
// the resources are properly cleaned up.
|
||||
let _ = (&instance, &adapter, &shader, &pipeline_layout);
|
||||
|
||||
*control_flow = ControlFlow::Wait;
|
||||
match event {
|
||||
Event::WindowEvent {
|
||||
event: WindowEvent::Resized(size),
|
||||
..
|
||||
} => {
|
||||
// Reconfigure the surface with the new size
|
||||
config.width = size.width;
|
||||
config.height = size.height;
|
||||
surface.configure(&device, &config);
|
||||
// On macos the window needs to be redrawn manually after resizing
|
||||
window.request_redraw();
|
||||
}
|
||||
Event::RedrawRequested(_) => {
|
||||
let frame = surface
|
||||
.get_current_texture()
|
||||
.expect("Failed to acquire next swap chain texture");
|
||||
let view = frame
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let mut encoder =
|
||||
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
|
||||
{
|
||||
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: None,
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::GREEN),
|
||||
store: true,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
});
|
||||
rpass.set_pipeline(&render_pipeline);
|
||||
rpass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
queue.submit(Some(encoder.finish()));
|
||||
frame.present();
|
||||
}
|
||||
Event::WindowEvent {
|
||||
event: WindowEvent::CloseRequested,
|
||||
..
|
||||
} => *control_flow = ControlFlow::Exit,
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))]
|
||||
pub fn main() {
|
||||
let event_loop = EventLoop::new();
|
||||
let window = winit::window::Window::new(&event_loop).unwrap();
|
||||
pollster::block_on(run(event_loop, window));
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> {
|
||||
let x = f32(i32(in_vertex_index) - 1);
|
||||
let y = f32(i32(in_vertex_index & 1u) * 2 - 1);
|
||||
return vec4<f32>(x, y, 0.0, 1.0);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main() -> @location(0) vec4<f32> {
|
||||
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
|
@ -13,17 +13,27 @@ required-features = ["service"]
|
|||
anyhow = { version = "1", optional = true }
|
||||
byteorder = "1.4"
|
||||
canary = { path = "../..", optional = true }
|
||||
env_logger = { version = "0.10", optional = true }
|
||||
canary-wgpu = { path = "../../renderers/wgpu", 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}
|
||||
parking_lot = { version = "0.12", optional = true }
|
||||
pollster = { version = "0.2", optional = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
slab = { version = "0.4", optional = true}
|
||||
slab = { version = "0.4", optional = true }
|
||||
winit = { version = "0.27", 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:canary-wgpu",
|
||||
"dep:mio",
|
||||
"dep:mio-signals",
|
||||
"dep:parking_lot",
|
||||
"dep:pollster",
|
||||
"dep:slab",
|
||||
"dep:winit",
|
||||
]
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use glium::glutin::event_loop::EventLoopBuilder;
|
||||
use winit::event_loop::EventLoopBuilder;
|
||||
|
||||
use canary_magpie::service::*;
|
||||
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, Path};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
|
@ -26,13 +26,6 @@ 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,7 +40,6 @@ pub struct SendMessage {
|
|||
#[serde(tag = "kind")]
|
||||
pub enum MagpieServerMsg {
|
||||
CreatePanel(CreatePanel),
|
||||
ClosePanel(ClosePanel),
|
||||
SendMessage(SendMessage),
|
||||
}
|
||||
|
||||
|
@ -71,6 +63,7 @@ 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,
|
||||
|
@ -218,16 +211,6 @@ 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::*;
|
||||
|
@ -237,6 +220,7 @@ 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 {
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
// 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};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Vertex {
|
||||
pub position: [f32; 2],
|
||||
pub color: [u8; 4],
|
||||
}
|
||||
|
||||
glium::implement_vertex!(Vertex, position normalize(false), color normalize(true));
|
||||
|
||||
impl Vertex {
|
||||
pub fn from_canary(size: Vec2, v: &canary::MeshVertex) -> Self {
|
||||
// TODO do this in the vertex shader with a size uniform
|
||||
let (r, g, b, a) = v.color.to_rgba_unmultiplied();
|
||||
Self {
|
||||
position: [
|
||||
(v.position.x / size.x) * 2.0 - 1.0,
|
||||
(v.position.y / size.y) * -2.0 + 1.0,
|
||||
],
|
||||
color: [r, g, b, a],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const VERTEX_SHADER_SRC: &str = r#"
|
||||
#version 330
|
||||
|
||||
in vec2 position;
|
||||
in vec4 color;
|
||||
|
||||
out vec4 frag_color;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
frag_color = color;
|
||||
}
|
||||
"#;
|
||||
|
||||
const FRAGMENT_SHADER_SRC: &str = r#"
|
||||
#version 330
|
||||
|
||||
in vec4 frag_color;
|
||||
|
||||
out vec4 fb_color;
|
||||
|
||||
void main() {
|
||||
fb_color = frag_color;
|
||||
}
|
||||
"#;
|
||||
|
||||
pub struct Graphics {
|
||||
pub display: glium::Display,
|
||||
pub program: glium::Program,
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
Self { display, program }
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, commands: &[DrawCommand]) {
|
||||
let mut joined_vs: Vec<Vertex> = Vec::new();
|
||||
let mut joined_is = Vec::new();
|
||||
|
||||
let (width, height) = {
|
||||
let size = self.display.gl_window().window().inner_size();
|
||||
let (width, height) = (size.width as f32, size.height as f32);
|
||||
(width * PX_PER_MM, height * PX_PER_MM)
|
||||
};
|
||||
|
||||
let size = Vec2 {
|
||||
x: width,
|
||||
y: height,
|
||||
};
|
||||
|
||||
for command in commands.iter() {
|
||||
match command {
|
||||
canary::DrawCommand::Mesh { vertices, indices } => {
|
||||
let voff = joined_vs.len() as canary::MeshIndex;
|
||||
joined_vs.extend(vertices.iter().map(|v| Vertex::from_canary(size, v)));
|
||||
joined_is.extend(indices.iter().map(|i| i + voff));
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
let vertex_buffer = glium::VertexBuffer::new(&self.display, &joined_vs).unwrap();
|
||||
let index_buffer = glium::IndexBuffer::new(
|
||||
&self.display,
|
||||
glium::index::PrimitiveType::TrianglesList,
|
||||
&joined_is,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let params = glium::DrawParameters {
|
||||
blend: glium::Blend::alpha_blending(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut target = self.display.draw();
|
||||
target.clear_color(0.0, 0.0, 0.0, 0.0);
|
||||
target
|
||||
.draw(
|
||||
&vertex_buffer,
|
||||
&index_buffer,
|
||||
&self.program,
|
||||
&glium::uniforms::EmptyUniforms,
|
||||
¶ms,
|
||||
)
|
||||
.unwrap();
|
||||
target.finish().unwrap();
|
||||
}
|
||||
}
|
|
@ -47,7 +47,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -75,20 +75,20 @@ impl Listener {
|
|||
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.");
|
||||
eprintln!("Socket is already in use. Another instance of Magpie may be running.");
|
||||
let kind = ErrorKind::AddrInUse;
|
||||
let error = Error::new(kind, "Socket is already in use.");
|
||||
return Err(error);
|
||||
}
|
||||
Err(ref err) if err.kind() == ErrorKind::ConnectionRefused => {
|
||||
log::warn!("Found leftover socket; removing.");
|
||||
eprintln!("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);
|
||||
eprintln!("Making socket at: {:?}", sock_path);
|
||||
let uds = UnixListener::bind(&sock_path)?;
|
||||
let path = sock_path.to_path_buf();
|
||||
Ok(Self { uds, path })
|
||||
|
@ -120,17 +120,16 @@ pub struct Client {
|
|||
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);
|
||||
eprintln!("flush_recv() error: {:?}", err);
|
||||
}
|
||||
|
||||
while let Some(msg) = self.messenger.try_recv() {
|
||||
log::debug!("Client #{}: {:?}", self.token.0, msg);
|
||||
println!("Client #{}: {:?}", self.token.0, msg);
|
||||
match msg {
|
||||
MagpieServerMsg::CreatePanel(CreatePanel {
|
||||
id,
|
||||
protocol,
|
||||
script,
|
||||
init_msg,
|
||||
}) => {
|
||||
let mut data = self.data.write();
|
||||
|
||||
|
@ -147,16 +146,9 @@ 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 };
|
||||
|
@ -170,7 +162,7 @@ impl Client {
|
|||
}
|
||||
|
||||
pub fn disconnect(mut self) {
|
||||
log::info!("Client #{} disconnected", self.token.0);
|
||||
println!("Client #{} disconnected", self.token.0);
|
||||
|
||||
let mut transport = self.messenger.into_transport();
|
||||
let mut data = self.data.write();
|
||||
|
@ -270,10 +262,9 @@ impl Ipc {
|
|||
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,7 +288,7 @@ 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;
|
||||
}
|
||||
|
@ -311,7 +302,7 @@ impl Ipc {
|
|||
self.clients.remove(event.token().0).disconnect();
|
||||
}
|
||||
} else {
|
||||
log::error!("Unrecognized event token: {:?}", event);
|
||||
eprintln!("Unrecognized event token: {:?}", event);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -325,7 +316,7 @@ impl Ipc {
|
|||
match self.poll(&mut events, Some(wait)) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!("IPC poll error: {:?}", e);
|
||||
eprintln!("IPC poll error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// Copyright (c) 2022 Marceline Cramer
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub mod gl;
|
||||
pub mod ipc;
|
||||
pub mod window;
|
||||
|
|
|
@ -3,16 +3,17 @@
|
|||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use canary::{CursorEventKind, Panel, Runtime, Vec2, PX_PER_MM};
|
||||
use glium::backend::glutin::DisplayCreationError;
|
||||
use glium::{glutin, Surface};
|
||||
use glutin::event::{ElementState, Event, MouseButton, WindowEvent};
|
||||
use glutin::event_loop::{ControlFlow, EventLoop, EventLoopProxy, EventLoopWindowTarget};
|
||||
use glutin::window::WindowId;
|
||||
use canary_wgpu::{wgpu, DrawTarget, Renderer};
|
||||
use pollster::FutureExt;
|
||||
use winit::dpi::PhysicalSize;
|
||||
use winit::event::{ElementState, Event, MouseButton, WindowEvent};
|
||||
use winit::event_loop::{ControlFlow, EventLoop, EventLoopProxy, EventLoopWindowTarget};
|
||||
use winit::window::{WindowBuilder, WindowId};
|
||||
|
||||
use crate::service::gl::Graphics;
|
||||
use crate::service::ipc::{IpcMessage, IpcMessageSender};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -21,7 +22,6 @@ pub enum WindowMessage {
|
|||
id: usize,
|
||||
protocol: String,
|
||||
script: PathBuf,
|
||||
init_msg: Vec<u8>,
|
||||
},
|
||||
CloseWindow {
|
||||
id: usize,
|
||||
|
@ -38,7 +38,12 @@ pub type WindowMessageSender = EventLoopProxy<WindowMessage>;
|
|||
pub struct Window {
|
||||
pub ipc_sender: IpcMessageSender,
|
||||
pub ipc_id: usize,
|
||||
pub graphics: Graphics,
|
||||
pub window: winit::window::Window,
|
||||
pub surface: wgpu::Surface,
|
||||
pub surface_config: wgpu::SurfaceConfiguration,
|
||||
pub device: Arc<wgpu::Device>,
|
||||
pub queue: Arc<wgpu::Queue>,
|
||||
pub renderer: Arc<Renderer>,
|
||||
pub panel: Panel,
|
||||
pub last_update: Instant,
|
||||
pub cursor_pos: Vec2,
|
||||
|
@ -50,21 +55,40 @@ impl Window {
|
|||
ipc_sender: IpcMessageSender,
|
||||
ipc_id: usize,
|
||||
panel: Panel,
|
||||
instance: &wgpu::Instance,
|
||||
adapter: &wgpu::Adapter,
|
||||
device: Arc<wgpu::Device>,
|
||||
queue: Arc<wgpu::Queue>,
|
||||
renderer: Arc<Renderer>,
|
||||
event_loop: &EventLoopWindowTarget<WindowMessage>,
|
||||
) -> Result<Self, DisplayCreationError> {
|
||||
let wb = glutin::window::WindowBuilder::new()
|
||||
) -> anyhow::Result<Self> {
|
||||
let window = WindowBuilder::new()
|
||||
.with_transparent(true)
|
||||
.with_decorations(false);
|
||||
let cb = glutin::ContextBuilder::new()
|
||||
.with_vsync(true)
|
||||
.with_multisampling(4);
|
||||
let display = glium::Display::new(wb, cb, &event_loop)?;
|
||||
let graphics = Graphics::new(display);
|
||||
.build(&event_loop)?;
|
||||
let surface = unsafe { instance.create_surface(&window) };
|
||||
let last_update = Instant::now();
|
||||
|
||||
let size = window.inner_size();
|
||||
|
||||
let surface_config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: wgpu::TextureFormat::Bgra8Unorm,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: wgpu::PresentMode::Fifo,
|
||||
};
|
||||
|
||||
surface.configure(&device, &surface_config);
|
||||
|
||||
Ok(Self {
|
||||
ipc_sender,
|
||||
ipc_id,
|
||||
graphics,
|
||||
window,
|
||||
surface,
|
||||
surface_config,
|
||||
device,
|
||||
queue,
|
||||
renderer,
|
||||
panel,
|
||||
last_update,
|
||||
cursor_pos: Vec2::ZERO,
|
||||
|
@ -73,11 +97,11 @@ impl Window {
|
|||
}
|
||||
|
||||
pub fn get_id(&self) -> WindowId {
|
||||
self.graphics.display.gl_window().window().id()
|
||||
self.window.id()
|
||||
}
|
||||
|
||||
pub fn request_redraw(&mut self) {
|
||||
self.graphics.display.gl_window().window().request_redraw();
|
||||
self.window.request_redraw();
|
||||
}
|
||||
|
||||
/// Receives all messages from the script and forwards them to IPC.
|
||||
|
@ -99,8 +123,24 @@ impl Window {
|
|||
}
|
||||
|
||||
pub fn draw(&mut self) {
|
||||
let output = self.surface.get_current_texture().unwrap();
|
||||
let view = output.texture.create_view(&Default::default());
|
||||
let commands = self.panel.draw();
|
||||
self.graphics.draw(&commands);
|
||||
|
||||
let size = Vec2::new(
|
||||
self.surface_config.width as f32,
|
||||
self.surface_config.height as f32,
|
||||
) * PX_PER_MM;
|
||||
|
||||
let target = DrawTarget {
|
||||
texture: &view,
|
||||
size,
|
||||
};
|
||||
|
||||
self.renderer.render(target, &commands);
|
||||
|
||||
output.present();
|
||||
|
||||
self.recv_messages();
|
||||
}
|
||||
|
||||
|
@ -109,17 +149,23 @@ impl Window {
|
|||
self.recv_messages();
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, new_size: Vec2) {
|
||||
self.panel.on_resize(new_size);
|
||||
pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
|
||||
let mm = Vec2::new(new_size.width as f32, new_size.height as f32) * PX_PER_MM;
|
||||
self.panel.on_resize(mm);
|
||||
self.recv_messages();
|
||||
self.window.request_redraw();
|
||||
|
||||
if new_size.width > 0 && new_size.height > 0 {
|
||||
self.surface_config.width = new_size.width;
|
||||
self.surface_config.height = new_size.height;
|
||||
self.surface.configure(&self.device, &self.surface_config);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_event(&mut self, event: WindowEvent) {
|
||||
match event {
|
||||
WindowEvent::Resized(size) => {
|
||||
self.resize(Vec2::new(size.width as f32, size.height as f32) * PX_PER_MM);
|
||||
self.request_redraw()
|
||||
}
|
||||
WindowEvent::Resized(size) => self.resize(size),
|
||||
WindowEvent::ScaleFactorChanged { new_inner_size, .. } => self.resize(*new_inner_size),
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
let x = position.x as f32 * PX_PER_MM;
|
||||
let y = position.y as f32 * PX_PER_MM;
|
||||
|
@ -163,6 +209,11 @@ pub struct WindowStore {
|
|||
pub ipc_to_window: HashMap<usize, WindowId>,
|
||||
pub windows: HashMap<WindowId, Window>,
|
||||
pub runtime: Runtime,
|
||||
pub instance: wgpu::Instance,
|
||||
pub adapter: wgpu::Adapter,
|
||||
pub device: Arc<wgpu::Device>,
|
||||
pub queue: Arc<wgpu::Queue>,
|
||||
pub renderer: Arc<Renderer>,
|
||||
}
|
||||
|
||||
impl WindowStore {
|
||||
|
@ -170,11 +221,42 @@ impl WindowStore {
|
|||
let backend = canary::backend::make_default_backend().unwrap();
|
||||
let runtime = Runtime::new(backend).unwrap();
|
||||
|
||||
let instance = wgpu::Instance::new(wgpu::Backends::all());
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptionsBase {
|
||||
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||
force_fallback_adapter: false,
|
||||
compatible_surface: None,
|
||||
})
|
||||
.block_on()
|
||||
.unwrap();
|
||||
let (device, queue) = adapter
|
||||
.request_device(
|
||||
&wgpu::DeviceDescriptor {
|
||||
label: None,
|
||||
features: wgpu::Features::empty(),
|
||||
limits: wgpu::Limits::default(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.block_on()
|
||||
.unwrap();
|
||||
|
||||
let device = Arc::new(device);
|
||||
let queue = Arc::new(queue);
|
||||
let renderer = Renderer::new(device.to_owned(), queue.to_owned());
|
||||
let renderer = Arc::new(renderer);
|
||||
|
||||
Self {
|
||||
ipc_sender,
|
||||
ipc_to_window: Default::default(),
|
||||
windows: Default::default(),
|
||||
runtime,
|
||||
instance,
|
||||
adapter,
|
||||
device,
|
||||
queue,
|
||||
renderer,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -195,20 +277,27 @@ impl WindowStore {
|
|||
id,
|
||||
protocol,
|
||||
script,
|
||||
init_msg,
|
||||
} => {
|
||||
log::debug!("Opening window {} with script {:?}...", id, script);
|
||||
let start = std::time::Instant::now();
|
||||
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(&protocol, vec![])?;
|
||||
|
||||
let window = Window::new(
|
||||
self.ipc_sender.to_owned(),
|
||||
id,
|
||||
panel,
|
||||
&self.instance,
|
||||
&self.adapter,
|
||||
self.device.to_owned(),
|
||||
self.queue.to_owned(),
|
||||
self.renderer.to_owned(),
|
||||
&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 +337,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);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
|
|
|
@ -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:async-std", "dep:canary-magpie", "dep:futures-util", "dep:zbus"]
|
||||
bin = ["dep:canary-magpie", "dep:futures-util", "dep:smol", "dep:zbus"]
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
// 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 async_std::os::unix::net::UnixStream;
|
||||
use smol::net::unix::UnixStream;
|
||||
|
||||
pub type MagpieClient = ClientMessenger<UnixStream>;
|
||||
|
||||
|
@ -124,11 +125,13 @@ async fn player_main(
|
|||
player: &PlayerProxy<'_>,
|
||||
magpie: &mut MagpieClient,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use futures_util::{FutureExt, StreamExt};
|
||||
use futures_util::StreamExt;
|
||||
let mut playback_status = player.receive_playback_status_changed().await.fuse();
|
||||
let mut metadata_tracker = player.receive_metadata_changed().await.fuse();
|
||||
let mut position_tracker = player.receive_position_changed().await.fuse();
|
||||
|
||||
let mut metadata = Metadata::update_new(magpie, player.metadata().await?).await;
|
||||
use futures_util::FutureExt;
|
||||
|
||||
loop {
|
||||
futures_util::select! {
|
||||
|
@ -185,76 +188,78 @@ async fn player_main(
|
|||
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 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;
|
||||
}
|
||||
smol::block_on(async {
|
||||
let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set");
|
||||
let sock_dir = Path::new(&sock_dir);
|
||||
let sock_path = sock_dir.join(MAGPIE_SOCK);
|
||||
let socket = UnixStream::connect(sock_path).await.unwrap();
|
||||
let mut magpie = MagpieClient::new(socket);
|
||||
let protocol = "tebibyte-media.desktop.music-player-controller".to_string();
|
||||
let script = std::path::PathBuf::from(&module_path);
|
||||
let msg = CreatePanel {
|
||||
id: 0,
|
||||
protocol,
|
||||
script,
|
||||
};
|
||||
|
||||
println!(
|
||||
"Connected to \"{}\" ({})",
|
||||
player.path().as_str(),
|
||||
player.destination().as_str()
|
||||
);
|
||||
connected = true;
|
||||
magpie.send_panel_json_async(0, &InMsg::Connected).await;
|
||||
let msg = MagpieServerMsg::CreatePanel(msg);
|
||||
magpie.send_async(&msg).await.unwrap();
|
||||
|
||||
match player_main(&player, &mut magpie).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
eprintln!("D-Bus error while connected to player: {:?}", err);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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"]
|
|
@ -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 {}
|
|
@ -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;
|
||||
}
|
|
@ -6,4 +6,5 @@ license = "AGPL-3.0-or-later"
|
|||
|
||||
[dependencies]
|
||||
canary = { path = "../.." }
|
||||
eframe = "0.18"
|
||||
canary-wgpu = { path = "../../renderers/wgpu" }
|
||||
eframe = { version = "0.19", features = ["wgpu"] }
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use canary::{CursorEventKind, Panel, Runtime, Script, PX_PER_MM};
|
||||
use canary_wgpu::Renderer;
|
||||
use eframe::egui;
|
||||
use std::time::Instant;
|
||||
|
||||
|
@ -13,20 +14,19 @@ fn main() {
|
|||
.to_owned();
|
||||
|
||||
let native_options = eframe::NativeOptions {
|
||||
renderer: eframe::Renderer::Wgpu,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"Canary Sandbox",
|
||||
native_options,
|
||||
Box::new(move |cc| {
|
||||
cc.egui_ctx.set_visuals(egui::Visuals::dark());
|
||||
Box::new(App::new(&module_path))
|
||||
}),
|
||||
Box::new(move |cc| Box::new(App::new(cc, &module_path))),
|
||||
);
|
||||
}
|
||||
|
||||
struct App {
|
||||
renderer: Renderer,
|
||||
script: Script,
|
||||
panels: Vec<PanelWindow>,
|
||||
next_idx: usize,
|
||||
|
@ -37,13 +37,23 @@ struct App {
|
|||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(module_path: &str) -> Self {
|
||||
pub fn new(cc: &eframe::CreationContext, module_path: &str) -> Self {
|
||||
cc.egui_ctx.set_visuals(egui::Visuals::dark());
|
||||
|
||||
let wgpu_state = cc
|
||||
.wgpu_render_state
|
||||
.as_ref()
|
||||
.expect("eframe should be using wgpu but has no wgpu render state");
|
||||
|
||||
let renderer = Renderer::new(wgpu_state.device.to_owned(), wgpu_state.queue.to_owned());
|
||||
|
||||
let backend = canary::backend::make_default_backend().unwrap();
|
||||
let runtime = Runtime::new(backend).unwrap();
|
||||
let module = std::fs::read(module_path).unwrap();
|
||||
let script = runtime.load_module(&module).unwrap();
|
||||
|
||||
Self {
|
||||
renderer,
|
||||
script,
|
||||
panels: vec![],
|
||||
next_idx: 0,
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
book
|
|
@ -0,0 +1,6 @@
|
|||
[book]
|
||||
authors = ["marceline-cramer"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "Canary GUI Book"
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
|||
# Credits
|
|
@ -0,0 +1 @@
|
|||
# Ecosystem
|
|
@ -0,0 +1 @@
|
|||
# Finding Scripts
|
|
@ -0,0 +1 @@
|
|||
# Fonts
|
|
@ -0,0 +1 @@
|
|||
# Localization
|
|
@ -0,0 +1 @@
|
|||
# Messages
|
|
@ -0,0 +1 @@
|
|||
# Protocols
|
|
@ -0,0 +1 @@
|
|||
# Glossary
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
# Backends
|
|
@ -0,0 +1 @@
|
|||
# Examples
|
|
@ -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.
|
|
@ -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
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
# Input
|
|
@ -0,0 +1 @@
|
|||
# Pointer
|
|
@ -0,0 +1 @@
|
|||
# Text
|
|
@ -0,0 +1 @@
|
|||
# Introduction
|
|
@ -0,0 +1 @@
|
|||
# Rendering
|
|
@ -0,0 +1 @@
|
|||
# Canvases
|
|
@ -0,0 +1 @@
|
|||
# Primitives
|
|
@ -0,0 +1 @@
|
|||
# Graphics State
|
|
@ -0,0 +1 @@
|
|||
# Tessellation
|
|
@ -0,0 +1 @@
|
|||
# Text
|
|
@ -101,22 +101,16 @@ impl Panel {
|
|||
}
|
||||
|
||||
#[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 {
|
||||
|
|
|
@ -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"
|
|
@ -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,
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "canary-wgpu"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bytemuck = { version = "1", features = ["derive"] }
|
||||
canary = { path = "../.." }
|
||||
wgpu = "0.13.1"
|
|
@ -0,0 +1,162 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use canary::{DrawCommand, Vec2};
|
||||
use wgpu::{util::*, *};
|
||||
|
||||
pub use wgpu;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Pod, Zeroable)]
|
||||
pub struct Vertex {
|
||||
pub position: [f32; 2],
|
||||
pub color: u32,
|
||||
}
|
||||
|
||||
impl Vertex {
|
||||
pub fn from_canary(size: Vec2, v: &canary::MeshVertex) -> Self {
|
||||
// TODO do this in the vertex shader with a size uniform
|
||||
Self {
|
||||
position: [
|
||||
(v.position.x / size.x) * 2.0 - 1.0,
|
||||
(v.position.y / size.y) * -2.0 + 1.0,
|
||||
],
|
||||
color: v.color.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const BUFFER_LAYOUT: VertexBufferLayout<'static> = VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<Self>() as BufferAddress,
|
||||
step_mode: VertexStepMode::Vertex,
|
||||
attributes: &[
|
||||
VertexAttribute {
|
||||
format: VertexFormat::Float32x2,
|
||||
offset: 0,
|
||||
shader_location: 0,
|
||||
},
|
||||
VertexAttribute {
|
||||
format: VertexFormat::Unorm8x4,
|
||||
offset: std::mem::size_of::<[f32; 2]>() as BufferAddress,
|
||||
shader_location: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
pub struct Renderer {
|
||||
device: Arc<Device>,
|
||||
queue: Arc<Queue>,
|
||||
pipeline_layout: PipelineLayout,
|
||||
pipeline: RenderPipeline,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub fn new(device: Arc<Device>, queue: Arc<Queue>) -> Self {
|
||||
let shader = device.create_shader_module(include_wgsl!("shader.wgsl"));
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: Some("Canary Pipeline Layout"),
|
||||
bind_group_layouts: &[],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||
label: Some("Canary Render Pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: VertexState {
|
||||
module: &shader,
|
||||
entry_point: "vs_main",
|
||||
buffers: &[Vertex::BUFFER_LAYOUT],
|
||||
},
|
||||
primitive: PrimitiveState {
|
||||
topology: PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: FrontFace::Ccw,
|
||||
cull_mode: None,
|
||||
unclipped_depth: false,
|
||||
polygon_mode: PolygonMode::Fill,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
fragment: Some(FragmentState {
|
||||
module: &shader,
|
||||
entry_point: "fs_main",
|
||||
targets: &[Some(ColorTargetState {
|
||||
format: TextureFormat::Bgra8Unorm,
|
||||
blend: Some(BlendState::ALPHA_BLENDING),
|
||||
write_mask: ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview: None,
|
||||
});
|
||||
|
||||
Self {
|
||||
device,
|
||||
queue,
|
||||
pipeline_layout,
|
||||
pipeline,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self, target: DrawTarget<'_>, cmds: &[DrawCommand]) {
|
||||
let size = target.size;
|
||||
|
||||
let mut joined_vs = Vec::new();
|
||||
let mut joined_is = Vec::new();
|
||||
|
||||
for cmd in cmds.iter() {
|
||||
match cmd {
|
||||
canary::DrawCommand::Mesh { vertices, indices } => {
|
||||
let voff = joined_vs.len() as canary::MeshIndex;
|
||||
joined_vs.extend(vertices.iter().map(|v| Vertex::from_canary(size, v)));
|
||||
joined_is.extend(indices.iter().map(|i| i + voff));
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
let vertex_buffer = self.device.create_buffer_init(&BufferInitDescriptor {
|
||||
label: Some("Canary Vertex Buffer"),
|
||||
contents: bytemuck::cast_slice(joined_vs.as_slice()),
|
||||
usage: BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
let index_buffer = self.device.create_buffer_init(&BufferInitDescriptor {
|
||||
label: Some("Canary Index Buffer"),
|
||||
contents: bytemuck::cast_slice(joined_is.as_slice()),
|
||||
usage: BufferUsages::INDEX,
|
||||
});
|
||||
|
||||
let mut cmd_encoder = self.device.create_command_encoder(&Default::default());
|
||||
|
||||
let mut rp = cmd_encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
label: Some("Canary Render Pass"),
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: target.texture,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::Clear(Color::TRANSPARENT),
|
||||
store: true,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
});
|
||||
|
||||
let indices = 0..(joined_is.len() as u32);
|
||||
rp.set_vertex_buffer(0, vertex_buffer.slice(..));
|
||||
rp.set_index_buffer(index_buffer.slice(..), IndexFormat::Uint32);
|
||||
rp.set_pipeline(&self.pipeline);
|
||||
rp.draw_indexed(indices, 0, 0..1);
|
||||
|
||||
drop(rp);
|
||||
|
||||
let cmd_buf = cmd_encoder.finish();
|
||||
self.queue.submit(std::iter::once(cmd_buf));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DrawTarget<'a> {
|
||||
pub texture: &'a TextureView,
|
||||
pub size: Vec2,
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) color: vec4<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
@location(0) position: vec2<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
) -> VertexOutput {
|
||||
var result: VertexOutput;
|
||||
result.position = vec4<f32>(position, 0.0, 1.0);
|
||||
result.color = color;
|
||||
return result;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(vertex: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return vertex.color.abgr;
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 68 KiB |
Binary file not shown.
Before Width: | Height: | Size: 116 KiB |
|
@ -11,9 +11,7 @@ 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,7 +7,6 @@ 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;
|
||||
|
||||
|
@ -15,7 +14,6 @@ use api::*;
|
|||
use canary_script::*;
|
||||
use main_menu::MainMenuPanel;
|
||||
use music_player::MusicPlayerPanel;
|
||||
use notifications::NotificationPanel;
|
||||
use widgets::Widget;
|
||||
|
||||
export_abi!(bind_panel_impl);
|
||||
|
@ -26,7 +24,6 @@ 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),
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -166,7 +166,7 @@ pub struct Theme {
|
|||
|
||||
/// The global theme.
|
||||
pub const THEME: Theme = Theme {
|
||||
palette: ROSE_PINE_MOON_PALETTE,
|
||||
palette: ARCTICA_PALETTE,
|
||||
metrics: Metrics {
|
||||
surface_rounding: 5.0,
|
||||
},
|
||||
|
|
|
@ -19,13 +19,11 @@ 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,21 +2,20 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
use std::collections::{hash_map::DefaultHasher, HashMap};
|
||||
use std::hash::{Hasher, BuildHasherDefault};
|
||||
use std::hash::Hasher;
|
||||
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::Passthru;
|
||||
use prehash::{DefaultPrehasher, Prehashed, Prehasher};
|
||||
|
||||
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 ModuleCache = Mutex<HashMap<Prehashed<u64>, wasmtime::Module, DefaultPrehasher>>;
|
||||
|
||||
pub struct WasmtimeBackend {
|
||||
engine: wasmtime::Engine,
|
||||
|
@ -25,8 +24,6 @@ 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);
|
||||
|
@ -43,31 +40,21 @@ 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 hash = hasher.finish();
|
||||
let fmt_hash = format!("{:x}", hash);
|
||||
let hashed = hasher.finish();
|
||||
|
||||
log::debug!("Loading module (hash: {})", fmt_hash);
|
||||
let prehasher = DefaultPrehasher::new();
|
||||
let prehashed = prehasher.prehash(hashed);
|
||||
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);
|
||||
let module = if let Some(module) = cache.get(&prehashed) {
|
||||
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()
|
||||
cache.insert(prehashed, module);
|
||||
cache.get(&prehashed).unwrap()
|
||||
};
|
||||
|
||||
let mut store = wasmtime::Store::new(&self.engine, abi);
|
||||
|
@ -93,12 +80,6 @@ impl Backend for WasmtimeBackend {
|
|||
|
||||
let instance = Arc::new(instance);
|
||||
|
||||
log::debug!(
|
||||
"Loaded module in {:?} (hash: {})",
|
||||
start.elapsed(),
|
||||
fmt_hash
|
||||
);
|
||||
|
||||
Ok(instance)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,6 @@ 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,7 +220,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 +241,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,
|
||||
|
|
Loading…
Reference in New Issue