mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 23:08:05 +08:00
* Extract CEF rendered UI into a separate process and crate * Review * Review * Review * Review * Remove necessary workarounds * Block on frame copy ack * Crop and resample frames correctly * Skip blank frames * Fix deps * Fix fmt * Fix clippy warning * Review * Fix todo
351 lines
11 KiB
Rust
351 lines
11 KiB
Rust
use cef::args::Args;
|
|
use cef::sys::{CEF_API_VERSION_LAST, cef_log_severity_t, cef_thread_id_t};
|
|
use cef::{
|
|
App, Browser, BrowserSettings, CefString, Client, DictionaryValue, ImplBrowser, ImplBrowserHost, ImplCommandLine, ImplRequestContext, LogSeverity, RequestContextSettings, SchemeHandlerFactory,
|
|
Settings, Task, ThreadId, WindowInfo, api_hash, browser_host_create_browser_sync, execute_process, post_task,
|
|
};
|
|
use std::cell::RefCell;
|
|
use std::marker::PhantomData;
|
|
use std::path::Path;
|
|
use std::sync::mpsc::Sender;
|
|
|
|
use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME, WINDOWLESS_FRAME_RATE};
|
|
use crate::delegate::BrowserDelegate;
|
|
use crate::dirs::TempDir;
|
|
use crate::frames::FrameStreamer;
|
|
use crate::input::{self, InputEvent};
|
|
use crate::internal::task::ClosureTask;
|
|
use crate::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderProcessAppImpl, SchemeHandlerFactoryImpl};
|
|
use crate::ipc::{MessageType, SendMessage};
|
|
use crate::view::ViewInfoUpdate;
|
|
|
|
thread_local! {
|
|
static CONTEXT: RefCell<Option<BrowserContext>> = const { RefCell::new(None) };
|
|
}
|
|
|
|
pub(crate) struct CefContext {
|
|
_not_send: PhantomData<*const ()>, // impl !Send for CefContext
|
|
}
|
|
|
|
impl CefContext {
|
|
pub(crate) fn create(delegate: BrowserDelegate, frames: FrameStreamer, view_info_sender: Sender<ViewInfoUpdate>, accelerated_paint: bool) -> Result<Self, InitError> {
|
|
let args = bootstrap(false);
|
|
#[cfg(target_os = "macos")]
|
|
crate::platform::mac::install_application();
|
|
|
|
let instance_dir = TempDir::new().map_err(|e| InitError::InstanceDirectoryCreationFailed(e.to_string()))?;
|
|
initialize(&args, instance_dir.as_ref(), accelerated_paint)?;
|
|
|
|
let (created_tx, created_rx) = std::sync::mpsc::channel();
|
|
let install_browser = move || {
|
|
let result = create_browser(delegate, frames, view_info_sender, instance_dir, accelerated_paint).map(|context| CONTEXT.with(|b| *b.borrow_mut() = Some(context)));
|
|
let _ = created_tx.send(result);
|
|
};
|
|
#[cfg(target_os = "macos")]
|
|
install_browser();
|
|
#[cfg(not(target_os = "macos"))]
|
|
run_on_ui_thread(install_browser);
|
|
|
|
created_rx.recv().unwrap_or(Err(InitError::BrowserCreationFailed))?;
|
|
Ok(Self { _not_send: PhantomData })
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
pub(crate) fn run<R: Send + 'static>(self, control: impl FnOnce(CefContextHandle) -> R + Send + 'static) -> R {
|
|
let result = control(CefContextHandle);
|
|
let (dropped_sender, dropped_receiver) = std::sync::mpsc::channel();
|
|
run_on_ui_thread(move || {
|
|
drop(CONTEXT.take());
|
|
let _ = dropped_sender.send(());
|
|
});
|
|
let _ = dropped_receiver.recv();
|
|
cef::shutdown();
|
|
result
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
pub(crate) fn run<R: Send + 'static>(self, control: impl FnOnce(CefContextHandle) -> R + Send + 'static) -> R {
|
|
let (result_sender, result_receiver) = std::sync::mpsc::channel();
|
|
let control_thread = std::thread::Builder::new()
|
|
.name("cef-host-control".to_string())
|
|
.spawn(move || {
|
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| control(CefContextHandle)));
|
|
with_context(|context| {
|
|
if let Some(host) = context.browser.host() {
|
|
host.close_browser(1);
|
|
}
|
|
});
|
|
run_on_ui_thread(cef::quit_message_loop);
|
|
match result {
|
|
Ok(result) => {
|
|
let _ = result_sender.send(result);
|
|
}
|
|
Err(panic) => std::panic::resume_unwind(panic),
|
|
}
|
|
})
|
|
.expect("Failed to spawn the CEF control thread");
|
|
cef::run_message_loop();
|
|
drop(CONTEXT.take());
|
|
cef::shutdown();
|
|
if let Err(panic) = control_thread.join() {
|
|
std::panic::resume_unwind(panic);
|
|
}
|
|
result_receiver.recv().expect("The CEF control thread ended without a result")
|
|
}
|
|
}
|
|
|
|
pub(crate) fn execute_helper_process() -> std::process::ExitCode {
|
|
let args = bootstrap(true);
|
|
assert_eq!(args.as_cmd_line().unwrap().has_switch(Some(&"type".into())), 1, "Not a CEF helper process");
|
|
let mut app = RenderProcessAppImpl::app();
|
|
let code = execute_process(Some(args.as_main_args()), Some(&mut app), std::ptr::null_mut());
|
|
std::process::ExitCode::from(code as u8)
|
|
}
|
|
|
|
fn bootstrap(helper: bool) -> Args {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let loader = cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), helper);
|
|
assert!(loader.load());
|
|
// LibraryLoader unloads the framework on drop
|
|
std::mem::forget(loader);
|
|
}
|
|
#[cfg(not(target_os = "macos"))]
|
|
let _ = helper;
|
|
|
|
let _ = api_hash(CEF_API_VERSION_LAST, 0);
|
|
Args::new()
|
|
}
|
|
|
|
fn initialize(args: &Args, instance_dir: &Path, accelerated_paint: bool) -> Result<(), InitError> {
|
|
let mut app = App::new(BrowserProcessAppImpl::new(accelerated_paint));
|
|
if cef::initialize(Some(args.as_main_args()), Some(&platform_settings(instance_dir)?), Some(&mut app), std::ptr::null_mut()) != 1 {
|
|
return Err(InitError::InitializationFailureCode(cef::get_exit_code() as u32));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn platform_settings(instance_dir: &Path) -> Result<Settings, InitError> {
|
|
let log_severity = match std::env::var("GRAPHITE_BROWSER_LOG").unwrap_or_default().to_lowercase().as_str() {
|
|
"debug" => cef_log_severity_t::LOGSEVERITY_VERBOSE,
|
|
"info" => cef_log_severity_t::LOGSEVERITY_INFO,
|
|
"warn" => cef_log_severity_t::LOGSEVERITY_WARNING,
|
|
"error" => cef_log_severity_t::LOGSEVERITY_ERROR,
|
|
"none" => cef_log_severity_t::LOGSEVERITY_DISABLE,
|
|
_ => cef_log_severity_t::LOGSEVERITY_FATAL,
|
|
};
|
|
|
|
let Some(root_cache_path) = instance_dir.to_str().map(CefString::from) else {
|
|
return Err(InitError::PathResolutionFailed(format!("non-UTF-8 instance directory path: {}", instance_dir.display())));
|
|
};
|
|
let base = Settings {
|
|
windowless_rendering_enabled: 1,
|
|
root_cache_path,
|
|
cache_path: "".into(),
|
|
disable_signal_handlers: 1,
|
|
log_severity: LogSeverity::from(log_severity),
|
|
..Default::default()
|
|
};
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let exe = std::env::current_exe().map_err(|e| InitError::PathResolutionFailed(format!("cannot get current exe path: {e}")))?;
|
|
let app_root = exe
|
|
.parent()
|
|
.and_then(|p| p.parent())
|
|
.and_then(|p| p.parent())
|
|
.ok_or_else(|| InitError::PathResolutionFailed(format!("executable is not inside an app bundle: {}", exe.display())))?;
|
|
let Some(main_bundle_path) = app_root.to_str().map(CefString::from) else {
|
|
return Err(InitError::PathResolutionFailed(format!("invalid app bundle path: {}", app_root.display())));
|
|
};
|
|
Ok(Settings {
|
|
main_bundle_path,
|
|
multi_threaded_message_loop: 0,
|
|
external_message_pump: 0,
|
|
no_sandbox: 1, // GPU helper crashes when running with sandbox
|
|
..base
|
|
})
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
Ok(Settings {
|
|
multi_threaded_message_loop: 1,
|
|
#[cfg(target_os = "linux")]
|
|
no_sandbox: 1,
|
|
..base
|
|
})
|
|
}
|
|
|
|
fn create_browser(delegate: BrowserDelegate, frames: FrameStreamer, view_info_sender: Sender<ViewInfoUpdate>, instance_dir: TempDir, accelerated_paint: bool) -> Result<BrowserContext, InitError> {
|
|
#[cfg(not(feature = "accelerated_paint"))]
|
|
let _ = accelerated_paint;
|
|
let mut client = Client::new(BrowserProcessClientImpl::new(&delegate, frames));
|
|
|
|
let window_info = WindowInfo {
|
|
windowless_rendering_enabled: 1,
|
|
#[cfg(feature = "accelerated_paint")]
|
|
shared_texture_enabled: accelerated_paint as i32,
|
|
..Default::default()
|
|
};
|
|
|
|
let settings = BrowserSettings {
|
|
windowless_frame_rate: WINDOWLESS_FRAME_RATE,
|
|
background_color: 0x0,
|
|
..Default::default()
|
|
};
|
|
|
|
let Some(mut incognito_request_context) = cef::request_context_create_context(
|
|
Some(&RequestContextSettings {
|
|
persist_session_cookies: 0,
|
|
cache_path: "".into(),
|
|
..Default::default()
|
|
}),
|
|
Option::<&mut cef::RequestContextHandler>::None,
|
|
) else {
|
|
return Err(InitError::RequestContextCreationFailed);
|
|
};
|
|
|
|
let mut scheme_handler_factory = SchemeHandlerFactory::new(SchemeHandlerFactoryImpl::new(delegate.clone()));
|
|
incognito_request_context.clear_scheme_handler_factories();
|
|
if incognito_request_context.register_scheme_handler_factory(Some(&RESOURCE_SCHEME.into()), Some(&RESOURCE_DOMAIN.into()), Some(&mut scheme_handler_factory)) != 1 {
|
|
return Err(InitError::SchemeHandlerRegistrationFailed);
|
|
}
|
|
|
|
let url = format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/");
|
|
browser_host_create_browser_sync(
|
|
Some(&window_info),
|
|
Some(&mut client),
|
|
Some(&url.as_str().into()),
|
|
Some(&settings),
|
|
Option::<&mut DictionaryValue>::None,
|
|
Some(&mut incognito_request_context),
|
|
)
|
|
.map(|browser| BrowserContext {
|
|
delegate,
|
|
browser,
|
|
view_info_sender,
|
|
_instance_dir: instance_dir,
|
|
})
|
|
.ok_or_else(|| {
|
|
tracing::error!("Failed to create browser");
|
|
InitError::BrowserCreationFailed
|
|
})
|
|
}
|
|
|
|
#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
pub(crate) enum InitError {
|
|
#[error("Failed to create the instance directory: {0}")]
|
|
InstanceDirectoryCreationFailed(String),
|
|
#[error("Initialization failed with code: {0}")]
|
|
InitializationFailureCode(u32),
|
|
#[error("Browser creation failed")]
|
|
BrowserCreationFailed,
|
|
#[error("Request context creation failed")]
|
|
RequestContextCreationFailed,
|
|
#[error("Failed to resolve a required path: {0}")]
|
|
PathResolutionFailed(String),
|
|
#[error("Scheme handler registration failed")]
|
|
SchemeHandlerRegistrationFailed,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct CefContextHandle;
|
|
|
|
impl CefContextHandle {
|
|
pub(crate) fn apply_input(&self, events: Vec<InputEvent>) {
|
|
with_context(move |context| {
|
|
for event in &events {
|
|
input::apply(&context.browser, event);
|
|
}
|
|
});
|
|
}
|
|
|
|
pub(crate) fn update_view_info(&self, update: ViewInfoUpdate) {
|
|
with_context(move |context| context.update_view_info(update));
|
|
}
|
|
|
|
pub(crate) fn refresh_view_info(&self) {
|
|
with_context(|context| context.refresh_view_info());
|
|
}
|
|
|
|
pub(crate) fn send_web_message(&self, message: Vec<u8>) {
|
|
with_context(move |context| context.send_web_message(message));
|
|
}
|
|
}
|
|
|
|
struct BrowserContext {
|
|
delegate: BrowserDelegate,
|
|
browser: Browser,
|
|
view_info_sender: Sender<ViewInfoUpdate>,
|
|
_instance_dir: TempDir,
|
|
}
|
|
|
|
impl BrowserContext {
|
|
fn update_view_info(&self, update: ViewInfoUpdate) {
|
|
let _ = self.view_info_sender.send(update);
|
|
}
|
|
|
|
fn refresh_view_info(&self) {
|
|
let view_info = self.delegate.view_info();
|
|
let Some(host) = self.browser.host() else {
|
|
tracing::error!("Browser host is not available, cannot refresh view info");
|
|
return;
|
|
};
|
|
host.set_zoom_level(view_info.zoom());
|
|
host.was_resized();
|
|
|
|
// Fix for CEF not updating the view after resize
|
|
// TODO: remove once https://github.com/chromiumembedded/cef/issues/3822 is fixed
|
|
host.invalidate(cef::PaintElementType::default());
|
|
}
|
|
|
|
fn send_web_message(&self, message: Vec<u8>) {
|
|
self.send_message(MessageType::SendToJS, &message);
|
|
}
|
|
}
|
|
|
|
impl Drop for BrowserContext {
|
|
fn drop(&mut self) {
|
|
tracing::debug!("Shutting down CEF");
|
|
if let Some(host) = self.browser.host() {
|
|
host.close_browser(1);
|
|
} else {
|
|
tracing::error!("Browser host is not available, cannot close browser");
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SendMessage for BrowserContext {
|
|
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
|
let Some(frame) = self.browser.main_frame() else {
|
|
tracing::error!("Main frame is not available, cannot send message");
|
|
return;
|
|
};
|
|
frame.send_message(message_type, message);
|
|
}
|
|
}
|
|
|
|
fn run_on_ui_thread<F>(closure: F)
|
|
where
|
|
F: FnOnce() + Send + 'static,
|
|
{
|
|
let closure_task = ClosureTask::new(closure);
|
|
let mut task = Task::new(closure_task);
|
|
if post_task(ThreadId::from(cef_thread_id_t::TID_UI), Some(&mut task)) != 1 {
|
|
tracing::error!("Failed to post a task to the CEF UI thread");
|
|
}
|
|
}
|
|
|
|
fn with_context<F>(closure: F)
|
|
where
|
|
F: FnOnce(&mut BrowserContext) + Send + 'static,
|
|
{
|
|
run_on_ui_thread(move || {
|
|
CONTEXT.with(|b| {
|
|
if let Some(context) = b.borrow_mut().as_mut() {
|
|
closure(context);
|
|
}
|
|
});
|
|
});
|
|
}
|