Skip to content

Commit 1d4d102

Browse files
Desktop: Make embedded resources optional (#3094)
* Make embedding resources optional * Move remaining cef rc to internal module * Move embedded resources to separate crate * Review fixup * Fix * Fix read * Add read error
1 parent 95ef8a5 commit 1d4d102

19 files changed

+389
-276
lines changed

Cargo.lock

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ members = [
33
"editor",
44
"desktop",
55
"desktop/wrapper",
6+
"desktop/embedded-resources",
67
"proc-macros",
78
"frontend/wasm",
89
"node-graph/gapplication-io",

desktop/Cargo.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ edition = "2024"
99
rust-version = "1.87"
1010

1111
[features]
12-
default = ["gpu", "accelerated_paint"]
12+
default = ["recommended", "embedded_resources"]
13+
recommended = ["gpu", "accelerated_paint"]
14+
embedded_resources = ["dep:graphite-desktop-embedded-resources"]
1315
gpu = ["graphite-desktop-wrapper/gpu"]
1416

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

2123
[dependencies]
22-
# # Local dependencies
24+
# Local dependencies
2325
graphite-desktop-wrapper = { path = "wrapper" }
26+
graphite-desktop-embedded-resources = { path = "embedded-resources", optional = true }
2427

2528
wgpu = { workspace = true }
2629
winit = { workspace = true, features = ["serde"] }
2730
thiserror = { workspace = true }
2831
futures = { workspace = true }
2932
cef = { workspace = true }
30-
include_dir = { workspace = true }
3133
tracing-subscriber = { workspace = true }
3234
tracing = { workspace = true }
3335
dirs = { workspace = true }

desktop/embedded-resources/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "graphite-desktop-embedded-resources"
3+
version = "0.1.0"
4+
description = "Graphite Desktop Embedded Resources"
5+
authors = ["Graphite Authors <[email protected]>"]
6+
license = "Apache-2.0"
7+
repository = ""
8+
edition = "2024"
9+
rust-version = "1.87"
10+
11+
[dependencies]
12+
include_dir = { workspace = true }
13+
14+
[lints.rust]
15+
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(embedded_resources)'] }

desktop/embedded-resources/build.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const RESOURCES: &str = "../../frontend/dist";
2+
3+
// Check if the directory `RESOURCES` exists and sets the embedded_resources cfg accordingly
4+
// Absolute path of `RESOURCES` available via the `EMBEDDED_RESOURCES` environment variable
5+
fn main() {
6+
let crate_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
7+
8+
println!("cargo:rerun-if-changed={RESOURCES}");
9+
if let Ok(resources) = crate_dir.join(RESOURCES).canonicalize()
10+
&& resources.exists()
11+
{
12+
println!("cargo:rustc-cfg=embedded_resources");
13+
println!("cargo:rustc-env=EMBEDDED_RESOURCES={}", resources.to_string_lossy());
14+
} else {
15+
println!("cargo:warning=Resource directory does not exist. Resources will not be embedded. Did you forget to build the frontend?");
16+
}
17+
}

desktop/embedded-resources/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//! This crate provides `EMBEDDED_RESOURCES` that can be included in the desktop application binary.
2+
//! It is intended to be used by the `embedded_resources` feature of the `graphite-desktop` crate.
3+
//! The build script checks if the specified resources directory exists and sets the `embedded_resources` cfg flag accordingly.
4+
//! If the resources directory does not exist, resources will not be embedded and a warning will be reported during compilation.
5+
6+
#[cfg(embedded_resources)]
7+
pub static EMBEDDED_RESOURCES: Option<include_dir::Dir> = Some(include_dir::include_dir!("$EMBEDDED_RESOURCES"));
8+
9+
#[cfg(not(embedded_resources))]
10+
pub static EMBEDDED_RESOURCES: Option<include_dir::Dir> = None;

desktop/src/cef.rs

Lines changed: 94 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
use crate::CustomEvent;
1717
use crate::render::FrameBufferRef;
1818
use graphite_desktop_wrapper::{WgpuContext, deserialize_editor_message};
19+
use std::fs::File;
20+
use std::io::{Cursor, Read};
21+
use std::path::PathBuf;
1922
use std::sync::mpsc::Receiver;
2023
use std::sync::{Arc, Mutex};
2124
use std::time::Instant;
@@ -27,7 +30,6 @@ mod input;
2730
mod internal;
2831
mod ipc;
2932
mod platform;
30-
mod scheme_handler;
3133
mod utility;
3234

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

41-
pub(crate) trait CefEventHandler: Clone {
43+
pub(crate) trait CefEventHandler: Clone + Send + Sync + 'static {
4244
fn window_size(&self) -> WindowSize;
4345
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>);
4446
#[cfg(feature = "accelerated_paint")]
4547
fn draw_gpu(&self, shared_texture: SharedTextureHandle);
48+
fn load_resource(&self, path: PathBuf) -> Option<Resource>;
4649
/// Scheudule the main event loop to run the cef event loop after the timeout
4750
/// [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation.
4851
fn schedule_cef_message_loop_work(&self, scheduled_time: Instant);
@@ -62,12 +65,34 @@ impl WindowSize {
6265
}
6366
}
6467

68+
#[derive(Clone)]
69+
pub(crate) struct Resource {
70+
pub(crate) reader: ResourceReader,
71+
pub(crate) mimetype: Option<String>,
72+
}
73+
74+
#[expect(dead_code)]
75+
#[derive(Clone)]
76+
pub(crate) enum ResourceReader {
77+
Embedded(Cursor<&'static [u8]>),
78+
File(Arc<File>),
79+
}
80+
impl Read for ResourceReader {
81+
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
82+
match self {
83+
ResourceReader::Embedded(cursor) => cursor.read(buf),
84+
ResourceReader::File(file) => file.as_ref().read(buf),
85+
}
86+
}
87+
}
88+
6589
#[derive(Clone)]
6690
pub(crate) struct CefHandler {
6791
window_size_receiver: Arc<Mutex<WindowSizeReceiver>>,
6892
event_loop_proxy: EventLoopProxy<CustomEvent>,
6993
wgpu_context: WgpuContext,
7094
}
95+
7196
struct WindowSizeReceiver {
7297
receiver: Receiver<WindowSize>,
7398
window_size: WindowSize,
@@ -142,6 +167,73 @@ impl CefEventHandler for CefHandler {
142167
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
143168
}
144169

170+
#[cfg(feature = "accelerated_paint")]
171+
fn draw_gpu(&self, shared_texture: SharedTextureHandle) {
172+
match shared_texture.import_texture(&self.wgpu_context.device) {
173+
Ok(texture) => {
174+
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
175+
}
176+
Err(e) => {
177+
tracing::error!("Failed to import shared texture: {}", e);
178+
}
179+
}
180+
}
181+
182+
fn load_resource(&self, path: PathBuf) -> Option<Resource> {
183+
let path = if path.as_os_str().is_empty() { PathBuf::from("index.html") } else { path };
184+
185+
let mimetype = match path.extension().and_then(|s| s.to_str()).unwrap_or("") {
186+
"html" => Some("text/html".to_string()),
187+
"css" => Some("text/css".to_string()),
188+
"txt" => Some("text/plain".to_string()),
189+
"wasm" => Some("application/wasm".to_string()),
190+
"js" => Some("application/javascript".to_string()),
191+
"png" => Some("image/png".to_string()),
192+
"jpg" | "jpeg" => Some("image/jpeg".to_string()),
193+
"svg" => Some("image/svg+xml".to_string()),
194+
"xml" => Some("application/xml".to_string()),
195+
"json" => Some("application/json".to_string()),
196+
"ico" => Some("image/x-icon".to_string()),
197+
"woff" => Some("font/woff".to_string()),
198+
"woff2" => Some("font/woff2".to_string()),
199+
"ttf" => Some("font/ttf".to_string()),
200+
"otf" => Some("font/otf".to_string()),
201+
"webmanifest" => Some("application/manifest+json".to_string()),
202+
"graphite" => Some("application/graphite+json".to_string()),
203+
_ => None,
204+
};
205+
206+
#[cfg(feature = "embedded_resources")]
207+
{
208+
if let Some(resources) = &graphite_desktop_embedded_resources::EMBEDDED_RESOURCES
209+
&& let Some(file) = resources.get_file(&path)
210+
{
211+
return Some(Resource {
212+
reader: ResourceReader::Embedded(Cursor::new(file.contents())),
213+
mimetype,
214+
});
215+
}
216+
}
217+
218+
#[cfg(not(feature = "embedded_resources"))]
219+
{
220+
use std::path::Path;
221+
let asset_path_env = std::env::var("GRAPHITE_RESOURCES").ok()?;
222+
let asset_path = Path::new(&asset_path_env);
223+
let file_path = asset_path.join(path.strip_prefix("/").unwrap_or(&path));
224+
if file_path.exists() && file_path.is_file() {
225+
if let Ok(file) = std::fs::File::open(file_path) {
226+
return Some(Resource {
227+
reader: ResourceReader::File(file.into()),
228+
mimetype,
229+
});
230+
}
231+
}
232+
}
233+
234+
None
235+
}
236+
145237
fn schedule_cef_message_loop_work(&self, scheduled_time: std::time::Instant) {
146238
let _ = self.event_loop_proxy.send_event(CustomEvent::ScheduleBrowserWork(scheduled_time));
147239
}
@@ -157,16 +249,4 @@ impl CefEventHandler for CefHandler {
157249
};
158250
let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(desktop_wrapper_message));
159251
}
160-
161-
#[cfg(feature = "accelerated_paint")]
162-
fn draw_gpu(&self, shared_texture: SharedTextureHandle) {
163-
match shared_texture.import_texture(&self.wgpu_context.device) {
164-
Ok(texture) => {
165-
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
166-
}
167-
Err(e) => {
168-
tracing::error!("Failed to import shared texture: {}", e);
169-
}
170-
}
171-
}
172252
}

