Execute editor natively

This commit is contained in:
Adam 2025-07-24 04:23:33 -07:00
parent 4fec24893e
commit 0c60ebaabc
21 changed files with 296 additions and 29 deletions

3
Cargo.lock generated
View file

@ -1837,7 +1837,9 @@ dependencies = [
"cef",
"dirs",
"futures",
"graphite-editor",
"include_dir",
"serde_json",
"thiserror 2.0.12",
"tracing",
"tracing-subscriber",
@ -1904,6 +1906,7 @@ dependencies = [
"math-parser",
"serde",
"serde-wasm-bindgen",
"serde_json",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",

View file

@ -9,17 +9,18 @@ edition = "2024"
rust-version = "1.87"
[features]
# default = ["gpu"]
# gpu = ["graphite-editor/gpu"]
default = ["gpu"]
gpu = ["graphite-editor/gpu"]
[dependencies]
# Local dependencies
# graphite-editor = { path = "../editor", features = [
# "gpu",
# "ron",
# "vello",
# "decouple-execution",
# ] }
# # Local dependencies
graphite-editor = { path = "../editor", features = [
"gpu",
"ron",
"vello",
"decouple-execution",
] }
wgpu = { workspace = true }
winit = { workspace = true, features = ["serde"] }
thiserror = { workspace = true }
@ -29,3 +30,4 @@ include_dir = { workspace = true }
tracing-subscriber = { workspace = true }
tracing = { workspace = true }
dirs = {workspace = true}
serde_json = { workspace = true }

View file

@ -2,6 +2,10 @@ use crate::CustomEvent;
use crate::WindowSize;
use crate::render::GraphicsState;
use crate::render::WgpuContext;
use graphite_editor::application::Editor;
use graphite_editor::dispatcher::Dispatcher;
use graphite_editor::messages::prelude::Message;
use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::mpsc::Sender;
use std::time::Duration;
@ -21,11 +25,13 @@ pub(crate) struct WinitApp {
pub(crate) cef_context: cef::Context<cef::Initialized>,
pub(crate) window: Option<Arc<Window>>,
cef_schedule: Option<Instant>,
// Cached frame buffer from CEF, used to check if mouse is on a transparent pixel
_ui_frame_buffer: Option<wgpu::Texture>,
window_size_sender: Sender<WindowSize>,
_viewport_frame_buffer: Option<wgpu::Texture>,
graphics_state: Option<GraphicsState>,
wgpu_context: WgpuContext,
pub(crate) editor: Editor,
}
impl WinitApp {
@ -39,6 +45,7 @@ impl WinitApp {
graphics_state: None,
window_size_sender,
wgpu_context,
editor: Editor::new(),
}
}
}
@ -97,6 +104,16 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
self.cef_schedule = Some(instant);
}
}
CustomEvent::MessageReceived { message } => {
let Ok(message) = serde_json::from_str::<Message>(&message) else {
tracing::error!("Message could not be deserialized: {:?}", message);
return;
};
println!("Message received: {message:?}");
let responses = self.editor.handle_message(message);
println!("responses: {:?}", responses);
// Send response to CEF
}
}
}

View file

@ -19,6 +19,8 @@ pub(crate) trait CefEventHandler: Clone {
/// Scheudule the main event loop to run the cef event loop after the timeout
/// [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation.
fn schedule_cef_message_loop_work(&self, scheduled_time: Instant);
fn send_message_to_editior(&self, message: String);
}
#[derive(Clone, Copy)]
@ -116,4 +118,7 @@ impl CefEventHandler for CefHandler {
fn schedule_cef_message_loop_work(&self, scheduled_time: std::time::Instant) {
let _ = self.event_loop_proxy.send_event(CustomEvent::ScheduleBrowserWork(scheduled_time));
}
fn send_message_to_editior(&self, message: String) {
let _ = self.event_loop_proxy.send_event(CustomEvent::MessageReceived { message });
}
}

View file

