Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ members = [
"editor",
"desktop",
"desktop/wrapper",
"desktop/embedded-resources",
"proc-macros",
"frontend/wasm",
"node-graph/gapplication-io",
Expand Down
8 changes: 5 additions & 3 deletions desktop/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ edition = "2024"
rust-version = "1.87"

[features]
default = ["gpu", "accelerated_paint"]
default = ["recommended", "embedded_resources"]
recommended = ["gpu", "accelerated_paint"]
embedded_resources = ["dep:graphite-desktop-embedded-resources"]
gpu = ["graphite-desktop-wrapper/gpu"]

# Hardware acceleration features
Expand All @@ -19,15 +21,15 @@ accelerated_paint_d3d11 = ["windows", "ash"]
accelerated_paint_iosurface = ["objc2-io-surface", "objc2-metal", "core-foundation"]

[dependencies]
# # Local dependencies
# Local dependencies
graphite-desktop-wrapper = { path = "wrapper" }
graphite-desktop-embedded-resources = { path = "embedded-resources", optional = true }

wgpu = { workspace = true }
winit = { workspace = true, features = ["serde"] }
thiserror = { workspace = true }
futures = { workspace = true }
cef = { workspace = true }
include_dir = { workspace = true }
tracing-subscriber = { workspace = true }
tracing = { workspace = true }
dirs = { workspace = true }
Expand Down
15 changes: 15 additions & 0 deletions desktop/embedded-resources/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "graphite-desktop-embedded-resources"
version = "0.1.0"
description = "Graphite Desktop Embedded Resources"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"

[dependencies]
include_dir = { workspace = true }

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(embedded_resources)'] }
17 changes: 17 additions & 0 deletions desktop/embedded-resources/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const RESOURCES: &str = "../../frontend/dist";

// Check if the directory `RESOURCES` exists and sets the embedded_resources cfg accordingly
// Absolute path of `RESOURCES` available via the `EMBEDDED_RESOURCES` environment variable
fn main() {
let crate_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());

println!("cargo:rerun-if-changed={RESOURCES}");
if let Ok(resources) = crate_dir.join(RESOURCES).canonicalize()
&& resources.exists()
{
println!("cargo:rustc-cfg=embedded_resources");
println!("cargo:rustc-env=EMBEDDED_RESOURCES={}", resources.to_string_lossy());
} else {
println!("cargo:warning=Resource directory does not exist. Resources will not be embedded. Did you forget to build the frontend?");
}
}
10 changes: 10 additions & 0 deletions desktop/embedded-resources/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! This crate provides `EMBEDDED_RESOURCES` that can be included in the desktop application binary.
//! It is intended to be used by the `embedded_resources` feature of the `graphite-desktop` crate.
//! The build script checks if the specified resources directory exists and sets the `embedded_resources` cfg flag accordingly.
//! If the resources directory does not exist, resources will not be embedded and a warning will be reported during compilation.

#[cfg(embedded_resources)]
pub static EMBEDDED_RESOURCES: Option<include_dir::Dir> = Some(include_dir::include_dir!("$EMBEDDED_RESOURCES"));

#[cfg(not(embedded_resources))]
pub static EMBEDDED_RESOURCES: Option<include_dir::Dir> = None;
108 changes: 94 additions & 14 deletions desktop/src/cef.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
use crate::CustomEvent;
use crate::render::FrameBufferRef;
use graphite_desktop_wrapper::{WgpuContext, deserialize_editor_message};
use std::fs::File;
use std::io::{Cursor, Read};
use std::path::PathBuf;
use std::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex};
use std::time::Instant;
Expand All @@ -27,7 +30,6 @@ mod input;
mod internal;
mod ipc;
mod platform;
mod scheme_handler;
mod utility;

#[cfg(feature = "accelerated_paint")]
Expand All @@ -38,11 +40,12 @@ use texture_import::SharedTextureHandle;
pub(crate) use context::{CefContext, CefContextBuilder, InitError};
use winit::event_loop::EventLoopProxy;

pub(crate) trait CefEventHandler: Clone {
pub(crate) trait CefEventHandler: Clone + Send + Sync + 'static {
fn window_size(&self) -> WindowSize;
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>);
#[cfg(feature = "accelerated_paint")]
fn draw_gpu(&self, shared_texture: SharedTextureHandle);
fn load_resource(&self, path: PathBuf) -> Option<Resource>;
/// 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);
Expand All @@ -62,12 +65,34 @@ impl WindowSize {
}
}

#[derive(Clone)]
pub(crate) struct Resource {
pub(crate) reader: ResourceReader,
pub(crate) mimetype: Option<String>,
}