desktop/src/cef/consts.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
pub(crate) const GRAPHITE_SCHEME: &str = "graphite-static";
2-
pub(crate) const FRONTEND_DOMAIN: &str = "frontend";
1+
pub(crate) const RESOURCE_SCHEME: &str = "resources";
2+
pub(crate) const RESOURCE_DOMAIN: &str = "resources";

desktop/src/cef/context/builder.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,21 @@ use cef::{
66

77
use super::CefContext;
88
use super::singlethreaded::SingleThreadedCefContext;
9-
use crate::cef::CefHandler;
10-
use crate::cef::consts::{FRONTEND_DOMAIN, GRAPHITE_SCHEME};
9+
use crate::cef::CefEventHandler;
10+
use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
1111
use crate::cef::dirs::{cef_cache_dir, cef_data_dir};
1212
use crate::cef::input::InputState;
1313
use crate::cef::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderHandlerImpl, RenderProcessAppImpl};
1414

15-
pub(crate) struct CefContextBuilder {
15+
pub(crate) struct CefContextBuilder<H: CefEventHandler> {
1616
pub(crate) args: Args,
1717
pub(crate) is_sub_process: bool,
18+
_marker: std::marker::PhantomData<H>,
1819
}
1920

20-
unsafe impl Send for CefContextBuilder {}
21+
unsafe impl<H: CefEventHandler> Send for CefContextBuilder<H> {}
2122

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

37-
Self { args, is_sub_process }
38+
Self {
39+
args,
40+
is_sub_process,
41+
_marker: std::marker::PhantomData,
42+
}
3843
}
3944

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

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

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

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

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

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

122127
let window_info = WindowInfo {
123128
windowless_rendering_enabled: 1,

desktop/src/cef/internal.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ mod browser_process_app;
22
mod browser_process_client;
33
mod browser_process_handler;
44
mod browser_process_life_span_handler;
5+
56
mod render_process_app;
67
mod render_process_handler;
78
mod render_process_v8_handler;
89

10+
mod resource_handler;
11+
mod scheme_handler_factory;
12+
913
pub(super) mod render_handler;
1014
pub(super) mod task;
1115

0 commit comments

Comments
 (0)