mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-17 07:18:04 +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
252 lines
5.9 KiB
Rust
252 lines
5.9 KiB
Rust
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)
|
|
}
|
|
}
|