Compare commits

..

16 Commits

61 changed files with 751 additions and 957 deletions

View File

@ -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

15
apps/android/Cargo.toml Normal file
View File

@ -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"

159
apps/android/src/lib.rs Normal file
View File

@ -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));
}

View File

@ -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);
}

View File

@ -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",
]

View File

@ -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)?;

View File

@ -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 {

View File

@ -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,
&params,
)
.unwrap();
target.finish().unwrap();
}
}

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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);
}
},
_ => {}

View File

@ -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"]

View File

@ -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);
}
}
}
}
});
}

View File

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

View File

@ -1,27 +0,0 @@
// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use serde::{Deserialize, Serialize};
pub use serde;
pub use serde_json;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Contents {
/// The optional name of the application sending the notification.
pub app_name: Option<String>,
/// The summary text briefly describing the notification.
pub summary: String,
/// The optional detailed body text.
pub body: Option<String>,
/// The timeout time in milliseconds since the display of the notification
/// at which the notification should automatically close.
pub timeout: Option<i32>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum OutMsg {}

View File

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

View File

@ -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"] }

View File

@ -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,

1
book/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
book

6
book/book.toml Normal file
View File

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

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

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

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

@ -0,0 +1 @@
# Credits

View File

@ -0,0 +1 @@
# Ecosystem

View File

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

View File

@ -0,0 +1 @@
# Fonts

View File

@ -0,0 +1 @@
# Localization

View File

@ -0,0 +1 @@
# Messages

View File

@ -0,0 +1 @@
# Protocols

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

@ -0,0 +1 @@
# Glossary

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

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

View File

@ -0,0 +1 @@
# Backends

View File

@ -0,0 +1 @@
# Examples

View File

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

View File

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

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

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

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

@ -0,0 +1 @@
# Input

View File

@ -0,0 +1 @@
# Pointer

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

@ -0,0 +1 @@
# Text

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

@ -0,0 +1 @@
# Introduction

View File

@ -0,0 +1 @@
# Rendering

View File

@ -0,0 +1 @@
# Canvases

View File

@ -0,0 +1 @@
# Primitives

View File

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

View File

@ -0,0 +1 @@
# Tessellation

View File

@ -0,0 +1 @@
# Text

View File

@ -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 {

View File

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

View File

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

View File

@ -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"

162
renderers/wgpu/src/lib.rs Normal file
View File

@ -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,
}

View File

@ -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

View File

@ -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"

View File

@ -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),
}

View File

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

View File

@ -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,
},

View File

@ -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>>;
}

View File

@ -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)
}
}

View File

@ -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()),

View File

@ -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,