mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-30 09:58:11 +08:00
Desktop: Isolate CEF-rendered UI into separate crate and process (#4321)
* 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
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
use std::process::ExitCode;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::{Receiver, RecvTimeoutError};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::remote::messages::HostControlMessage;
|
||||
use crate::remote::spawn::HostHandle;
|
||||
|
||||
mod consts;
|
||||
mod context;
|
||||
mod delegate;
|
||||
mod dirs;
|
||||
mod events;
|
||||
mod frames;
|
||||
mod input;
|
||||
mod internal;
|
||||
mod ipc;
|
||||
mod platform;
|
||||
mod remote;
|
||||
mod resources;
|
||||
mod utility;
|
||||
mod view;
|
||||
|
||||
pub struct UiContext<S: Stage = Started> {
|
||||
inner: S::ContextData,
|
||||
}
|
||||
|
||||
impl UiContext<Setup> {
|
||||
pub fn setup() -> UiSetupResult {
|
||||
#[cfg(target_os = "macos")]
|
||||
ipc_channel::set_bootstrap_prefix(consts::IPC_BOOTSTRAP_PREFIX);
|
||||
|
||||
let raw_args: Vec<String> = std::env::args().collect();
|
||||
if raw_args.iter().any(|arg| arg.starts_with(consts::BROWSER_HOST_CONFIG_FLAG)) {
|
||||
remote::host::run();
|
||||
return UiSetupResult::Helper(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
if raw_args.iter().any(|arg| arg.starts_with("--type=")) {
|
||||
return UiSetupResult::Helper(run_helper());
|
||||
}
|
||||
UiSetupResult::Ready(UiContext { inner: () })
|
||||
}
|
||||
|
||||
pub fn start(self, config: UiConfig) -> Result<UiContext<Started>, UiError> {
|
||||
let acceleration = platform::accelerated_paint(matches!(config.acceleration, Acceleration::Disabled));
|
||||
let handle = remote::spawn::spawn_host(acceleration)?;
|
||||
Ok(UiContext { inner: Arc::new(handle) })
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub enum UiSetupResult {
|
||||
Ready(UiContext<Setup>),
|
||||
Failed,
|
||||
Helper(ExitCode),
|
||||
}
|
||||
|
||||
impl UiContext<Started> {
|
||||
pub fn instance(&self, device: &wgpu::Device, queue: &wgpu_sync::Queue) -> Result<UiInstance, UiError> {
|
||||
let surface = frames::FrameSurface::new(device.clone(), queue.clone());
|
||||
|
||||
let (queue, events) = events::EventQueue::new();
|
||||
let shutdown_complete = remote::spawn::start_instance(&self.inner, surface, queue.clone())?;
|
||||
|
||||
Ok(UiInstance {
|
||||
inner: Arc::new(UiInstanceInner {
|
||||
host: self.inner.clone(),
|
||||
input: Mutex::new(input::InputState::default()),
|
||||
events: Mutex::new(events),
|
||||
queue,
|
||||
shutdown_complete: Mutex::new(shutdown_complete),
|
||||
shutdown_started: AtomicBool::new(false),
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for UiContext<Started> {
|
||||
fn clone(&self) -> Self {
|
||||
UiContext { inner: self.inner.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Setup {}
|
||||
pub enum Started {}
|
||||
|
||||
#[expect(private_bounds)]
|
||||
pub trait Stage: Sealed {}
|
||||
impl Stage for Setup {}
|
||||
impl Stage for Started {}
|
||||
trait Sealed {
|
||||
type ContextData;
|
||||
}
|
||||
impl Sealed for Setup {
|
||||
type ContextData = ();
|
||||
}
|
||||
impl Sealed for Started {
|
||||
type ContextData = Arc<HostHandle>;
|
||||
}
|
||||
|
||||
pub fn temp_dir_root() -> std::path::PathBuf {
|
||||
dirs::app_tmp_dir()
|
||||
}
|
||||
|
||||
pub fn run_helper() -> ExitCode {
|
||||
context::execute_helper_process()
|
||||
}
|
||||
|
||||
pub struct UiConfig {
|
||||
pub acceleration: Acceleration,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Acceleration {
|
||||
Auto,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum UiError {
|
||||
#[error("failed to bootstrap the UI backend: {0}")]
|
||||
Bootstrap(String),
|
||||
#[error("failed to spawn the UI backend host process: {0}")]
|
||||
Spawn(std::io::Error),
|
||||
#[error("the UI backend host process exited during startup: {0}")]
|
||||
HostExited(String),
|
||||
#[error("timed out waiting for the UI backend host process to connect")]
|
||||
HandshakeTimeout,
|
||||
#[error("UI backend handshake failed: {0}")]
|
||||
Handshake(String),
|
||||
#[error("the UI runtime already drives an instance")]
|
||||
InstanceLimit,
|
||||
}
|
||||
|
||||
pub struct UiInstance {
|
||||
inner: Arc<UiInstanceInner>,
|
||||
}
|
||||
|
||||
pub(crate) struct UiInstanceInner {
|
||||
host: Arc<HostHandle>,
|
||||
input: Mutex<input::InputState>,
|
||||
events: Mutex<Receiver<UiEvent>>,
|
||||
queue: events::EventQueue,
|
||||
shutdown_complete: Mutex<Receiver<()>>,
|
||||
shutdown_started: AtomicBool,
|
||||
}
|
||||
|
||||
impl UiInstance {
|
||||
pub fn send(&self, command: UiCommand) {
|
||||
let shared = &self.inner;
|
||||
match command {
|
||||
UiCommand::Input(event) => {
|
||||
let events = {
|
||||
let Ok(mut input) = shared.input.lock() else {
|
||||
tracing::error!("Failed to lock the input state");
|
||||
return;
|
||||
};
|
||||
input::translate(&mut input, &event)
|
||||
};
|
||||
if !events.is_empty() {
|
||||
shared.host.send(HostControlMessage::Input(events));
|
||||
}
|
||||
}
|
||||
UiCommand::Resized { width, height } => shared.host.send(HostControlMessage::UpdateViewInfo(view::ViewInfoUpdate::Size { width, height })),
|
||||
UiCommand::ScaleChanged(scale) => shared.host.send(HostControlMessage::UpdateViewInfo(view::ViewInfoUpdate::Scale(scale))),
|
||||
UiCommand::Refresh => shared.host.send(HostControlMessage::RefreshViewInfo),
|
||||
UiCommand::Message(message) => shared.host.send(HostControlMessage::SendWebMessage(message)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv(&self) -> Option<UiEvent> {
|
||||
let shared = &self.inner;
|
||||
let Ok(receiver) = shared.events.lock() else {
|
||||
return None;
|
||||
};
|
||||
loop {
|
||||
match receiver.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(event) => return Some(event),
|
||||
Err(RecvTimeoutError::Timeout) => {
|
||||
if shared.queue.is_terminated() {
|
||||
return receiver.try_recv().ok();
|
||||
}
|
||||
}
|
||||
Err(RecvTimeoutError::Disconnected) => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) {
|
||||
self.inner.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for UiInstance {
|
||||
fn clone(&self) -> Self {
|
||||
UiInstance { inner: self.inner.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
impl UiInstanceInner {
|
||||
fn shutdown(&self) {
|
||||
if self.shutdown_started.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
if let Ok(receiver) = self.shutdown_complete.lock() {
|
||||
self.host.shutdown(&receiver);
|
||||
}
|
||||
self.queue.mark_terminated();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for UiInstanceInner {
|
||||
fn drop(&mut self) {
|
||||
self.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UiCommand {
|
||||
Input(winit::event::WindowEvent),
|
||||
Resized { width: u32, height: u32 },
|
||||
ScaleChanged(f64),
|
||||
Refresh,
|
||||
Message(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UiEvent {
|
||||
Ready,
|
||||
Frame(wgpu::Texture),
|
||||
Cursor(Cursor),
|
||||
Message(Vec<u8>),
|
||||
Failure(String),
|
||||
Crashed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Cursor {
|
||||
Icon(winit::cursor::CursorIcon),
|
||||
Custom { rgba: Vec<u8>, width: u16, height: u16, hotspot_x: u16, hotspot_y: u16 },
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<winit::cursor::CursorIcon> for Cursor {
|
||||
fn from(icon: winit::cursor::CursorIcon) -> Self {
|
||||
Cursor::Icon(icon)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user