@ -77,8 +77,8 @@ impl Context<Setup> {
return Err(InitError::InitializationFailed);
}
let render_handler = RenderHandlerImpl::new(event_handler.clone());
let mut client = Client::new(ClientImpl::new(RenderHandler::new(render_handler)));
let render_handler = RenderHandler::new(RenderHandlerImpl::new(event_handler.clone()));
let mut client = Client::new(ClientImpl::new(render_handler, event_handler.clone()));
let url = CefString::from(format!("{GRAPHITE_SCHEME}://{FRONTEND_DOMAIN}/").as_str());

View file

@ -2,6 +2,8 @@ mod app;
mod browser_process_handler;
mod client;
mod non_browser_app;
mod non_browser_render_process_handler;
mod non_browser_v8_handler;
mod render_handler;
pub(crate) use app::AppImpl;

View file

@ -3,6 +3,7 @@ use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
use cef::{BrowserProcessHandler, CefString, ImplApp, ImplCommandLine, SchemeRegistrar, WrapApp};
use crate::cef::CefEventHandler;
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
use super::browser_process_handler::BrowserProcessHandlerImpl;

View file

@ -1,21 +1,25 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_client_t, cef_base_ref_counted_t};
use cef::{ImplClient, RenderHandler, WrapClient};
use cef::{ImplClient, ImplProcessMessage, RenderHandler, WrapClient};
pub(crate) struct ClientImpl {
use crate::cef::CefEventHandler;
pub(crate) struct ClientImpl<H: CefEventHandler> {
object: *mut RcImpl<_cef_client_t, Self>,
render_handler: RenderHandler,
event_handler: H,
}
impl ClientImpl {
pub(crate) fn new(render_handler: RenderHandler) -> Self {
impl<H: CefEventHandler> ClientImpl<H> {
pub(crate) fn new(render_handler: RenderHandler, event_handler: H) -> Self {
Self {
object: std::ptr::null_mut(),
render_handler,
event_handler,
}
}
}
impl ImplClient for ClientImpl {
impl<H: CefEventHandler> ImplClient for ClientImpl<H> {
fn render_handler(&self) -> Option<RenderHandler> {
Some(self.render_handler.clone())
}
@ -23,9 +27,33 @@ impl ImplClient for ClientImpl {
fn get_raw(&self) -> *mut _cef_client_t {
self.object.cast()
}
fn on_process_message_received(
&self,
browser: Option<&mut cef::Browser>,
frame: Option<&mut cef::Frame>,
source_process: cef::ProcessId,
message: Option<&mut cef::ProcessMessage>,
) -> ::std::os::raw::c_int {
let Some(message) = message else {
tracing::event!(tracing::Level::ERROR, "No message in RenderProcessHandlerImpl::on_process_message_received");
return 1;
};
let pointer: *mut cef::sys::_cef_string_utf16_t = message.name().into();
let message = unsafe {
let str = (*pointer).str_;
let len = (*pointer).length;
let slice = std::slice::from_raw_parts(str, len as usize);
String::from_utf16(slice).unwrap()
};
let _ = self.event_handler.send_message_to_editior(message);
0
}
}
impl Clone for ClientImpl {
impl<H: CefEventHandler> Clone for ClientImpl<H> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
@ -34,10 +62,11 @@ impl Clone for ClientImpl {
Self {
object: self.object,
render_handler: self.render_handler.clone(),
event_handler: self.event_handler.clone(),
}
}
}
impl Rc for ClientImpl {
impl<H: CefEventHandler> Rc for ClientImpl<H> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
@ -45,7 +74,7 @@ impl Rc for ClientImpl {
}
}
}
impl WrapClient for ClientImpl {
impl<H: CefEventHandler> WrapClient for ClientImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_client_t, Self>) {
self.object = object;
}

View file

@ -2,6 +2,7 @@ use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
use cef::{App, ImplApp, SchemeRegistrar, WrapApp};
use crate::cef::internal::non_browser_render_process_handler::NonBrowserRenderProcessHandlerImpl;
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
pub(crate) struct NonBrowserAppImpl {
@ -14,6 +15,10 @@ impl NonBrowserAppImpl {
}
impl ImplApp for NonBrowserAppImpl {
fn render_process_handler(&self) -> Option<cef::RenderProcessHandler> {
Some(cef::RenderProcessHandler::new(NonBrowserRenderProcessHandlerImpl::new()))
}
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
GraphiteSchemeHandlerFactory::register_schemes(registrar);
}

View file

@ -0,0 +1,64 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_browser_process_handler_t, _cef_render_process_handler_t, cef_base_ref_counted_t, cef_browser_process_handler_t, cef_v8_handler_t, cef_v8_propertyattribute_t};
use cef::{
CefString, ImplBrowserProcessHandler, ImplRenderProcessHandler, ImplV8Context, ImplV8Value, SchemeHandlerFactory, V8Handler, V8Propertyattribute, V8Value, WrapBrowserProcessHandler,
WrapRenderProcessHandler, v8_value_create_function,
};
use crate::cef::internal::non_browser_v8_handler::NonBrowserV8HandlerImpl;
pub(crate) struct NonBrowserRenderProcessHandlerImpl {
object: *mut RcImpl<_cef_render_process_handler_t, Self>,
}
impl NonBrowserRenderProcessHandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
}
impl ImplRenderProcessHandler for NonBrowserRenderProcessHandlerImpl {
fn on_context_created(&self, browser: Option<&mut cef::Browser>, frame: Option<&mut cef::Frame>, context: Option<&mut cef::V8Context>) {
let Some(context) = context else {
tracing::event!(tracing::Level::ERROR, "No browser in RenderProcessHandlerImpl::on_context_created");
return;
};
let mut v8_handler = V8Handler::new(NonBrowserV8HandlerImpl::new());
let Some(mut function) = v8_value_create_function(Some(&CefString::from("sendMessageToCef")), Some(&mut v8_handler)) else {
tracing::event!(tracing::Level::ERROR, "Failed to create V8 function");
return;
};
let Some(global) = context.global() else {
tracing::event!(tracing::Level::ERROR, "No global object in RenderProcessHandlerImpl::on_context_created");
return;
};
global.set_value_bykey(Some(&CefString::from("sendMessageToCef")), Some(&mut function), V8Propertyattribute::default());
}
fn get_raw(&self) -> *mut _cef_render_process_handler_t {
self.object.cast()
}
}
impl Clone for NonBrowserRenderProcessHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl Rc for NonBrowserRenderProcessHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapRenderProcessHandler for NonBrowserRenderProcessHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_process_handler_t, Self>) {
self.object = object;
}
}

View file

@ -0,0 +1,85 @@
use cef::{
CefString, Frame, ImplFrame, ImplV8Context, ImplV8Handler, ImplV8Value, ProcessId, ProcessMessage, V8Value, WrapV8Handler, process_message_create,
rc::Rc,
string_userfree_utf16_free,
sys::{cef_process_id_t, cef_string_userfree_utf16_free},
v8_context_get_current_context,
};
use tracing::event;
use winit::event_loop::EventLoopProxy;
pub struct NonBrowserV8HandlerImpl {
object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>,
}
impl NonBrowserV8HandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
}
impl ImplV8Handler for NonBrowserV8HandlerImpl {
fn execute(
&self,
name: Option<&cef::CefString>,
_object: Option<&mut V8Value>,
arguments: Option<&[Option<V8Value>]>,
_retval: Option<&mut Option<V8Value>>,
_exception: Option<&mut cef::CefString>,
) -> ::std::os::raw::c_int {
if let Some(name) = name {
if name.to_string() == "sendMessageToCef".to_string() {
let string = arguments.unwrap().get(0).unwrap().as_ref().unwrap().string_value();
let pointer: *mut cef::sys::_cef_string_utf16_t = string.into();
let message = unsafe {
let str = (*pointer).str_;
let len = (*pointer).length;
let slice = std::slice::from_raw_parts(str, len as usize);
String::from_utf16(slice).unwrap()
};
let Some(mut process_message) = process_message_create(Some(&CefString::from(message.as_str()))) else {
tracing::event!(tracing::Level::ERROR, "Failed to create process message");
return 0;
};
let Some(frame) = v8_context_get_current_context().and_then(|context| context.frame()) else {
tracing::event!(tracing::Level::ERROR, "No current V8 context in V8HandlerImpl::execute");
return 0;
};
frame.send_process_message(cef_process_id_t::PID_BROWSER.into(), Some(&mut process_message));
}
}
0
}
fn get_raw(&self) -> *mut cef::sys::_cef_v8_handler_t {
self.object.cast()
}
}
impl Clone for NonBrowserV8HandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl Rc for NonBrowserV8HandlerImpl {
fn as_base(&self) -> &cef::sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapV8Handler for NonBrowserV8HandlerImpl {
fn wrap_rc(&mut self, object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>) {
self.object = object;
}
}

View file

@ -20,6 +20,9 @@ mod dirs;
pub(crate) enum CustomEvent {
UiUpdate(wgpu::Texture),
ScheduleBrowserWork(Instant),
MessageReceived { message: String },
// // Called from the editor if the render node is evaluated and returns an UpdateViewport message
// ViewportUpdate { texture: wgpu::TextureView },
}
fn main() {

View file

@ -99,10 +99,16 @@ pub(crate) struct GraphicsState {
surface: wgpu::Surface<'static>,
context: WgpuContext,
config: wgpu::SurfaceConfiguration,
texture: Option<wgpu::Texture>,
bind_group: Option<wgpu::BindGroup>,
render_pipeline: wgpu::RenderPipeline,
sampler: wgpu::Sampler,
// Cached texture for UI rendering
ui_texture: Option<wgpu::Texture>,
ui_bind_group: Option<wgpu::BindGroup>,
// Cached texture for node graph output
// viewport_texture: Option<wgpu::Texture>,
// // Returned from CEF js event callback
pub viewport_top_left: (u32, u32),
}
impl GraphicsState {
@ -211,10 +217,11 @@ impl GraphicsState {
surface,
context,
config,
texture: None,
bind_group: None,
render_pipeline,
sampler,
ui_texture: None,
ui_bind_group: None,
viewport_top_left: (0, 0),
}
}
@ -228,9 +235,9 @@ impl GraphicsState {
pub(crate) fn bind_texture(&mut self, texture: &wgpu::Texture) {
let bind_group = self.create_bindgroup(texture);
self.texture = Some(texture.clone());
self.ui_texture = Some(texture.clone());
self.bind_group = Some(bind_group);
self.ui_bind_group = Some(bind_group);
}
fn create_bindgroup(&self, texture: &wgpu::Texture) -> wgpu::BindGroup {
@ -275,7 +282,7 @@ impl GraphicsState {
});
render_pass.set_pipeline(&self.render_pipeline);
if let Some(bind_group) = &self.bind_group {
if let Some(bind_group) = &self.ui_bind_group {
render_pass.set_bind_group(0, bind_group, &[]);
render_pass.draw(0..6, 0..1); // Draw 3 vertices for fullscreen triangle
} else {

View file

@ -14,7 +14,6 @@ license = "Apache-2.0"
default = ["wasm"]
wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"]
gpu = ["interpreted-executor/gpu", "wgpu-executor"]
tauri = ["ron", "decouple-execution"]
decouple-execution = []
resvg = ["graphene-std/resvg"]
vello = ["graphene-std/vello", "resvg"]

View file

@ -12,6 +12,7 @@
"production": "npm run setup && npm run wasm:build-production && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-production\"",
"---------- BUILDS ----------": "",
"build-dev": "npm run wasm:build-dev && vite build",
"build-native": "npm run native:build-dev && vite build",
"build-profiling": "npm run wasm:build-profiling && vite build",
"build": "npm run wasm:build-production && vite build",
"---------- UTILITIES ----------": "",
@ -19,6 +20,7 @@
"lint-fix": "eslint . --fix && tsc --noEmit",
"---------- INTERNAL ----------": "",
"setup": "node package-installer.js",
"native:build-dev": "wasm-pack build ./wasm --dev --target=web --features native",
"wasm:build-dev": "wasm-pack build ./wasm --dev --target=web",
"wasm:build-profiling": "wasm-pack build ./wasm --profiling --target=web",
"wasm:build-production": "wasm-pack build ./wasm --release --target=web",

View file

@ -5,6 +5,8 @@
import Editor from "@graphite/components/Editor.svelte";
import { send_message_to_cef } from "/wasm/pkg/graphite_wasm";
let editor: GraphiteEditor | undefined = undefined;
onMount(async () => {
@ -17,6 +19,9 @@
// Destroy the WASM editor handle
editor?.handle.free();
});
console.log("Test from app.svelte javascript");
sendMessageToCef("Test from app direct");
</script>
{#if editor !== undefined}

View file

@ -48,6 +48,7 @@
onMount(() => {
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready
console.log("init after frontend ready from js");
editor.handle.initAfterFrontendReady(operatingSystem());
});

View file

@ -171,7 +171,7 @@
function canvasPointerDown(e: PointerEvent) {
const onEditbox = e.target instanceof HTMLDivElement && e.target.contentEditable;
console.log("Canvas pointer down", e, onEditbox);
if (!onEditbox) viewport?.setPointerCapture(e.pointerId);
if (window.document.activeElement instanceof HTMLElement) {
window.document.activeElement.blur();

View file

@ -13,7 +13,7 @@ license = "Apache-2.0"
[features]
default = ["gpu"]
gpu = ["editor/gpu"]
tauri = ["editor/tauri"]
native = []
[lib]
crate-type = ["cdylib", "rlib"]
@ -31,6 +31,7 @@ graphene-std = { workspace = true }
graph-craft = { workspace = true }
log = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
wasm-bindgen = { workspace = true }
serde-wasm-bindgen = { workspace = true }
js-sys = { workspace = true }

View file

@ -154,6 +154,7 @@ impl EditorHandle {
}
// Sends a message to the dispatcher in the Editor Backend
#[cfg(not(feature = "native"))]
fn dispatch<T: Into<Message>>(&self, message: T) {
// Process no further messages after a crash to avoid spamming the console
if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
@ -169,6 +170,16 @@ impl EditorHandle {
}
}
#[cfg(feature = "native")]
fn dispatch<T: Into<Message>>(&self, message: T) {
let message: Message = message.into();
let Ok(serialized_message) = serde_json::to_string(&message) else {
log::error!("Failed to serialize message");
return;
};
crate::send_message_to_cef(serialized_message)
}
// Sends a FrontendMessage to JavaScript
fn send_frontend_message_to_js(&self, mut message: FrontendMessage) {
if let FrontendMessage::UpdateImageData { ref image_data } = message {
@ -202,11 +213,17 @@ impl EditorHandle {
}
}
#[cfg(feature = "native")]
#[wasm_bindgen(js_name = initAfterFrontendReady)]
pub fn init_after_frontend_ready(&self, platform: String) {
log::debug!("Init after frontend ready from rust");
}
// ========================================================================
// Add additional JS -> Rust wrapper functions below as needed for calling
// the backend from the web frontend.
// ========================================================================
#[cfg(not(feature = "native"))]
#[wasm_bindgen(js_name = initAfterFrontendReady)]
pub fn init_after_frontend_ready(&self, platform: String) {
// Send initialization messages

View file

@ -27,6 +27,7 @@ thread_local! {
#[wasm_bindgen(start)]
pub fn init_graphite() {
// Set up the panic hook
#[cfg(not(feature = "native"))]
panic::set_hook(Box::new(panic_hook));
// Set up the logger with a default level of debug
@ -105,6 +106,24 @@ extern "C" {
fn trace(msg: &str, format: &str);
}
#[wasm_bindgen]
extern "C" {
fn sendMessageToCefFromWasm(message: String);
}
#[wasm_bindgen]
pub fn send_message_to_cef(message: String) {
let global = js_sys::global();
// Get the function by name
let func = js_sys::Reflect::get(&global, &JsValue::from_str("sendMessageToCef")).expect("Function not found");
let func = func.dyn_into::<js_sys::Function>().expect("Not a function");
// Call it with argument
func.call1(&JsValue::NULL, &JsValue::from_str(&message)).expect("Function call failed");
}
#[derive(Default)]
pub struct WasmLog;