adam changes

This commit is contained in:
Adam
2025-07-22 14:59:00 -07:00
parent 809a00979a
commit 18029731ff
37 changed files with 2356 additions and 3915 deletions

3344
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,66 +1,9 @@
[workspace]
members = [
"editor",
"desktop",
"proc-macros",
"frontend/wasm",
"node-graph/gapplication-io",
"node-graph/gbrush",
"node-graph/gcore",
"node-graph/gstd",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"node-graph/graph-craft",
"node-graph/graphene-cli",
"node-graph/graster-nodes",
"node-graph/gsvg-renderer",
"node-graph/interpreted-executor",
"node-graph/node-macro",
"node-graph/preprocessor",
"libraries/dyn-any",
"libraries/path-bool",
"libraries/bezier-rs",
"libraries/math-parser",
"website/other/bezier-rs-demos/wasm",
]
default-members = [
"editor",
"frontend/wasm",
"node-graph/gbrush",
"node-graph/gcore",
"node-graph/gstd",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"node-graph/graph-craft",
"node-graph/graphene-cli",
"node-graph/graster-nodes",
"node-graph/gsvg-renderer",
"node-graph/interpreted-executor",
"node-graph/node-macro",
]
resolver = "2"
[workspace.dependencies]
# Local dependencies
bezier-rs = { path = "libraries/bezier-rs", features = ["dyn-any", "serde"] }
dyn-any = { path = "libraries/dyn-any", features = ["derive", "glam", "reqwest", "log-bad-types", "rc"] }
preprocessor = { path = "node-graph/preprocessor"}
math-parser = { path = "libraries/math-parser" }
path-bool = { path = "libraries/path-bool" }
graphene-application-io = { path = "node-graph/gapplication-io" }
graphene-brush = { path = "node-graph/gbrush" }
graphene-core = { path = "node-graph/gcore" }
graphene-math-nodes = { path = "node-graph/gmath-nodes" }
graphene-path-bool = { path = "node-graph/gpath-bool" }
graph-craft = { path = "node-graph/graph-craft" }
graphene-raster-nodes = { path = "node-graph/graster-nodes" }
graphene-std = { path = "node-graph/gstd" }
graphene-svg-renderer = { path = "node-graph/gsvg-renderer" }
interpreted-executor = { path = "node-graph/interpreted-executor" }
node-macro = { path = "node-graph/node-macro" }
wgpu-executor = { path = "node-graph/wgpu-executor" }
graphite-proc-macros = { path = "proc-macros" }
# Workspace dependencies
rustc-hash = "2.0"
bytemuck = { version = "1.13", features = ["derive"] }
@@ -69,6 +12,8 @@ serde_json = "1.0"
serde-wasm-bindgen = "0.6"
reqwest = { version = "0.12", features = ["blocking", "rustls-tls", "json"] }
futures = "0.3"
cef = "138.5.0"
include_dir = "0.7.4"
env_logger = "0.11"
log = "0.4"
bitflags = { version = "2.4", features = ["serde"] }

View File

@@ -8,25 +8,10 @@ repository = ""
edition = "2021"
rust-version = "1.79"
[features]
default = ["gpu"]
gpu = ["graphite-editor/gpu"]
[dependencies]
# Local dependencies
graphite-editor = { path = "../editor", features = [
"gpu",
"ron",
"vello",
"decouple-execution",
] }
wgpu = { workspace = true }
winit = { workspace = true, features = ["serde"] }
base64.workspace = true
thiserror.workspace = true
pollster = "0.3"
cef = "138.5.0"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracing = "0.1.41"
bytemuck = { version = "1.23.1", features = ["derive"] }
include_dir = "0.7.4"
thiserror = { workspace = true }
futures = { workspace = true }
cef = { workspace = true }
include_dir = { workspace = true }

View File