#[expect(dead_code)]
#[derive(Clone)]
pub(crate) enum ResourceReader {
Embedded(Cursor<&'static [u8]>),
File(Arc<File>),
}
impl Read for ResourceReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
ResourceReader::Embedded(cursor) => cursor.read(buf),
ResourceReader::File(file) => file.as_ref().read(buf),
}
}
}

#[derive(Clone)]
pub(crate) struct CefHandler {
window_size_receiver: Arc<Mutex<WindowSizeReceiver>>,
event_loop_proxy: EventLoopProxy<CustomEvent>,
wgpu_context: WgpuContext,
}

struct WindowSizeReceiver {
receiver: Receiver<WindowSize>,
window_size: WindowSize,
Expand Down Expand Up @@ -142,6 +167,73 @@ impl CefEventHandler for CefHandler {
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
}

#[cfg(feature = "accelerated_paint")]
fn draw_gpu(&self, shared_texture: SharedTextureHandle) {
match shared_texture.import_texture(&self.wgpu_context.device) {
Ok(texture) => {
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
}
Err(e) => {
tracing::error!("Failed to import shared texture: {}", e);
}
}
}

fn load_resource(&self, path: PathBuf) -> Option<Resource> {
let path = if path.as_os_str().is_empty() { PathBuf::from("index.html") } else { path };

let mimetype = match path.extension().and_then(|s| s.to_str()).unwrap_or("") {
"html" => Some("text/html".to_string()),
"css" => Some("text/css".to_string()),
"txt" => Some("text/plain".to_string()),
"wasm" => Some("application/wasm".to_string()),
"js" => Some("application/javascript".to_string()),
"png" => Some("image/png".to_string()),
"jpg" | "jpeg" => Some("image/jpeg".to_string()),
"svg" => Some("image/svg+xml".to_string()),
"xml" => Some("application/xml".to_string()),
"json" => Some("application/json".to_string()),
"ico" => Some("image/x-icon".to_string()),
"woff" => Some("font/woff".to_string()),
"woff2" => Some("font/woff2".to_string()),
"ttf" => Some("font/ttf".to_string()),
"otf" => Some("font/otf".to_string()),
"webmanifest" => Some("application/manifest+json".to_string()),
"graphite" => Some("application/graphite+json".to_string()),
_ => None,
};

#[cfg(feature = "embedded_resources")]
{
if let Some(resources) = &graphite_desktop_embedded_resources::EMBEDDED_RESOURCES
&& let Some(file) = resources.get_file(&path)
{
return Some(Resource {
reader: ResourceReader::Embedded(Cursor::new(file.contents())),
mimetype,
});
}
}

#[cfg(not(feature = "embedded_resources"))]
{
use std::path::Path;
let asset_path_env = std::env::var("GRAPHITE_RESOURCES").ok()?;
let asset_path = Path::new(&asset_path_env);
let file_path = asset_path.join(path.strip_prefix("/").unwrap_or(&path));
if file_path.exists() && file_path.is_file() {
if let Ok(file) = std::fs::File::open(file_path) {
return Some(Resource {
reader: ResourceReader::File(file.into()),
mimetype,
});
}
}
}

None
}

fn schedule_cef_message_loop_work(&self, scheduled_time: std::time::Instant) {
let _ = self.event_loop_proxy.send_event(CustomEvent::ScheduleBrowserWork(scheduled_time));
}
Expand All @@ -157,16 +249,4 @@ impl CefEventHandler for CefHandler {
};
let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(desktop_wrapper_message));
}

#[cfg(feature = "accelerated_paint")]
fn draw_gpu(&self, shared_texture: SharedTextureHandle) {
match shared_texture.import_texture(&self.wgpu_context.device) {
Ok(texture) => {
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
}
Err(e) => {
tracing::error!("Failed to import shared texture: {}", e);
}
}
}
}
4 changes: 2 additions & 2 deletions desktop/src/cef/consts.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
pub(crate) const GRAPHITE_SCHEME: &str = "graphite-static";
pub(crate) const FRONTEND_DOMAIN: &str = "frontend";
pub(crate) const RESOURCE_SCHEME: &str = "resources";
pub(crate) const RESOURCE_DOMAIN: &str = "resources";
29 changes: 17 additions & 12 deletions desktop/src/cef/context/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,21 @@ use cef::{

use super::CefContext;
use super::singlethreaded::SingleThreadedCefContext;
use crate::cef::CefHandler;
use crate::cef::consts::{FRONTEND_DOMAIN, GRAPHITE_SCHEME};
use crate::cef::CefEventHandler;
use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
use crate::cef::dirs::{cef_cache_dir, cef_data_dir};
use crate::cef::input::InputState;
use crate::cef::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderHandlerImpl, RenderProcessAppImpl};