@@ -1,7 +1,8 @@
use std::fs::metadata;
fn main() {
let frontend_dir = format!("{}/../frontend/dist", env!("CARGO_MANIFEST_DIR"));
let frontend_dir = format!("{}/../frontend-native/dist", env!("CARGO_MANIFEST_DIR"));
println!("frontend_dir: {}", frontend_dir);
metadata(&frontend_dir).expect("Failed to find frontend directory. Please build the frontend first.");
metadata(format!("{}/index.html", &frontend_dir)).expect("Failed to find index.html in frontend directory.");

View File

@@ -4,10 +4,14 @@ use cef::{browser_host_create_browser_sync, initialize, BrowserSettings, Diction
use thiserror::Error;
use winit::event::WindowEvent;
use crate::cef::internal::OffscreenRenderHandler;
use crate::render::FrameBufferHandle;
use crate::WindowStateHandle;
use super::input::{handle_window_event, InputState};
use super::EventHandler;
use super::internal::{AppImpl, ClientImpl, NonBrowserAppImpl, RenderHandlerImpl};
use super::internal::{AppImpl, ClientImpl, OffscreenApp, RenderHandlerImpl};
pub(crate) struct Setup {}
pub(crate) struct Initialized {}
@@ -37,7 +41,7 @@ impl Context<Setup> {
let is_browser_process = cmd.has_switch(Some(&switch)) != 1;
if !is_browser_process {
let process_type = CefString::from(&cmd.switch_value(Some(&switch)));
let mut app = NonBrowserAppImpl::new();
let mut app = OffscreenApp::new();
let ret = execute_process(Some(args.as_main_args()), Some(&mut app), std::ptr::null_mut());
if ret >= 0 {
return Err(SetupError::SubprocessFailed(process_type.to_string()));
@@ -53,19 +57,8 @@ impl Context<Setup> {
})
}
pub(crate) fn init(self, event_handler: impl EventHandler) -> Result<Context<Initialized>, InitError> {
let mut settings = Settings::default();
settings.windowless_rendering_enabled = 1;
settings.multi_threaded_message_loop = 0;
let mut cef_app = AppImpl::new(event_handler.clone());
let res = initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut());
if res != 1 {
return Err(InitError::InitializationFailed);
}
let render_handler = RenderHandlerImpl::new(event_handler.clone());
pub(crate) fn init(self, frame_buffer: FrameBufferHandle) -> Result<Context<Initialized>, InitError> {
let render_handler = OffscreenRenderHandler::new(frame_buffer);
let mut client = ClientImpl::new(render_handler);
let url = CefString::from("graphite://frontend/");

View File

@@ -23,7 +23,7 @@ pub(crate) fn handle_window_event(context: &mut Context<Initialized>, event: &Wi
context.input_state.update_mouse_position(position);
let mouse_event: MouseEvent = (&context.input_state).into();
browser.host().unwrap().send_mouse_move_event(Some(&mouse_event), 0);
// browser.host().unwrap().send_mouse_move_event(Some(&mouse_event), 0);
}
}
WindowEvent::MouseInput { state, button, .. } => {

View File

@@ -1,31 +1,35 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
use cef::{App, BrowserProcessHandler, ImplApp, SchemeRegistrar, WrapApp};
use cef::{App, BrowserProcessHandler, Frame, ImplApp, SchemeRegistrar, WrapApp};
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
use crate::cef::EventHandler;
use crate::render::{FrameBuffer, FrameBufferHandle};
use super::browser_process_handler::BrowserProcessHandlerImpl;
use super::browser_process_handler::OffscreenBrowserProcessHandler;
pub(crate) struct AppImpl<H: EventHandler> {
object: *mut RcImpl<_cef_app_t, Self>,
event_handler: H,
struct OffscreenApp {
object: *mut RcImpl<cef_dll_sys::_cef_app_t, Self>,
frame_buffer: Arc<Mutex<FrameBuffer>>,
}
impl<H: EventHandler> AppImpl<H> {
pub(crate) fn new(event_handler: H) -> App {
impl OffscreenApp {
fn new(frame_buffer: Arc<Mutex<FrameBuffer>>) -> App {
App::new(Self {
object: std::ptr::null_mut(),
event_handler,
frame_buffer,
})
}
}
impl<H: EventHandler> ImplApp for AppImpl<H> {
impl ImplApp for OffscreenApp {
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(BrowserProcessHandlerImpl::new(self.event_handler.clone()))
println!("browser_process_handler");
Some(OffscreenBrowserProcessHandler::new(self.frame_buffer.clone()))
}
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
println!("on_register_custom_schemes");
GraphiteSchemeHandlerFactory::register_schemes(registrar);
}

View File

@@ -3,22 +3,17 @@ use cef::sys::{_cef_browser_process_handler_t, cef_base_ref_counted_t, cef_brows
use cef::{BrowserProcessHandler, CefString, ImplBrowserProcessHandler, SchemeHandlerFactory, WrapBrowserProcessHandler};
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
use crate::cef::EventHandler;
pub(crate) struct BrowserProcessHandlerImpl<H: EventHandler> {
pub(crate) struct OffscreenBrowserProcessHandler {
object: *mut RcImpl<cef_browser_process_handler_t, Self>,
event_handler: H,
}
impl<H: EventHandler> BrowserProcessHandlerImpl<H> {
pub(crate) fn new(event_handler: H) -> BrowserProcessHandler {
BrowserProcessHandler::new(Self {
object: std::ptr::null_mut(),
event_handler,
})
impl OffscreenBrowserProcessHandler {
pub(crate) fn new() -> BrowserProcessHandler {
BrowserProcessHandler::new(Self { object: std::ptr::null_mut() })
}
}
impl<H: EventHandler> ImplBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
impl ImplBrowserProcessHandler for OffscreenBrowserProcessHandler {
fn on_context_initialized(&self) {
cef::register_scheme_handler_factory(Some(&CefString::from("graphite")), None, Some(&mut SchemeHandlerFactory::new(GraphiteSchemeHandlerFactory::new())));
}
@@ -28,19 +23,16 @@ impl<H: EventHandler> ImplBrowserProcessHandler for BrowserProcessHandlerImpl<H>
}
}
impl<H: EventHandler> Clone for BrowserProcessHandlerImpl<H> {
impl Clone for OffscreenBrowserProcessHandler {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
event_handler: self.event_handler.clone(),
}
Self { object: self.object }
}
}
impl<H: EventHandler> Rc for BrowserProcessHandlerImpl<H> {
impl Rc for OffscreenBrowserProcessHandler {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
@@ -48,7 +40,7 @@ impl<H: EventHandler> Rc for BrowserProcessHandlerImpl<H> {
}
}
}
impl<H: EventHandler> WrapBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
impl WrapBrowserProcessHandler for OffscreenBrowserProcessHandler {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_browser_process_handler_t, Self>) {
self.object = object;
}

View File

@@ -2,6 +2,7 @@ use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_client_t, cef_base_ref_counted_t};
use cef::{Client, ImplClient, RenderHandler, WrapClient};
pub(crate) struct ClientImpl {
object: *mut RcImpl<_cef_client_t, Self>,
render_handler: RenderHandler,

View File

@@ -1,10 +1,8 @@
mod app;
mod browser_process_handler;
mod client;
mod non_browser_app;
mod offscreen_app;
mod render_handler;
pub(crate) use app::AppImpl;
pub(crate) use client::ClientImpl;
pub(crate) use non_browser_app::NonBrowserAppImpl;
pub(crate) use render_handler::RenderHandlerImpl;
pub(crate) use offscreen_app::OffscreenApp;
pub(crate) use render_handler::OffscreenRenderHandler;

View File

@@ -1,19 +1,25 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
use cef::{App, ImplApp, SchemeRegistrar, WrapApp};
use cef::{App, BrowserProcessHandler, ImplApp, SchemeRegistrar, WrapApp};
use crate::cef::internal::browser_process_handler::OffscreenBrowserProcessHandler;
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
use crate::render::FrameBufferHandle;
pub(crate) struct NonBrowserAppImpl {
pub(crate) struct OffscreenApp {
object: *mut RcImpl<_cef_app_t, Self>,
}
impl NonBrowserAppImpl {
impl OffscreenApp {
pub(crate) fn new() -> App {
App::new(Self { object: std::ptr::null_mut() })
}
}
impl ImplApp for NonBrowserAppImpl {
impl ImplApp for OffscreenApp {
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(OffscreenBrowserProcessHandler::new())
}
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
GraphiteSchemeHandlerFactory::register_schemes(registrar);
}
@@ -23,7 +29,7 @@ impl ImplApp for NonBrowserAppImpl {
}
}
impl Clone for NonBrowserAppImpl {
impl Clone for OffscreenApp {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
@@ -32,7 +38,7 @@ impl Clone for NonBrowserAppImpl {
Self { object: self.object }
}
}
impl Rc for NonBrowserAppImpl {
impl Rc for OffscreenApp {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
@@ -40,7 +46,7 @@ impl Rc for NonBrowserAppImpl {
}
}
}
impl WrapApp for NonBrowserAppImpl {
impl WrapApp for OffscreenApp {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
self.object = object;
}

View File

@@ -1,31 +1,30 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_render_handler_t, cef_base_ref_counted_t};
use cef::{Browser, ImplBrowser, ImplBrowserHost, ImplRenderHandler, PaintElementType, Rect, RenderHandler, WrapRenderHandler};
use cef::{Browser, ImplRenderHandler, PaintElementType, Rect, RenderHandler, WrapRenderHandler};
use crate::cef::EventHandler;
use crate::render::FrameBufferHandle;
pub(crate) struct RenderHandlerImpl<H: EventHandler> {
// CEF render handler for offscreen rendering
pub struct OffscreenRenderHandler {
object: *mut RcImpl<_cef_render_handler_t, Self>,
event_handler: H,
frame_buffer: FrameBufferHandle,
}
impl<H: EventHandler> RenderHandlerImpl<H> {
pub(crate) fn new(event_handler: H) -> RenderHandler {
impl OffscreenRenderHandler {
pub(crate) fn new(frame_buffer: FrameBufferHandle) -> RenderHandler {
RenderHandler::new(Self {
object: std::ptr::null_mut(),
event_handler,
frame_buffer,
})
}
}
impl<H: EventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
impl ImplRenderHandler for OffscreenRenderHandler {
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
let frame_buffer = self.frame_buffer.inner.lock().unwrap();
let width = frame_buffer.width() as i32;
let height = frame_buffer.height() as i32;
if let Some(rect) = rect {
let view = self.event_handler.view();
*rect = Rect {
x: 0,
y: 0,
width: view.width as i32,
height: view.height as i32,
};
*rect = Rect { x: 0, y: 0, width, height };
}
}
@@ -41,12 +40,7 @@ impl<H: EventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
) {
let buffer_size = (width * height * 4) as usize;
let buffer_slice = unsafe { std::slice::from_raw_parts(buffer, buffer_size) };
let draw_successful = self.event_handler.draw(buffer_slice.to_vec(), width as usize, height as usize);
if !draw_successful {
if let Some(browser) = browser {
browser.host().unwrap().was_resized();
}
}
self.frame_buffer.inner.lock().unwrap().add_buffer(buffer_slice, width, height);
}
fn get_raw(&self) -> *mut _cef_render_handler_t {
@@ -54,7 +48,13 @@ impl<H: EventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
}
}
impl<H: EventHandler> Clone for RenderHandlerImpl<H> {
impl WrapRenderHandler for OffscreenRenderHandler {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_handler_t, Self>) {
self.object = object;
}
}
impl Clone for OffscreenRenderHandler {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
@@ -62,11 +62,12 @@ impl<H: EventHandler> Clone for RenderHandlerImpl<H> {
}
Self {
object: self.object,
event_handler: self.event_handler.clone(),
frame_buffer: self.frame_buffer.clone(),
}
}
}
impl<H: EventHandler> Rc for RenderHandlerImpl<H> {
impl Rc for OffscreenRenderHandler {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
@@ -74,8 +75,3 @@ impl<H: EventHandler> Rc for RenderHandlerImpl<H> {
}
}
}
impl<H: EventHandler> WrapRenderHandler for RenderHandlerImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_handler_t, Self>) {
self.object = object;
}
}

View File

@@ -6,21 +6,4 @@ mod context;
mod internal;
pub(crate) trait EventHandler: Clone {
fn view(&self) -> View;
fn draw(&self, buffer: Vec<u8>, width: usize, height: usize) -> bool;
}
#[derive(Clone)]
pub(crate) struct View {
pub(crate) width: usize,
pub(crate) height: usize,
}
impl View {
pub(crate) fn new(width: usize, height: usize) -> Self {
Self { width, height }
}
}
pub(crate) use context::{Context, InitError, Initialized, Setup, SetupError};

View File

@@ -61,7 +61,7 @@ impl ImplSchemeHandlerFactory for GraphiteSchemeHandlerFactory {
}
}
static FRONTEND: Dir = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist");
static FRONTEND: Dir = include_dir!("$CARGO_MANIFEST_DIR/../frontend-native/dist");
struct GraphiteFrontendResourceHandler<'a> {
object: *mut RcImpl<_cef_resource_handler_t, Self>,
@@ -74,7 +74,7 @@ impl<'a> GraphiteFrontendResourceHandler<'a> {
let data = if let Some(file) = file {
Some(RefCell::new(file.contents().iter()))
} else {
println!("Failed to find asset at path: {}", path);
println!("Failed to find asset at path: {}/{}", FRONTEND.path().to_str().unwrap(), path);
None
};
let mimetype = if let Some(file) = file {

View File

@@ -1,203 +1,42 @@
use std::fmt::Debug;
use std::process::exit;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::thread;
use std::time::Duration;
use winit::application::ApplicationHandler;
use winit::event::*;
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
use winit::window::{Window, WindowId};
use winit::window::WindowId;
mod cef;
use cef::Setup;
mod winit_app;
mod render;
use render::{FrameBuffer, GraphicsState};
pub(crate) enum CustomEvent {
UiUpdate,
Resized,
DoBrowserWork,
}
use crate::render::FrameBufferHandle;
use crate::winit_app::WinitApp;
pub(crate) struct WindowState {
width: Option<usize>,
height: Option<usize>,
ui_fb: Option<FrameBuffer>,
preview_fb: Option<FrameBuffer>,
graphics_state: Option<GraphicsState>,
event_loop_proxy: Option<EventLoopProxy<CustomEvent>>,
}
impl WindowState {
fn new() -> Self {
Self {
width: None,
height: None,
ui_fb: None,
preview_fb: None,
graphics_state: None,
event_loop_proxy: None,
}
}
fn handle(self) -> WindowStateHandle {
WindowStateHandle { inner: Arc::new(Mutex::new(self)) }
}
}
impl Debug for WindowState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WindowState")
.field("width", &self.width.is_some())
.field("height", &self.height.is_some())
.field("ui_fb", &self.ui_fb.is_some())
.field("preview_fb", &self.preview_fb.is_some())
.field("graphics_state", &self.graphics_state.is_some())
.finish()
}
}
pub(crate) struct WindowStateHandle {
inner: Arc<Mutex<WindowState>>,
}
impl WindowStateHandle {
fn with<'a, P>(&self, p: P) -> Result<(), PoisonError<MutexGuard<'a, WindowState>>>
where
P: FnOnce(&mut WindowState),
{
match self.inner.lock() {
Ok(mut guard) => Ok(p(&mut guard)),
Err(_) => todo!("not error handling yet"),
}
}
}
impl Clone for WindowStateHandle {
fn clone(&self) -> Self {
Self { inner: self.inner.clone() }
}
}
#[derive(Clone)]
struct CefEventHandler {
window_state: WindowStateHandle,
}
impl CefEventHandler {
fn new(window_state: WindowStateHandle) -> Self {
Self { window_state }
}
}
impl cef::EventHandler for CefEventHandler {
fn view(&self) -> cef::View {
let mut w = 1;
let mut h = 1;
self.window_state
.with(|s| match s {
WindowState {
width: Some(width),
height: Some(height),
..
} => {
w = *width;
h = *height;
}
_ => {}
})
.unwrap();
cef::View::new(w, h)
}
fn draw(&self, buffer: Vec<u8>, width: usize, height: usize) -> bool {
let fb = FrameBuffer::new(buffer, width, height)
.map_err(|e| {
panic!("Failed to create FrameBuffer: {}", e);
})
.unwrap();
let mut correct_size = true;
self.window_state
.with(|s| {
if let Some(event_loop_proxy) = &s.event_loop_proxy {
let _ = event_loop_proxy.send_event(CustomEvent::UiUpdate);
let _ = event_loop_proxy.send_event(CustomEvent::DoBrowserWork);
}
if width != s.width.unwrap_or(1) || height != s.height.unwrap_or(1) {
correct_size = false;
} else {
s.ui_fb = Some(fb);
}
})
.unwrap();
correct_size
}
}
struct WinitApp {
window_state: WindowStateHandle,
cef_context: cef::Context<cef::Initialized>,
window: Option<Arc<Window>>,
}
impl WinitApp {
fn new(window_state: WindowStateHandle, cef_context: cef::Context<cef::Initialized>) -> Self {
Self {
window_state,
cef_context,
window: None,
}
}
}
impl ApplicationHandler<CustomEvent> for WinitApp {
impl ApplicationHandler for WinitApp {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
self.window_state
.with(|s| match s {
WindowState { width: Some(w), height: Some(h), .. } => {
let window = Arc::new(
event_loop
.create_window(
Window::default_attributes()
.with_title("CEF Offscreen Rendering")
.with_inner_size(winit::dpi::LogicalSize::new(*w as u32, *h as u32)),
)
.unwrap(),
);
let graphics_state = pollster::block_on(GraphicsState::new(window.clone()));
println!("resumed");
let graphics_state = futures::executor::block_on(GraphicsState::init(event_loop));
let width = graphics_state.window.inner_size().width;
let height = graphics_state.window.inner_size().height;
self.resize(width, height);
// Initialize with a test pattern so we always have something to render
let initial_data = vec![34u8; (width * height * 4) as usize]; // Gray texture #22222222
self.frame_buffer.inner.lock().unwrap().add_buffer(&initial_data, width, height);
self.window = Some(window.clone());
s.graphics_state = Some(graphics_state);
let _ = thread::spawn(move || loop {
thread::sleep(Duration::from_millis(100));
window.request_redraw();
});
println!("Winit window created and ready");
}
_ => {}
})
.unwrap();
self.graphics_state = Some(graphics_state);
}
fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) {
match event {
CustomEvent::DoBrowserWork => {
self.cef_context.work();
}
CustomEvent::UiUpdate | CustomEvent::Resized => {
if let Some(window) = &self.window {
window.request_redraw();
}
}
}
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
// Try load the frame buffer into the ui texture if it changes
self.try_load_frame_buffer();
// Update the viewport texture if the canvas element changes
self.try_load_viewport();
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
@@ -209,94 +48,39 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
event_loop.exit();
}
WindowEvent::Resized(physical_size) => {
self.window_state
.with(|s| {
s.width = Some(physical_size.width as usize);
s.height = Some(physical_size.height as usize);
if let Some(elp) = &s.event_loop_proxy {
let _ = elp.send_event(CustomEvent::Resized);
}
if let Some(event_loop_proxy) = &s.event_loop_proxy {
let _ = event_loop_proxy.send_event(CustomEvent::DoBrowserWork);
}
})
.unwrap();
self.resize(physical_size.width, physical_size.height);
}
WindowEvent::RedrawRequested => {
self.cef_context.work();
self.window_state
.with(|s| match s {
WindowState {
width: Some(width),
height: Some(height),
graphics_state: Some(graphics_state),
ui_fb,
..
} => {
if let Some(fb) = &*ui_fb {
graphics_state.update_texture(fb);
if fb.width() != *width && fb.height() != *height {
graphics_state.resize(*width, *height);
}
} else {
graphics_state.resize(*width, *height);
}
match graphics_state.render() {
Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => {
graphics_state.resize(*width, *height);
}
Err(wgpu::SurfaceError::OutOfMemory) => {
event_loop.exit();
}
Err(e) => eprintln!("{:?}", e),
}
}
_ => {}
})
.unwrap();
match self.render() {
Ok(_) => {}
Err(wgpu::SurfaceError::OutOfMemory) => {
event_loop.exit();
}
Err(e) => eprintln!("{:?}", e),
}
}
_ => {}
}
self.window_state
.with(|s| {
if let Some(event_loop_proxy) = &s.event_loop_proxy {
let _ = event_loop_proxy.send_event(CustomEvent::DoBrowserWork);
}
})
.unwrap();
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let cef_context = match cef::Context::<Setup>::new() {
Ok(c) => c,
Err(cef::SetupError::Subprocess) => exit(0),
Err(cef::SetupError::SubprocessFailed(t)) => {
println!("Subprocess of type {t} failed");
println!("Subprocess of type {t} failed. args: {:?}", args);
exit(1);
}
};
let window_state = WindowState::new().handle();
let frame_buffer = FrameBufferHandle::new();
window_state
.with(|s| {
s.width = Some(1200);
s.height = Some(800);
})
.unwrap();
let event_loop = EventLoop::<CustomEvent>::with_user_event().build().unwrap();
event_loop.set_control_flow(ControlFlow::Wait);
window_state.with(|s| s.event_loop_proxy = Some(event_loop.create_proxy())).unwrap();
let cef_context = match cef_context.init(CefEventHandler::new(window_state.clone())) {
let cef_context = match cef_context.init(frame_buffer.clone()) {
Ok(c) => c,
Err(cef::InitError::InitializationFailed) => {
println!("Cef initialization failed");
@@ -306,8 +90,11 @@ fn main() {
println!("Cef initialized successfully");
let mut winit_app = WinitApp::new(window_state, cef_context);
let mut winit_app = WinitApp::new(cef_context, frame_buffer);
// Start winit event loop
let event_loop = EventLoop::new().unwrap();
event_loop.set_control_flow(ControlFlow::Poll);
event_loop.run_app(&mut winit_app).unwrap();
winit_app.cef_context.shutdown();

View File

@@ -1,36 +0,0 @@
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
var out: VertexOutput;
let pos = array(
// 1st triangle
vec2f( -1.0, -1.0), // center
vec2f( 1.0, -1.0), // right, center
vec2f( -1.0, 1.0), // center, top
// 2nd triangle
vec2f( -1.0, 1.0), // center, top
vec2f( 1.0, -1.0), // right, center
vec2f( 1.0, 1.0), // right, top
);
let xy = pos[vertex_index];
out.clip_position = vec4f(xy , 0.0, 1.0);
let coords = (xy / 2. + 0.5);
out.tex_coords = vec2f(coords.x, 1. - coords.y);
return out;
}
@group(0) @binding(0)
var t_diffuse: texture_2d<f32>;
@group(0) @binding(1)
var s_diffuse: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(t_diffuse, s_diffuse, in.tex_coords);
}

View File

@@ -1,74 +1,163 @@
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use cef::Frame;
use thiserror::Error;
use winit::window::Window;
use winit::{event_loop::ActiveEventLoop, window::Window};
pub(crate) struct FrameBuffer {
buffer: Vec<u8>,
width: usize,
height: usize,
#[derive(Clone)]
pub struct FrameBufferHandle {
pub inner: Arc<Mutex<FrameBuffer>>,
}
#[derive(Error, Debug)]
pub(crate) enum FrameBufferError {
#[error("Invalid buffer size {buffer_size}, expected {expected_size} for width {width} multiplied with height {height} multiplied by 4 channels")]
InvalidSize { buffer_size: usize, expected_size: usize, width: usize, height: usize },
}
impl FrameBuffer {
pub(crate) fn new(buffer: Vec<u8>, width: usize, height: usize) -> Result<Self, FrameBufferError> {
let fb = Self { buffer, width, height };
fb.validate_size()?;
Ok(fb)
}
pub(crate) fn buffer(&self) -> &[u8] {
&self.buffer
}
pub(crate) fn width(&self) -> usize {
self.width
}
pub(crate) fn height(&self) -> usize {
self.height
}
fn validate_size(&self) -> Result<(), FrameBufferError> {
if self.buffer.len() != self.width * self.height * 4 {
Err(FrameBufferError::InvalidSize {
buffer_size: self.buffer.len(),
expected_size: self.width * self.height * 4,
width: self.width,
height: self.height,
})
} else {
Ok(())
impl FrameBufferHandle {
pub fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(FrameBuffer::new())),
}
}
}
#[derive(Debug)]
pub struct FrameBuffer {
// The buffer is only valid after the CEF on_paint, and before it is loaded into the texture
buffer_has_new_data: bool,
buffer: Vec<u8>,
width: u32,
height: u32,
viewport_resized: bool,
viewport_top_left_x: u32,
viewport_top_left_y: u32,
viewport_width: u32,
viewport_height: u32,
}
#[derive(Error, Debug)]
pub(crate) enum FrameBufferError {
#[error("Invalid buffer size. Expected {expected_width}x{expected_height} recieved {received_width}x{received_height}")]
InvalidSize {
expected_width: usize,
expected_height: usize,
received_width: usize,
received_height: usize,
},
#[error("Buffer dimensions are correct, but the allocated vec length : {vec_length} does not match the buffer length: {buffer_length}")]
InvalidBufferSize { vec_length: usize, buffer_length: usize },
}
impl FrameBuffer {
//Initialize the frame buffer to 4k, but set width and height to 0 as it should be initialized when
pub fn new() -> Self {
Self {
buffer: Vec::with_capacity(3840 * 2160 * 4),
buffer_has_new_data: false,
width: 0,
height: 0,
viewport_resized: false,
viewport_top_left_x: 0,
viewport_top_left_y: 0,
viewport_height: 0,
viewport_width: 0,
}
}
// Always keep the frame buffer in sync with the window size
pub fn resize(&mut self, width: u32, height: u32) -> Result<Self, FrameBufferError> {
let new_size = width * height * 4;
if self.buffer.len() < new_size {
self.buffer.resize(new_size, 0);
} else {
self.buffer.truncate(new_size);
}
}
pub fn add_buffer(&mut self, buffer_slice: &[u8], width: u32, height: u32) -> Result<(), FrameBufferError> {
if width != self.width || height != self.height {
Err(FrameBufferError::InvalidSize {
expected_width: self.width,
expected_height: self.height,
received_width: width,
received_height: height,
})
} else if buffer_slice.len() != self.buffer.len() {
Err(FrameBufferError::InvalidBufferSize {
vec_length: self.buffer.len(),
buffer_length: buffer_slice.len(),
})
} else {
self.buffer.copy_from_slice(buffer_slice);
self.buffer_has_new_data = true;
Ok(())
}
}
pub(crate) fn take_buffer(&mut self) -> Option<(&[u8], u32, u32)> {
if buffer_has_new_data {
Some((&self.buffer, self.width, self.height));
self.buffer_has_new_data = false;
} else {
None
}
}
pub fn add_viewport_size(&mut self, viewport_top_left_x: u32, viewport_top_left_y: u32, viewport_width: u32, viewport_height: u32) {
if self.viewport_top_left_x != viewport_top_left_x || self.viewport_top_left_y != viewport_top_left_y || self.viewport_width != viewport_width || self.viewport_height != viewport_height {
self.viewport_resized = true;
}
self.viewport_top_left_x = viewport_top_left_x;
self.viewport_top_left_y = viewport_top_left_y;
self.viewport_width = viewport_width;
self.viewport_height = viewport_height;
}
pub fn get_viewport_size(&mut self) -> Option<(u32, u32, u32, u32)> {
if self.viewport_resized {
self.viewport_resized = false;
Some((self.viewport_top_left_x, self.viewport_top_left_y, self.viewport_width, self.viewport_height))
} else {
None
}
}
pub(crate) fn width(&self) -> u32 {
self.width
}
pub(crate) fn height(&self) -> u32 {
self.height
}
}
pub(crate) struct GraphicsState {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
texture: Option<wgpu::Texture>,
bind_group: Option<wgpu::BindGroup>,
render_pipeline: wgpu::RenderPipeline,
sampler: wgpu::Sampler,
pub window: Arc<Window>,
pub surface: wgpu::Surface<'static>,
pub device: wgpu::Device,
pub queue: wgpu::Queue,
pub config: wgpu::SurfaceConfiguration,
pub render_pipeline: wgpu::RenderPipeline,
pub sampler: wgpu::Sampler,
}
impl GraphicsState {
pub(crate) async fn new(window: Arc<Window>) -> Self {
pub(crate) async fn init(event_loop: &ActiveEventLoop) -> Self {
let window = Arc::new(
event_loop
.create_window(
Window::default_attributes()
.with_title("CEF Offscreen Rendering Test")
.with_inner_size(winit::dpi::LogicalSize::new(800, 600)),
)
.unwrap(),
);
let size = window.inner_size();
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
let surface = instance.create_surface(window).unwrap();
let surface = instance.create_surface(window.clone()).unwrap();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
@@ -106,10 +195,61 @@ impl GraphicsState {
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &config);
// Create shader module
let shader = device.create_shader_module(wgpu::include_wgsl!("fullscreen_texture.wgsl"));
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Shader"),
source: wgpu::ShaderSource::Wgsl(
r#"
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
var out: VertexOutput;
let pos = array(
// 1st triangle
vec2f( -1.0, -1.0), // center
vec2f( 1.0, -1.0), // right, center
vec2f( -1.0, 1.0), // center, top
// 2nd triangle
vec2f( -1.0, 1.0), // center, top
vec2f( 1.0, -1.0), // right, center
vec2f( 1.0, 1.0), // right, top
);
let xy = pos[vertex_index];
out.clip_position = vec4f(xy , 0.0, 1.0);
let coords = (xy/ 2. + 0.5);
out.tex_coords = vec2f(coords.x, 1. - coords.y);
// // Generate a fullscreen triangle
// let x = f32(i32(vertex_index) - 1);
// let y = f32(i32(vertex_index & 1u) * 2 - 1);
// out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
// out.tex_coords = vec2<f32>((x + 1.0) * 0.5, (1.0 - y) * 0.5);
return out;
}
@group(0) @binding(0)
var t_diffuse: texture_2d<f32>;
@group(0) @binding(1)
var s_diffuse: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
// Test: use texture coordinates as colors to debug
// return vec4<f32>(in.tex_coords.x, in.tex_coords.y, 0.0, 1.0);
// Uncomment this line to use CEF texture:
return textureSample(t_diffuse, s_diffuse, in.tex_coords);
}
"#
.into(),
),
});
// Create sampler
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
@@ -188,141 +328,23 @@ impl GraphicsState {
cache: None,
});
let mut graphics_state = Self {
Self {
window,
surface,
device,
queue,
config,
texture: None,
bind_group: None,
render_pipeline,
sampler,
};
// Initialize with a test pattern so we always have something to render
let width = 800;
let height = 600;
let initial_data = vec![34u8; width * height * 4]; // Gray texture #222222FF
let fb = FrameBuffer::new(initial_data, width, height)
.map_err(|e| {
panic!("Failed to create initial FrameBuffer: {}", e);
})
.unwrap();
graphics_state.update_texture(&fb);
graphics_state
}
pub(crate) fn resize(&mut self, width: usize, height: usize) {
if width > 0 && height > 0 && (self.config.width != width as u32 || self.config.height != height as u32) {
self.config.width = width as u32;
self.config.height = height as u32;
self.surface.configure(&self.device, &self.config);
}
}
pub(crate) fn update_texture(&mut self, frame_buffer: &FrameBuffer) {
let data = frame_buffer.buffer();
let width = frame_buffer.width() as u32;
let height = frame_buffer.height() as u32;
// Creates the cached ui texture, reconfigures the surface
pub(crate) fn resize_surface(&mut self, width: u32, height: u32) {
if width > 0 && height > 0 && (self.config.width != width || self.config.height != height) {
self.config.width = width;
self.config.height = height;
self.surface.configure(&self.device, &self.config);
}
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("CEF Texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Bgra8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
self.queue.write_texture(
wgpu::ImageCopyTexture {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
data,
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &self.render_pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
label: Some("texture_bind_group"),
});
self.texture = Some(texture);
self.bind_group = Some(bind_group);
}
pub(crate) fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
let output = self.surface.get_current_texture()?;
let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder") });
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.01, g: 0.01, b: 0.01, a: 1.0 }),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
render_pass.set_pipeline(&self.render_pipeline);
if let Some(bind_group) = &self.bind_group {
render_pass.set_bind_group(0, bind_group, &[]);
render_pass.draw(0..6, 0..1); // Draw 3 vertices for fullscreen triangle
} else {
println!("No bind group available - showing clear color only");
}
}
self.queue.submit(std::iter::once(encoder.finish()));
output.present();
Ok(())
}
}

218
desktop/src/winit_app.rs Normal file
View File

@@ -0,0 +1,218 @@
use std::process::exit;
use winit::application::ApplicationHandler;
use winit::event::*;
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
use winit::window::WindowId;
use crate::cef::{Context, Initialized};
use crate::render::{FrameBuffer, FrameBufferHandle, GraphicsState};
pub struct WinitApp {
pub cef_context: Context<Initialized>,
// Persistent initalized state when the window is created
pub graphics_state: Option<GraphicsState>,
// Shared between winit and cef, and stores the state for the window size and ui frame buffer.
// Automatically kept in sync with the width/height in graphics state surface config
pub frame_buffer: FrameBufferHandle,
// Cached node graph output texture. And its position relative to the full ui
pub viewport_top_left: u32,
pub viewport_top_right: u32,
pub viewport_texture: Option<wgpu::Texture>,
pub viewport_bind_group: Option<wgpu::BindGroup>,
// Cached UI texture and bindgroup for the CEF overlay
pub ui_texture: Option<wgpu::Texture>,
pub ui_bind_group: Option<wgpu::BindGroup>,
}
impl WinitApp {
pub fn new(cef_context: Context<Initialized>, frame_buffer: FrameBufferHandle) -> Self {
Self {
cef_context,
graphics_state: None,
frame_buffer,
viewport_top_left: 0,
viewport_top_right: 0,
viewport_texture: None,
viewport_bind_group: None,
ui_texture: None,
ui_bind_group: None,
}
}
// The single entrypoint for window resizing. It updates the frame buffer, surface config, cached UI overlay texture, and clears the viewport texture
pub fn resize(&mut self, width: u32, height: u32) {
if let Some(graphics_state) = &mut self.graphics_state {
// Updates the surface config
graphics_state.resize_surface(width, height);
// Creates the cached ui texture, reconfigures the surface, and updates the frame buffer
self.ui_texture = Some(graphics_state.device.create_texture(&wgpu::TextureDescriptor {
label: Some("CEF Texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Bgra8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
}));
self.ui_bind_group = Some(graphics_state.device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &graphics_state.render_pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(self.ui_texture.as_ref().unwrap()),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&graphics_state.sampler),
},
],
label: Some("texture_bind_group"),
}));
// Invalidate the viewport texture, since we need to wait until cef calls on paint so we can get the new viewport size, which always changes when the window changes
self.viewport_bind_group = None;
self.viewport_texture = None;
}
// Keep the frame buffer in sync
self.frame_buffer.lock().unwrap().resize(width, height);
if let Some(browser) = &self.cef_context.browser {
browser.host().unwrap().was_resized();
}
}
// Composites the cached ui overlay texture onto the cached node graph texture. This should be called when the DOM or node graph gets evaluated.
pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
let Some(graphics_state) = &mut self.graphics_state else {
println!("Graphics state not initialized in render function");
return Ok(());
};
let output = graphics_state.surface.get_current_texture()?;
let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = graphics_state.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder") });
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.1, g: 0.2, b: 0.3, a: 1.0 }),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
render_pass.set_pipeline(&graphics_state.render_pipeline);
if let Some(node_graph_bind_group) = &self.viewport_bind_group {
render_pass.set_bind_group(0, node_graph_bind_group, &[]);
render_pass.draw(0..6, 0..1);
}
if let Some(ui_bind_group) = &self.ui_bind_group {
render_pass.set_bind_group(0, ui_bind_group, &[]);
render_pass.draw(0..6, 0..1);
} else {
println!("No bind group available for ui overlay");
}
}
graphics_state.queue.submit(std::iter::once(encoder.finish()));
output.present();
Ok(())
}
// Loads the framebuffer data from CEF into the texture if it changed
pub fn try_load_frame_buffer(&mut self) -> Result<(), String> {
// Load the data from the shared frame buffer to the texture
if let Some((new_buffer, width, height)) = self.frame_buffer.inner.lock().unwrap().take_buffer() {
let Some(cached_ui_texture) = &self.ui_texture else {
return Err("UI texture must be initialzed before loading framebuffer data".to_string());
};
let Some(graphics_state) = &mut self.graphics_state else {
return Err("graphics state must exist before loading framebuffer data".to_string());
};
graphics_state.queue.write_texture(
wgpu::ImageCopyTexture {
texture: &cached_ui_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
new_buffer,
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
}
Ok(())
}
// Load the viewport texture and keep its position in sync with the browser viewport
pub fn try_load_viewport(&mut self) -> Result<(), String> {
let Some(graphics_state) = &mut self.graphics_state else {
println!("Graphics state not initialized in try_load_viewport");
return Ok(());
};
// Only runs if the viewport changes, in which case the cached texture should be recreated
if let Some((top_left, top_right, width, height)) = self.frame_buffer.inner.lock().unwrap().get_viewport_size() {
self.viewport_top_left = top_left;
self.viewport_top_right = top_right;
self.ui_texture = Some(graphics_state.device.create_texture(&wgpu::TextureDescriptor {
label: Some("Vello Texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
// Based on Vello requirements https://github.com/linebender/vello/blob/daf940230a24cbb123a458b6de95721af47aef98/vello/src/lib.rs#L460C36-L460C46
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::STORAGE_BINDING,
view_formats: &[],
}));
self.ui_bind_group = Some(graphics_state.device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &graphics_state.render_pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(self.ui_texture.as_ref().unwrap()),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&graphics_state.sampler),
},
],
label: Some("texture_bind_group"),
}));
}
}
}

24
frontend-native/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

47
frontend-native/README.md Normal file
View File

@@ -0,0 +1,47 @@
# Svelte + TS + Vite
This template should help get you started developing with Svelte and TypeScript in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## Need an official Svelte framework?
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
**Why enable `allowJs` in the TS template?**
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
```ts
// store.ts
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Svelte + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1417
frontend-native/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
{
"name": "native-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tsconfig/svelte": "^5.0.4",
"svelte": "^5.35.5",
"svelte-check": "^4.2.2",
"typescript": "~5.8.3",
"vite": "^7.0.4"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,47 @@
<script lang="ts">
import svelteLogo from './assets/svelte.svg'
import viteLogo from '/vite.svg'
import Counter from './lib/Counter.svelte'
</script>
<main>
<div>
<a href="https://vite.dev" target="_blank" rel="noreferrer">
<img src={viteLogo} class="logo" alt="Vite Logo" />
</a>
<a href="https://svelte.dev" target="_blank" rel="noreferrer">
<img src={svelteLogo} class="logo svelte" alt="Svelte Logo" />
</a>
</div>
<h1>Vite + Svelte</h1>
<div class="card">
<Counter />
</div>
<p>
Check out <a href="https://github.com/sveltejs/kit#readme" target="_blank" rel="noreferrer">SvelteKit</a>, the official Svelte app framework powered by Vite!
</p>
<p class="read-the-docs">
Click on the Vite and Svelte logos to learn more
</p>
</main>
<style>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.svelte:hover {
filter: drop-shadow(0 0 2em #ff3e00aa);
}
.read-the-docs {
color: #888;
}
</style>

View File

@@ -0,0 +1,79 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,10 @@
<script lang="ts">
let count: number = $state(0)
const increment = () => {
count += 1
}
</script>
<button onclick={increment}>
count is {count}
</button>

View File

@@ -0,0 +1,9 @@
import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'
const app = mount(App, {
target: document.getElementById('app')!,
})
export default app

2
frontend-native/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@@ -0,0 +1,7 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
}

View File

@@ -0,0 +1,20 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"resolveJsonModule": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": true,
"moduleDetection": "force"
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
}

View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vite.dev/config/
export default defineConfig({
plugins: [svelte()],
})

View File

@@ -4,6 +4,7 @@
"scripts": {
"---------- DEV SERVER ----------": "",
"start": "cd frontend && npm start",
"start-desktop": "cd frontend-native && npm run build && cargo run -p graphite-desktop",
"profiling": "cd frontend && npm run profiling",
"production": "cd frontend && npm run production",
"---------- BUILDS ----------": "",