pub(crate) struct CefContextBuilder {
pub(crate) struct CefContextBuilder<H: CefEventHandler> {
pub(crate) args: Args,
pub(crate) is_sub_process: bool,
_marker: std::marker::PhantomData<H>,
}

unsafe impl Send for CefContextBuilder {}
unsafe impl<H: CefEventHandler> Send for CefContextBuilder<H> {}

impl CefContextBuilder {
impl<H: CefEventHandler> CefContextBuilder<H> {
pub(crate) fn new() -> Self {
#[cfg(target_os = "macos")]
let _loader = {
Expand All @@ -34,7 +35,11 @@ impl CefContextBuilder {
let switch = CefString::from("type");
let is_sub_process = cmd.has_switch(Some(&switch)) == 1;

Self { args, is_sub_process }
Self {
args,
is_sub_process,
_marker: std::marker::PhantomData,
}
}

pub(crate) fn is_sub_process(&self) -> bool {
Expand All @@ -45,7 +50,7 @@ impl CefContextBuilder {
let cmd = self.args.as_cmd_line().unwrap();
let switch = CefString::from("type");
let process_type = CefString::from(&cmd.switch_value(Some(&switch)));
let mut app = RenderProcessAppImpl::app();
let mut app = RenderProcessAppImpl::<H>::app();
let ret = execute_process(Some(self.args.as_main_args()), Some(&mut app), std::ptr::null_mut());
if ret >= 0 {
SetupError::SubprocessFailed(process_type.to_string())
Expand All @@ -55,7 +60,7 @@ impl CefContextBuilder {
}

#[cfg(target_os = "macos")]
pub(crate) fn initialize(self, event_handler: CefHandler) -> Result<impl CefContext, InitError> {
pub(crate) fn initialize(self, event_handler: H) -> Result<impl CefContext, InitError> {
let settings = Settings {
windowless_rendering_enabled: 1,
multi_threaded_message_loop: 0,
Expand All @@ -71,7 +76,7 @@ impl CefContextBuilder {
}

#[cfg(not(target_os = "macos"))]
pub(crate) fn initialize(self, event_handler: CefHandler) -> Result<impl CefContext, InitError> {
pub(crate) fn initialize(self, event_handler: H) -> Result<impl CefContext, InitError> {
let settings = Settings {
windowless_rendering_enabled: 1,
multi_threaded_message_loop: 1,
Expand All @@ -97,7 +102,7 @@ impl CefContextBuilder {
Ok(super::multithreaded::MultiThreadedCefContextProxy)
}

fn initialize_inner(self, event_handler: &CefHandler, settings: Settings) -> Result<(), InitError> {
fn initialize_inner(self, event_handler: &H, settings: Settings) -> Result<(), InitError> {
let mut cef_app = App::new(BrowserProcessAppImpl::new(event_handler.clone()));
let result = cef::initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut());
// Attention! Wrapping this in an extra App is necessary, otherwise the program still compiles but segfaults
Expand All @@ -113,11 +118,11 @@ impl CefContextBuilder {
}
}

fn create_browser(event_handler: CefHandler) -> Result<SingleThreadedCefContext, InitError> {
fn create_browser<H: CefEventHandler>(event_handler: H) -> Result<SingleThreadedCefContext, InitError> {
let render_handler = RenderHandler::new(RenderHandlerImpl::new(event_handler.clone()));
let mut client = Client::new(BrowserProcessClientImpl::new(render_handler, event_handler.clone()));

let url = CefString::from(format!("{GRAPHITE_SCHEME}://{FRONTEND_DOMAIN}/").as_str());
let url = CefString::from(format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/").as_str());

let window_info = WindowInfo {
windowless_rendering_enabled: 1,
Expand Down
4 changes: 4 additions & 0 deletions desktop/src/cef/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ mod browser_process_app;
mod browser_process_client;
mod browser_process_handler;
mod browser_process_life_span_handler;

mod render_process_app;
mod render_process_handler;
mod render_process_v8_handler;

mod resource_handler;
mod scheme_handler_factory;

pub(super) mod render_handler;
pub(super) mod task;

Expand Down
8 changes: 3 additions & 5 deletions desktop/src/cef/internal/browser_process_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ use cef::rc::{Rc, RcImpl};
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;
use super::scheme_handler_factory::SchemeHandlerFactoryImpl;
use crate::cef::CefEventHandler;

pub(crate) struct BrowserProcessAppImpl<H: CefEventHandler> {
object: *mut RcImpl<_cef_app_t, Self>,
Expand All @@ -30,7 +28,7 @@ impl<H: CefEventHandler + Clone> ImplApp for BrowserProcessAppImpl<H> {
}

fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
GraphiteSchemeHandlerFactory::register_schemes(registrar);
SchemeHandlerFactoryImpl::<H>::register_schemes(registrar);
}

fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) {
Expand Down
Loading