mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +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:
53
desktop/ui/Cargo.toml
Normal file
53
desktop/ui/Cargo.toml
Normal file
@@ -0,0 +1,53 @@
|
||||
[package]
|
||||
name = "graphite-desktop-ui"
|
||||
description = "Renders the Graphite editor frontend UI into wgpu textures"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
embedded_resources = ["dep:graphite-desktop-embedded-resources"]
|
||||
accelerated_paint = ["dep:ash", "dep:bytemuck", "dep:objc2-io-surface", "dep:objc2-metal", "dep:mach2"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
graphite-desktop-embedded-resources = { path = "../embedded-resources", optional = true }
|
||||
|
||||
wgpu = { workspace = true }
|
||||
wgpu-sync = { workspace = true }
|
||||
winit = { workspace = true, features = ["serde"] }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
rand = { workspace = true, features = ["thread_rng"] }
|
||||
cef = { workspace = true }
|
||||
bytemuck = { workspace = true, optional = true }
|
||||
ipc-channel = "0.22"
|
||||
|
||||
# Linux-specific dependencies
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc = "0.2"
|
||||
ash = { version = "0.38", optional = true }
|
||||
|
||||
# Windows-specific dependencies
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.62.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Direct3D12",
|
||||
"Win32_Security",
|
||||
"Win32_System_JobObjects",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
# Mac-specific dependencies
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
libc = "0.2"
|
||||
mach2 = { version = "0.4", optional = true }
|
||||
objc2 = { version = "0.6.1", default-features = false }
|
||||
objc2-foundation = { version = "0.3.2", default-features = false }
|
||||
objc2-app-kit = { version = "0.3.2", default-features = false }
|
||||
objc2-io-surface = { version = "0.3.2", optional = true }
|
||||
objc2-metal = { version = "0.3", features = ["objc2-io-surface"], optional = true }
|
||||
37
desktop/ui/src/consts.rs
Normal file
37
desktop/ui/src/consts.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) const RESOURCE_SCHEME: &str = "resources";
|
||||
pub(crate) const RESOURCE_DOMAIN: &str = "resources";
|
||||
|
||||
pub(crate) const BROWSER_HOST_CONFIG_FLAG: &str = "--graphite-browser-host=";
|
||||
|
||||
pub(crate) const WINDOWLESS_FRAME_RATE: i32 = 60;
|
||||
pub(crate) const FRAMES_IN_FLIGHT_LIMIT: u64 = 3;
|
||||
pub(crate) const FRAME_SEGMENT_POOL_SIZE: u64 = FRAMES_IN_FLIGHT_LIMIT + 1; // allow one extra staged frame
|
||||
pub(crate) const FRAME_SEGMENT_GRANULARITY: usize = 2 * 1024 * 1024; // 2 MiB
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) const FRAME_ACK_TIMEOUT: Duration = Duration::from_millis(250);
|
||||
|
||||
pub(crate) const HOST_HELLO_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
pub(crate) const HOST_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) const IPC_BOOTSTRAP_PREFIX: &str = "art.graphite.Graphite.ipc.";
|
||||
|
||||
pub(crate) const SCROLL_LINE_HEIGHT: usize = 40;
|
||||
pub(crate) const SCROLL_LINE_WIDTH: usize = 40;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) const SCROLL_SPEED_X: f32 = 3.;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) const SCROLL_SPEED_Y: f32 = 3.;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) const SCROLL_SPEED_X: f32 = 1.;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) const SCROLL_SPEED_Y: f32 = 1.;
|
||||
|
||||
pub(crate) const PINCH_ZOOM_SPEED: f64 = 300.;
|
||||
|
||||
pub(crate) const MULTICLICK_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
pub(crate) const MULTICLICK_ALLOWED_TRAVEL: usize = 4;
|
||||
350
desktop/ui/src/context.rs
Normal file
350
desktop/ui/src/context.rs
Normal file
@@ -0,0 +1,350 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
59
desktop/ui/src/delegate.rs
Normal file
59
desktop/ui/src/delegate.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::remote::messages::EventMessage;
|
||||
use super::view::{ViewInfo, ViewInfoReceiver, ViewInfoUpdate};
|
||||
use crate::Cursor;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct BrowserDelegate(Arc<Inner>);
|
||||
|
||||
struct Inner {
|
||||
sender: Arc<Mutex<IpcSender<EventMessage>>>,
|
||||
view_info: Mutex<ViewInfoReceiver>,
|
||||
}
|
||||
|
||||
impl BrowserDelegate {
|
||||
pub(crate) fn new(sender: Arc<Mutex<IpcSender<EventMessage>>>, view_info_receiver: Receiver<ViewInfoUpdate>) -> Self {
|
||||
Self(Arc::new(Inner {
|
||||
sender,
|
||||
view_info: Mutex::new(ViewInfoReceiver::new(view_info_receiver)),
|
||||
}))
|
||||
}
|
||||
|
||||
fn send(&self, message: EventMessage) {
|
||||
let Ok(sender) = self.0.sender.lock() else {
|
||||
tracing::error!("Failed to lock host message sender");
|
||||
return;
|
||||
};
|
||||
if let Err(e) = sender.send(message) {
|
||||
tracing::debug!("Failed to send message to main process: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn view_info(&self) -> ViewInfo {
|
||||
let Ok(mut guard) = self.0.view_info.lock() else {
|
||||
tracing::error!("Failed to lock the view info mirror");
|
||||
return ViewInfo::new();
|
||||
};
|
||||
guard.current()
|
||||
}
|
||||
|
||||
pub(crate) fn load_resource(&self, path: PathBuf) -> Option<crate::resources::Resource> {
|
||||
crate::resources::load(path)
|
||||
}
|
||||
|
||||
pub(crate) fn cursor_change(&self, cursor: Cursor) {
|
||||
self.send(EventMessage::CursorChange(cursor));
|
||||
}
|
||||
|
||||
pub(crate) fn initialized_web_communication(&self) {
|
||||
self.send(EventMessage::WebCommunicationInitialized);
|
||||
}
|
||||
|
||||
pub(crate) fn receive_web_message(&self, message: &[u8]) {
|
||||
self.send(EventMessage::WebMessage(message.to_vec()));
|
||||
}
|
||||
}
|
||||
50
desktop/ui/src/dirs.rs
Normal file
50
desktop/ui/src/dirs.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const APP_DIRECTORY_NAME: &str = "graphite";
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
const APP_DIRECTORY_NAME: &str = "Graphite";
|
||||
|
||||
pub(crate) fn app_tmp_dir() -> PathBuf {
|
||||
let path = std::env::temp_dir().join(APP_DIRECTORY_NAME);
|
||||
if let Err(e) = fs::create_dir_all(&path) {
|
||||
tracing::error!("Failed to create temp directory at {path:?}: {e}");
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
/// Temporary directory that is automatically deleted when dropped.
|
||||
pub struct TempDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TempDir {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
Self::new_with_parent(app_tmp_dir())
|
||||
}
|
||||
|
||||
pub fn new_with_parent(parent: impl AsRef<Path>) -> io::Result<Self> {
|
||||
let random_suffix = format!("{:032x}", rand::random::<u128>());
|
||||
let name = format!("{}_{}", std::process::id(), random_suffix);
|
||||
let path = parent.as_ref().join(name);
|
||||
fs::create_dir_all(&path)?;
|
||||
Ok(Self { path })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
let result = fs::remove_dir_all(&self.path);
|
||||
if let Err(e) = result {
|
||||
tracing::error!("Failed to remove temporary directory at {:?}: {}", self.path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for TempDir {
|
||||
fn as_ref(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
41
desktop/ui/src/events.rs
Normal file
41
desktop/ui/src/events.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
use crate::UiEvent;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct EventQueue {
|
||||
sender: std::sync::mpsc::Sender<UiEvent>,
|
||||
terminated: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl EventQueue {
|
||||
pub(crate) fn new() -> (Self, Receiver<UiEvent>) {
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
(
|
||||
Self {
|
||||
sender,
|
||||
terminated: Arc::new(AtomicBool::new(false)),
|
||||
},
|
||||
receiver,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn send(&self, event: UiEvent) {
|
||||
let _ = self.sender.send(event);
|
||||
}
|
||||
|
||||
pub(crate) fn terminate(&self, event: UiEvent) {
|
||||
let _ = self.sender.send(event);
|
||||
self.terminated.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub(crate) fn mark_terminated(&self) {
|
||||
self.terminated.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub(crate) fn is_terminated(&self) -> bool {
|
||||
self.terminated.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
14
desktop/ui/src/frames.rs
Normal file
14
desktop/ui/src/frames.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) mod import;
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) mod plane;
|
||||
pub(crate) mod receive;
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
mod resample;
|
||||
pub(crate) mod sequence;
|
||||
pub(crate) mod sink;
|
||||
mod streamer;
|
||||
mod surface;
|
||||
|
||||
pub(crate) use streamer::FrameStreamer;
|
||||
pub(crate) use surface::FrameSurface;
|
||||
117
desktop/ui/src/frames/import.rs
Normal file
117
desktop/ui/src/frames/import.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use cef::sys::cef_color_type_t;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) mod d3d11;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) mod dmabuf;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) mod iosurface;
|
||||
|
||||
pub(crate) type TextureImportResult = Result<wgpu::Texture, TextureImportError>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum TextureImportError {
|
||||
#[error("Invalid texture handle: {0}")]
|
||||
InvalidHandle(String),
|
||||
#[error("Unsupported texture format: {format:?}")]
|
||||
UnsupportedFormat { format: cef_color_type_t },
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[error("Hardware acceleration not available: {reason}")]
|
||||
HardwareUnavailable { reason: String },
|
||||
#[error("Vulkan operation failed: {operation}")]
|
||||
#[cfg(target_os = "linux")]
|
||||
VulkanError { operation: String },
|
||||
#[error("Platform-specific error: {message}")]
|
||||
PlatformError { message: String },
|
||||
}
|
||||
|
||||
impl From<wgpu::hal::DeviceError> for TextureImportError {
|
||||
fn from(e: wgpu::hal::DeviceError) -> Self {
|
||||
TextureImportError::PlatformError {
|
||||
message: format!("wgpu-hal DeviceError: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct ContentRect {
|
||||
pub(crate) x: u32,
|
||||
pub(crate) y: u32,
|
||||
pub(crate) width: u32,
|
||||
pub(crate) height: u32,
|
||||
pub(crate) source_width: u32,
|
||||
pub(crate) source_height: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum ContentMapping {
|
||||
Identity,
|
||||
Scaled(ContentRect),
|
||||
}
|
||||
|
||||
impl ContentRect {
|
||||
pub(crate) fn mapping(self, width: u32, height: u32) -> ContentMapping {
|
||||
let valid = self.width > 0
|
||||
&& self.height > 0
|
||||
&& self.source_width > 0
|
||||
&& self.source_height > 0
|
||||
&& self.x.checked_add(self.width).is_some_and(|right| right <= width)
|
||||
&& self.y.checked_add(self.height).is_some_and(|bottom| bottom <= height);
|
||||
let full = self.x == 0 && self.y == 0 && (self.width, self.height) == (width, height) && (self.source_width, self.source_height) == (width, height);
|
||||
if valid && !full { ContentMapping::Scaled(self) } else { ContentMapping::Identity }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&cef::AcceleratedPaintInfo> for ContentRect {
|
||||
type Error = TextureImportError;
|
||||
|
||||
fn try_from(info: &cef::AcceleratedPaintInfo) -> Result<Self, Self::Error> {
|
||||
let invalid = || TextureImportError::InvalidHandle("Failed to create content rect".into());
|
||||
let content = &info.extra.content_rect;
|
||||
let width = u32::try_from(content.width).ok().filter(|&width| width > 0).ok_or_else(invalid)?;
|
||||
let height = u32::try_from(content.height).ok().filter(|&height| height > 0).ok_or_else(invalid)?;
|
||||
let source = &info.extra.source_size;
|
||||
let (source_width, source_height) = if info.extra.has_source_size != 0 && source.width > 0 && source.height > 0 {
|
||||
(source.width as u32, source.height as u32)
|
||||
} else {
|
||||
(width, height)
|
||||
};
|
||||
Ok(Self {
|
||||
x: content.x.max(0) as u32,
|
||||
y: content.y.max(0) as u32,
|
||||
width,
|
||||
height,
|
||||
source_width,
|
||||
source_height,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait TextureImporter {
|
||||
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult;
|
||||
}
|
||||
|
||||
fn wgpu_format(format: cef_color_type_t) -> Result<wgpu::TextureFormat, TextureImportError> {
|
||||
match format {
|
||||
cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(wgpu::TextureFormat::Bgra8Unorm),
|
||||
cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(wgpu::TextureFormat::Rgba8Unorm),
|
||||
_ => Err(TextureImportError::UnsupportedFormat { format }),
|
||||
}
|
||||
}
|
||||
|
||||
fn texture_descriptor(width: u32, height: u32, format: cef_color_type_t, label: &'static str) -> Result<wgpu::TextureDescriptor<'static>, TextureImportError> {
|
||||
Ok(wgpu::TextureDescriptor {
|
||||
label: Some(label),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu_format(format)?,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
136
desktop/ui/src/frames/import/d3d11.rs
Normal file
136
desktop/ui/src/frames/import/d3d11.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use super::{TextureImportError, TextureImportResult, TextureImporter, texture_descriptor, wgpu_format};
|
||||
use cef::sys::cef_color_type_t;
|
||||
use std::os::raw::c_void;
|
||||
use wgpu::hal::api;
|
||||
|
||||
pub struct D3D11Importer {
|
||||
pub handle: *mut c_void,
|
||||
pub format: cef_color_type_t,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl TextureImporter for D3D11Importer {
|
||||
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
if self.handle.is_null() {
|
||||
return Err(TextureImportError::InvalidHandle("Null D3D11 shared texture handle".to_string()));
|
||||
}
|
||||
|
||||
let is_d3d12_backend = unsafe { device.as_hal::<api::Dx12>().is_some() };
|
||||
|
||||
if is_d3d12_backend {
|
||||
let texture = self.import_via_d3d12(device)?;
|
||||
return Ok(texture);
|
||||
}
|
||||
|
||||
let texture = self.import_via_vulkan(device)?;
|
||||
tracing::trace!("Successfully imported D3D11 shared texture via Vulkan");
|
||||
Ok(texture)
|
||||
}
|
||||
}
|
||||
|
||||
impl D3D11Importer {
|
||||
pub fn from_parts(handle: u64, width: u32, height: u32, format: cef_color_type_t) -> Self {
|
||||
Self {
|
||||
handle: handle as *mut c_void,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
fn import_via_d3d12(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
use wgpu::hal::api;
|
||||
let hal_texture = unsafe {
|
||||
let hal_device_guard = device.as_hal::<api::Dx12>();
|
||||
let Some(hal_device) = hal_device_guard else {
|
||||
return Err(TextureImportError::HardwareUnavailable {
|
||||
reason: "Device is not using D3D12 backend".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let d3d12_resource = self.import_d3d11_handle_to_d3d12(&hal_device)?;
|
||||
|
||||
let hal_texture = <api::Dx12 as wgpu::hal::Api>::Device::texture_from_raw(
|
||||
d3d12_resource,
|
||||
wgpu_format(self.format)?,
|
||||
wgpu::TextureDimension::D2,
|
||||
wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
1, // mip_level_count
|
||||
1, // sample_count
|
||||
);
|
||||
|
||||
Ok::<_, TextureImportError>(hal_texture)
|
||||
}?;
|
||||
|
||||
let texture = unsafe { device.create_texture_from_hal::<api::Dx12>(hal_texture, &texture_descriptor(self.width, self.height, self.format, "CEF D3D11→D3D12 Shared Texture")?) };
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
fn import_via_vulkan(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
use wgpu::{TextureUses, wgc::api::Vulkan};
|
||||
let hal_texture = unsafe {
|
||||
let hal_device_guard = device.as_hal::<Vulkan>();
|
||||
let Some(hal_device) = hal_device_guard else {
|
||||
return Err(TextureImportError::HardwareUnavailable {
|
||||
reason: "Device is not using Vulkan backend".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let hal_texture = <Vulkan as wgpu::hal::Api>::Device::texture_from_d3d11_shared_handle(
|
||||
&hal_device,
|
||||
windows::Win32::Foundation::HANDLE(self.handle),
|
||||
&wgpu::hal::TextureDescriptor {
|
||||
label: Some("CEF D3D11 Shared Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu_format(self.format)?,
|
||||
usage: TextureUses::COPY_DST | TextureUses::COPY_SRC | TextureUses::RESOURCE,
|
||||
memory_flags: wgpu::hal::MemoryFlags::empty(),
|
||||
view_formats: vec![],
|
||||
},
|
||||
)
|
||||
.map_err(|e| TextureImportError::PlatformError {
|
||||
message: format!("Failed to import D3D11 shared handle into Vulkan: {:?}", e),
|
||||
})?;
|
||||
|
||||
Ok::<_, TextureImportError>(hal_texture)
|
||||
}?;
|
||||
|
||||
let texture = unsafe { device.create_texture_from_hal::<Vulkan>(hal_texture, &texture_descriptor(self.width, self.height, self.format, "CEF D3D11 Shared Texture")?) };
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
fn import_d3d11_handle_to_d3d12(&self, hal_device: &<wgpu::hal::api::Dx12 as wgpu::hal::Api>::Device) -> Result<windows::Win32::Graphics::Direct3D12::ID3D12Resource, TextureImportError> {
|
||||
use windows::Win32::Graphics::Direct3D12::*;
|
||||
|
||||
let d3d12_device = hal_device.raw_device();
|
||||
|
||||
if self.width == 0 || self.height == 0 {
|
||||
return Err(TextureImportError::InvalidHandle("Invalid D3D11 texture dimensions".to_string()));
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let mut shared_resource: Option<ID3D12Resource> = None;
|
||||
d3d12_device
|
||||
.OpenSharedHandle(windows::Win32::Foundation::HANDLE(self.handle), &mut shared_resource)
|
||||
.map_err(|e| TextureImportError::PlatformError {
|
||||
message: format!("Failed to open D3D11 shared handle on D3D12: {:?}", e),
|
||||
})?;
|
||||
|
||||
shared_resource.ok_or_else(|| TextureImportError::InvalidHandle("Failed to get D3D12 resource from shared handle".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
226
desktop/ui/src/frames/import/dmabuf.rs
Normal file
226
desktop/ui/src/frames/import/dmabuf.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use super::{TextureImportError, TextureImportResult, TextureImporter, texture_descriptor, wgpu_format};
|
||||
use ash::vk;
|
||||
use cef::sys::cef_color_type_t;
|
||||
use wgpu::hal::api;
|
||||
|
||||
pub struct DmaBufImporter {
|
||||
fds: Vec<std::os::fd::OwnedFd>,
|
||||
format: cef_color_type_t,
|
||||
modifier: u64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
strides: Vec<u32>,
|
||||
offsets: Vec<u32>,
|
||||
}
|
||||
|
||||
impl TextureImporter for DmaBufImporter {
|
||||
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
if self.fds.len() != 1 {
|
||||
return Err(TextureImportError::InvalidHandle(format!("Expected exactly one DMA-BUF plane fd, got {}", self.fds.len())));
|
||||
}
|
||||
|
||||
if self.strides.len() != self.fds.len() || self.offsets.len() != self.fds.len() {
|
||||
return Err(TextureImportError::InvalidHandle(format!(
|
||||
"DMA-BUF plane count mismatch: {} fds, {} strides, {} offsets",
|
||||
self.fds.len(),
|
||||
self.strides.len(),
|
||||
self.offsets.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let texture = self.import_via_vulkan(device)?;
|
||||
tracing::trace!("Successfully imported DMA-BUF texture via Vulkan");
|
||||
Ok(texture)
|
||||
}
|
||||
}
|
||||
|
||||
impl DmaBufImporter {
|
||||
pub fn from_parts(fds: Vec<std::os::fd::OwnedFd>, strides: Vec<u32>, offsets: Vec<u32>, modifier: u64, width: u32, height: u32, format: cef_color_type_t) -> Self {
|
||||
Self {
|
||||
fds,
|
||||
format,
|
||||
modifier,
|
||||
width,
|
||||
height,
|
||||
strides,
|
||||
offsets,
|
||||
}
|
||||
}
|
||||
|
||||
fn import_via_vulkan(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
use wgpu::{TextureUses, wgc::api::Vulkan};
|
||||
let hal_texture = unsafe {
|
||||
let hal_device_guard = device.as_hal::<api::Vulkan>();
|
||||
let Some(hal_device) = hal_device_guard else {
|
||||
return Err(TextureImportError::HardwareUnavailable {
|
||||
reason: "Device is not using Vulkan backend".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let (vk_image, device_memory) = self.create_vulkan_image_from_dmabuf(&hal_device)?;
|
||||
|
||||
let hal_texture = <api::Vulkan as wgpu::hal::Api>::Device::texture_from_raw(
|
||||
&hal_device,
|
||||
vk_image,
|
||||
&wgpu::hal::TextureDescriptor {
|
||||
label: Some("CEF DMA-BUF Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu_format(self.format)?,
|
||||
usage: TextureUses::COPY_DST | TextureUses::COPY_SRC | TextureUses::RESOURCE,
|
||||
memory_flags: wgpu::hal::MemoryFlags::empty(),
|
||||
view_formats: vec![],
|
||||
},
|
||||
None,
|
||||
wgpu::hal::vulkan::TextureMemory::Dedicated(device_memory),
|
||||
);
|
||||
|
||||
Ok::<_, TextureImportError>(hal_texture)
|
||||
}?;
|
||||
|
||||
let texture = unsafe { device.create_texture_from_hal::<Vulkan>(hal_texture, &texture_descriptor(self.width, self.height, self.format, "CEF DMA-BUF Texture")?) };
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
fn create_vulkan_image_from_dmabuf(&self, hal_device: &<api::Vulkan as wgpu::hal::Api>::Device) -> Result<(vk::Image, vk::DeviceMemory), TextureImportError> {
|
||||
let device = hal_device.raw_device();
|
||||
let instance = hal_device.shared_instance().raw_instance();
|
||||
|
||||
if self.width == 0 || self.height == 0 {
|
||||
return Err(TextureImportError::InvalidHandle("Invalid DMA-BUF dimensions".to_string()));
|
||||
}
|
||||
|
||||
let image_create_info = vk::ImageCreateInfo::default()
|
||||
.image_type(vk::ImageType::TYPE_2D)
|
||||
.format(vulkan_format(self.format)?)
|
||||
.extent(vk::Extent3D {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth: 1,
|
||||
})
|
||||
.mip_levels(1)
|
||||
.array_layers(1)
|
||||
.samples(vk::SampleCountFlags::TYPE_1)
|
||||
.tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT)
|
||||
.usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_SRC)
|
||||
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
||||
|
||||
let plane_layouts = self
|
||||
.offsets
|
||||
.iter()
|
||||
.zip(&self.strides)
|
||||
.map(|(&offset, &stride)| vk::SubresourceLayout {
|
||||
offset: offset as u64,
|
||||
size: 0, // Will be calculated by driver
|
||||
row_pitch: stride as u64,
|
||||
array_pitch: 0,
|
||||
depth_pitch: 0,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut drm_format_modifier = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default()
|
||||
.drm_format_modifier(self.modifier)
|
||||
.plane_layouts(&plane_layouts);
|
||||
|
||||
let mut external_memory_info = vk::ExternalMemoryImageCreateInfo::default().handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT);
|
||||
|
||||
let image_create_info = image_create_info.push_next(&mut drm_format_modifier).push_next(&mut external_memory_info);
|
||||
|
||||
let image = unsafe {
|
||||
device.create_image(&image_create_info, None).map_err(|e| TextureImportError::VulkanError {
|
||||
operation: format!("Failed to create Vulkan image: {e:?}"),
|
||||
})?
|
||||
};
|
||||
|
||||
let memory_requirements = unsafe { device.get_image_memory_requirements(image) };
|
||||
|
||||
// Duplicate the file descriptor
|
||||
let dup_fd = unsafe { libc::dup(std::os::fd::AsRawFd::as_raw_fd(&self.fds[0])) };
|
||||
if dup_fd == -1 {
|
||||
// SAFETY: the image was created above and never bound or returned.
|
||||
unsafe { device.destroy_image(image, None) };
|
||||
return Err(TextureImportError::PlatformError {
|
||||
message: "Failed to duplicate DMA-BUF file descriptor".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let external_memory_fd = ash::khr::external_memory_fd::Device::new(instance, device);
|
||||
let mut fd_properties = vk::MemoryFdPropertiesKHR::default();
|
||||
if let Err(e) = unsafe { external_memory_fd.get_memory_fd_properties(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT, dup_fd, &mut fd_properties) } {
|
||||
// SAFETY: import failed and the fd is still ours, need to clean up the image and close the fd
|
||||
unsafe {
|
||||
device.destroy_image(image, None);
|
||||
libc::close(dup_fd);
|
||||
}
|
||||
return Err(TextureImportError::VulkanError {
|
||||
operation: format!("Failed to query DMA-BUF fd memory properties: {e:?}"),
|
||||
});
|
||||
}
|
||||
|
||||
let mut import_memory_fd = vk::ImportMemoryFdInfoKHR::default().handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT).fd(dup_fd);
|
||||
|
||||
let memory_properties = unsafe { instance.get_physical_device_memory_properties(hal_device.raw_physical_device()) };
|
||||
|
||||
let compatible_type_bits = memory_requirements.memory_type_bits & fd_properties.memory_type_bits;
|
||||
let Some(memory_type_index) = find_memory_type_index(compatible_type_bits, vk::MemoryPropertyFlags::empty(), &memory_properties) else {
|
||||
// SAFETY: import failed and the fd is still ours, need to clean up the image and close the fd
|
||||
unsafe {
|
||||
device.destroy_image(image, None);
|
||||
libc::close(dup_fd);
|
||||
}
|
||||
return Err(TextureImportError::VulkanError {
|
||||
operation: "Failed to find suitable memory type for DMA-BUF".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let allocate_info = vk::MemoryAllocateInfo::default()
|
||||
.allocation_size(memory_requirements.size)
|
||||
.memory_type_index(memory_type_index)
|
||||
.push_next(&mut import_memory_fd);
|
||||
|
||||
let device_memory = match unsafe { device.allocate_memory(&allocate_info, None) } {
|
||||
Ok(memory) => memory,
|
||||
Err(e) => {
|
||||
// SAFETY: import failed and the fd is still ours, need to clean up the image and close the fd
|
||||
unsafe {
|
||||
device.destroy_image(image, None);
|
||||
libc::close(dup_fd);
|
||||
}
|
||||
return Err(TextureImportError::VulkanError {
|
||||
operation: format!("Failed to allocate memory for DMA-BUF: {e:?}"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = unsafe { device.bind_image_memory(image, device_memory, 0) } {
|
||||
// SAFETY: import failed, need to clean up the image and free the memory
|
||||
unsafe {
|
||||
device.destroy_image(image, None);
|
||||
device.free_memory(device_memory, None);
|
||||
}
|
||||
return Err(TextureImportError::VulkanError {
|
||||
operation: format!("Failed to bind memory to image: {e:?}"),
|
||||
});
|
||||
}
|
||||
|
||||
Ok((image, device_memory))
|
||||
}
|
||||
}
|
||||
|
||||
fn vulkan_format(format: cef_color_type_t) -> Result<vk::Format, TextureImportError> {
|
||||
match format {
|
||||
cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(vk::Format::B8G8R8A8_UNORM),
|
||||
cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(vk::Format::R8G8B8A8_UNORM),
|
||||
_ => Err(TextureImportError::UnsupportedFormat { format }),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_memory_type_index(type_filter: u32, properties: vk::MemoryPropertyFlags, mem_properties: &vk::PhysicalDeviceMemoryProperties) -> Option<u32> {
|
||||
(0..mem_properties.memory_type_count).find(|&i| (type_filter & (1 << i)) != 0 && mem_properties.memory_types[i as usize].property_flags.contains(properties))
|
||||
}
|
||||
93
desktop/ui/src/frames/import/iosurface.rs
Normal file
93
desktop/ui/src/frames/import/iosurface.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use super::{TextureImportError, TextureImportResult, TextureImporter, texture_descriptor};
|
||||
use cef::sys::cef_color_type_t;
|
||||
use objc2::rc::Retained;
|
||||
use objc2_io_surface::IOSurfaceRef;
|
||||
use objc2_metal::{MTLDevice, MTLPixelFormat, MTLStorageMode, MTLTextureDescriptor, MTLTextureType, MTLTextureUsage};
|
||||
use wgpu::TextureDescriptor;
|
||||
|
||||
use std::os::raw::c_void;
|
||||
|
||||
pub struct IOSurfaceImporter {
|
||||
pub handle: *mut c_void,
|
||||
pub format: cef_color_type_t,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl TextureImporter for IOSurfaceImporter {
|
||||
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
let texture = self.import_via_metal(device)?;
|
||||
tracing::trace!("Successfully imported IOSurface texture via Metal");
|
||||
Ok(texture)
|
||||
}
|
||||
}
|
||||
|
||||
impl IOSurfaceImporter {
|
||||
pub fn from_parts(handle: *mut c_void, width: u32, height: u32, format: cef_color_type_t) -> Self {
|
||||
Self { handle, format, width, height }
|
||||
}
|
||||
|
||||
fn get_metal_desc(&self, texture_desc: &TextureDescriptor) -> Result<Retained<MTLTextureDescriptor>, TextureImportError> {
|
||||
if self.width == 0 || self.height == 0 {
|
||||
return Err(TextureImportError::InvalidHandle("Invalid IOSurface texture dimensions".to_string()));
|
||||
}
|
||||
|
||||
let metal_desc = MTLTextureDescriptor::new();
|
||||
unsafe {
|
||||
metal_desc.setWidth(texture_desc.size.width as _);
|
||||
metal_desc.setHeight(texture_desc.size.height as _);
|
||||
metal_desc.setArrayLength(texture_desc.array_layer_count() as _);
|
||||
metal_desc.setMipmapLevelCount(texture_desc.mip_level_count as _);
|
||||
metal_desc.setSampleCount(texture_desc.sample_count as _);
|
||||
metal_desc.setTextureType(MTLTextureType::Type2D);
|
||||
metal_desc.setPixelFormat(match texture_desc.format {
|
||||
wgpu::TextureFormat::Rgba8Unorm => MTLPixelFormat::RGBA8Unorm,
|
||||
wgpu::TextureFormat::Bgra8Unorm => MTLPixelFormat::BGRA8Unorm,
|
||||
_ => unimplemented!(),
|
||||
});
|
||||
metal_desc.setUsage(MTLTextureUsage::ShaderRead);
|
||||
metal_desc.setStorageMode(MTLStorageMode::Managed);
|
||||
}
|
||||
|
||||
Ok(metal_desc)
|
||||
}
|
||||
|
||||
fn import_via_metal(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
let io_surface = std::ptr::NonNull::new(self.handle.cast::<IOSurfaceRef>()).ok_or(TextureImportError::InvalidHandle("Invalid IOSurface handle".to_string()))?;
|
||||
|
||||
let texture_desc = texture_descriptor(self.width, self.height, self.format, "Cef Texture")?;
|
||||
let hal_tex = {
|
||||
let metal_desc = self.get_metal_desc(&texture_desc)?;
|
||||
|
||||
let texture = unsafe {
|
||||
let hal_device_guard = device.as_hal::<wgpu::wgc::api::Metal>();
|
||||
let Some(hal_device) = hal_device_guard else {
|
||||
return Err(TextureImportError::InvalidHandle("Failed to get Metal device from wgpu".to_string()));
|
||||
};
|
||||
|
||||
let texture = hal_device
|
||||
.raw_device()
|
||||
.newTextureWithDescriptor_iosurface_plane(metal_desc.as_ref(), io_surface.as_ref(), 0)
|
||||
.ok_or(TextureImportError::InvalidHandle("Invalid IOSurface handle".to_string()))?;
|
||||
|
||||
let hal_tex = <wgpu::wgc::api::Metal as wgpu::hal::Api>::Device::texture_from_raw(
|
||||
texture,
|
||||
texture_desc.format,
|
||||
MTLTextureType::Type2D,
|
||||
texture_desc.array_layer_count(),
|
||||
texture_desc.mip_level_count,
|
||||
wgpu::hal::CopyExtent {
|
||||
width: texture_desc.size.width,
|
||||
height: texture_desc.size.height,
|
||||
depth: texture_desc.array_layer_count(),
|
||||
},
|
||||
);
|
||||
|
||||
Ok::<_, TextureImportError>(hal_tex)
|
||||
}?;
|
||||
texture
|
||||
};
|
||||
|
||||
Ok(unsafe { device.create_texture_from_hal::<wgpu::wgc::api::Metal>(hal_tex, &texture_desc) })
|
||||
}
|
||||
}
|
||||
35
desktop/ui/src/frames/plane.rs
Normal file
35
desktop/ui/src/frames/plane.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) use linux::*;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) use win::*;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) use mac::*;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub(crate) enum RecvResult {
|
||||
Frame(WireFrame),
|
||||
WouldBlock,
|
||||
#[cfg_attr(target_os = "macos", allow(dead_code))]
|
||||
Closed,
|
||||
}
|
||||
|
||||
/// Decode the wire representation of `cef_color_type_t` (its `u32` discriminant),
|
||||
/// logging unknown discriminants.
|
||||
fn wire_color_type(format: u32) -> Option<cef::sys::cef_color_type_t> {
|
||||
match format {
|
||||
0 => Some(cef::sys::cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888),
|
||||
1 => Some(cef::sys::cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888),
|
||||
_ => {
|
||||
tracing::error!("Unknown color type {format} in accelerated frame");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
265
desktop/ui/src/frames/plane/linux.rs
Normal file
265
desktop/ui/src/frames/plane/linux.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use std::os::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::RecvResult;
|
||||
use crate::frames::surface::FrameSurface;
|
||||
use crate::remote::HostConfig;
|
||||
use crate::remote::messages::EventMessage;
|
||||
pub(crate) const FRAME_SOCKET_CHILD_FD: RawFd = 3;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct FrameDescriptor {
|
||||
seq: u64,
|
||||
modifier: u64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: u32,
|
||||
plane_count: u32,
|
||||
strides: [u32; 4],
|
||||
offsets: [u32; 4],
|
||||
content_x: u32,
|
||||
content_y: u32,
|
||||
content_width: u32,
|
||||
content_height: u32,
|
||||
source_width: u32,
|
||||
source_height: u32,
|
||||
}
|
||||
|
||||
const DESCRIPTOR_BYTES: usize = std::mem::size_of::<FrameDescriptor>();
|
||||
const MAX_PLANES: usize = 4;
|
||||
|
||||
pub(crate) fn socketpair() -> std::io::Result<(OwnedFd, OwnedFd)> {
|
||||
let mut fds = [0 as RawFd; 2];
|
||||
// SAFETY: socketpair call; on success fds are owned by us.
|
||||
let result = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_SEQPACKET | libc::SOCK_CLOEXEC, 0, fds.as_mut_ptr()) };
|
||||
if result != 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
// SAFETY: socketpair succeeded, so both fds are valid and not owned elsewhere.
|
||||
Ok(unsafe { (OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])) })
|
||||
}
|
||||
|
||||
pub(crate) struct PlaneSender {
|
||||
socket: OwnedFd,
|
||||
}
|
||||
|
||||
impl PlaneSender {
|
||||
pub(crate) fn from_config(config: &HostConfig, _events: Arc<Mutex<IpcSender<EventMessage>>>) -> Option<Self> {
|
||||
let fd = config.frame_socket_fd?;
|
||||
// SAFETY: the spawner dup2'd this fd for us; nothing else owns it.
|
||||
let socket = unsafe { OwnedFd::from_raw_fd(fd) };
|
||||
|
||||
// Restore CLOEXEC so subprocesses don't inherit the socket.
|
||||
// SAFETY: plain fcntl on an fd we own.
|
||||
if unsafe { libc::fcntl(socket.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC) } != 0 {
|
||||
tracing::warn!("Failed to set CLOEXEC on the frame socket: {}", std::io::Error::last_os_error());
|
||||
}
|
||||
Some(Self { socket })
|
||||
}
|
||||
|
||||
pub(crate) fn stage(&self, info: &cef::AcceleratedPaintInfo) -> Option<StagedFrame> {
|
||||
let coded_size = &info.extra.coded_size;
|
||||
if coded_size.width <= 0 || coded_size.height <= 0 {
|
||||
tracing::error!("Accelerated paint delivered an invalid coded size: {}x{}", coded_size.width, coded_size.height);
|
||||
return None;
|
||||
}
|
||||
|
||||
let plane_count = (info.plane_count.max(0) as usize).min(info.planes.len());
|
||||
let mut fds = Vec::with_capacity(plane_count);
|
||||
let mut strides = [0u32; 4];
|
||||
let mut offsets = [0u32; 4];
|
||||
for (i, plane) in info.planes[..plane_count].iter().enumerate() {
|
||||
// SAFETY: CEF keeps the plane fds valid for the `on_accelerated_paint` callback.
|
||||
let fd = unsafe { BorrowedFd::borrow_raw(plane.fd) };
|
||||
match fd.try_clone_to_owned() {
|
||||
Ok(owned) => fds.push(owned),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to duplicate DMA-BUF plane fd: {e}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
strides[i] = plane.stride;
|
||||
offsets[i] = plane.offset as u32;
|
||||
}
|
||||
|
||||
let content = crate::frames::import::ContentRect::try_from(info).unwrap_or_default();
|
||||
Some(StagedFrame {
|
||||
descriptor: FrameDescriptor {
|
||||
seq: 0,
|
||||
modifier: info.modifier,
|
||||
width: coded_size.width as u32,
|
||||
height: coded_size.height as u32,
|
||||
format: *info.format.as_ref() as u32,
|
||||
plane_count: plane_count as u32,
|
||||
strides,
|
||||
offsets,
|
||||
content_x: content.x,
|
||||
content_y: content.y,
|
||||
content_width: content.width,
|
||||
content_height: content.height,
|
||||
source_width: content.source_width,
|
||||
source_height: content.source_height,
|
||||
},
|
||||
fds,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn send(&self, seq: u64, frame: StagedFrame) -> std::io::Result<()> {
|
||||
let mut descriptor = frame.descriptor;
|
||||
descriptor.seq = seq;
|
||||
|
||||
debug_assert!(frame.fds.len() <= MAX_PLANES);
|
||||
let fd_bytes = frame.fds.len() * std::mem::size_of::<RawFd>();
|
||||
let mut iov = libc::iovec {
|
||||
iov_base: &descriptor as *const FrameDescriptor as *mut libc::c_void,
|
||||
iov_len: DESCRIPTOR_BYTES,
|
||||
};
|
||||
let mut cmsg_buffer = [0u8; unsafe { libc::CMSG_SPACE((MAX_PLANES * std::mem::size_of::<RawFd>()) as u32) } as usize];
|
||||
let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
|
||||
msg.msg_iov = &mut iov;
|
||||
msg.msg_iovlen = 1;
|
||||
msg.msg_control = cmsg_buffer.as_mut_ptr().cast();
|
||||
msg.msg_controllen = unsafe { libc::CMSG_SPACE(fd_bytes as u32) } as _;
|
||||
unsafe {
|
||||
let cmsg = libc::CMSG_FIRSTHDR(&msg);
|
||||
(*cmsg).cmsg_level = libc::SOL_SOCKET;
|
||||
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
|
||||
(*cmsg).cmsg_len = libc::CMSG_LEN(fd_bytes as u32) as _;
|
||||
let data = libc::CMSG_DATA(cmsg) as *mut RawFd;
|
||||
for (i, fd) in frame.fds.iter().enumerate() {
|
||||
data.add(i).write_unaligned(fd.as_raw_fd());
|
||||
}
|
||||
}
|
||||
loop {
|
||||
// SAFETY: msg and everything it points to are valid for the duration of the call.
|
||||
let sent = unsafe { libc::sendmsg(self.socket.as_raw_fd(), &msg, libc::MSG_NOSIGNAL) };
|
||||
if sent >= 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let error = std::io::Error::last_os_error();
|
||||
if error.kind() != std::io::ErrorKind::Interrupted {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct StagedFrame {
|
||||
descriptor: FrameDescriptor,
|
||||
fds: Vec<OwnedFd>,
|
||||
}
|
||||
|
||||
pub(crate) struct PlaneReceiver {
|
||||
socket: OwnedFd,
|
||||
}
|
||||
|
||||
impl PlaneReceiver {
|
||||
pub(crate) fn new(socket: OwnedFd) -> Self {
|
||||
Self { socket }
|
||||
}
|
||||
|
||||
pub(crate) fn recv_blocking(&self) -> std::io::Result<RecvResult> {
|
||||
self.recv(false)
|
||||
}
|
||||
|
||||
pub(crate) fn try_recv(&self) -> std::io::Result<RecvResult> {
|
||||
self.recv(true)
|
||||
}
|
||||
|
||||
fn recv(&self, nonblocking: bool) -> std::io::Result<RecvResult> {
|
||||
let mut descriptor: FrameDescriptor = bytemuck::Zeroable::zeroed();
|
||||
let mut iov = libc::iovec {
|
||||
iov_base: (&mut descriptor as *mut FrameDescriptor).cast(),
|
||||
iov_len: DESCRIPTOR_BYTES,
|
||||
};
|
||||
// SAFETY: pure size computation.
|
||||
let mut cmsg_buffer = [0u8; unsafe { libc::CMSG_SPACE((MAX_PLANES * std::mem::size_of::<RawFd>()) as u32) } as usize];
|
||||
|
||||
let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
|
||||
msg.msg_iov = &mut iov;
|
||||
msg.msg_iovlen = 1;
|
||||
msg.msg_control = cmsg_buffer.as_mut_ptr().cast();
|
||||
msg.msg_controllen = cmsg_buffer.len() as _;
|
||||
|
||||
let flags = libc::MSG_CMSG_CLOEXEC | if nonblocking { libc::MSG_DONTWAIT } else { 0 };
|
||||
let received = loop {
|
||||
// SAFETY: msg and everything it points to are valid for the duration of the call.
|
||||
let received = unsafe { libc::recvmsg(self.socket.as_raw_fd(), &mut msg, flags) };
|
||||
if received >= 0 {
|
||||
break received;
|
||||
}
|
||||
let error = std::io::Error::last_os_error();
|
||||
match error.kind() {
|
||||
std::io::ErrorKind::Interrupted => continue,
|
||||
std::io::ErrorKind::WouldBlock => return Ok(RecvResult::WouldBlock),
|
||||
_ => return Err(error),
|
||||
}
|
||||
};
|
||||
|
||||
let mut fds = Vec::new();
|
||||
// SAFETY: traversing the cmsgs recvmsg just filled; SCM_RIGHTS payload is fds now owned by us.
|
||||
unsafe {
|
||||
let mut cmsg = libc::CMSG_FIRSTHDR(&msg);
|
||||
while !cmsg.is_null() {
|
||||
if (*cmsg).cmsg_level == libc::SOL_SOCKET && (*cmsg).cmsg_type == libc::SCM_RIGHTS {
|
||||
let data = libc::CMSG_DATA(cmsg) as *const RawFd;
|
||||
let count = ((*cmsg).cmsg_len as usize - libc::CMSG_LEN(0) as usize) / std::mem::size_of::<RawFd>();
|
||||
for i in 0..count {
|
||||
fds.push(OwnedFd::from_raw_fd(data.add(i).read_unaligned()));
|
||||
}
|
||||
}
|
||||
cmsg = libc::CMSG_NXTHDR(&msg, cmsg);
|
||||
}
|
||||
}
|
||||
|
||||
if received == 0 {
|
||||
return Ok(RecvResult::Closed);
|
||||
}
|
||||
if received as usize != DESCRIPTOR_BYTES || (msg.msg_flags & libc::MSG_CTRUNC) != 0 {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"malformed frame message: {received} bytes, flags {:#x} ({} fds)",
|
||||
msg.msg_flags,
|
||||
fds.len()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(RecvResult::Frame(WireFrame { descriptor, fds }))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WireFrame {
|
||||
descriptor: FrameDescriptor,
|
||||
fds: Vec<OwnedFd>,
|
||||
}
|
||||
|
||||
impl WireFrame {
|
||||
pub(crate) fn seq(&self) -> u64 {
|
||||
self.descriptor.seq
|
||||
}
|
||||
|
||||
pub(crate) fn import(self, surface: &FrameSurface) -> Option<wgpu::Texture> {
|
||||
let descriptor = self.descriptor;
|
||||
let format = super::wire_color_type(descriptor.format)?;
|
||||
let plane_count = (descriptor.plane_count as usize).min(self.fds.len());
|
||||
let content = crate::frames::import::ContentRect {
|
||||
x: descriptor.content_x,
|
||||
y: descriptor.content_y,
|
||||
width: descriptor.content_width,
|
||||
height: descriptor.content_height,
|
||||
source_width: descriptor.source_width,
|
||||
source_height: descriptor.source_height,
|
||||
};
|
||||
let importer = crate::frames::import::dmabuf::DmaBufImporter::from_parts(
|
||||
self.fds,
|
||||
descriptor.strides[..plane_count].to_vec(),
|
||||
descriptor.offsets[..plane_count].to_vec(),
|
||||
descriptor.modifier,
|
||||
descriptor.width,
|
||||
descriptor.height,
|
||||
format,
|
||||
);
|
||||
surface.import_texture(importer, content)
|
||||
}
|
||||
}
|
||||
305
desktop/ui/src/frames/plane/mac.rs
Normal file
305
desktop/ui/src/frames/plane/mac.rs
Normal file
@@ -0,0 +1,305 @@
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use std::ffi::CString;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use mach2::kern_return::KERN_SUCCESS;
|
||||
use mach2::message::{
|
||||
MACH_MSG_PORT_DESCRIPTOR, MACH_MSG_SUCCESS, MACH_MSG_TIMEOUT_NONE, MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MOVE_SEND, MACH_MSGH_BITS_COMPLEX, MACH_RCV_MSG, MACH_RCV_TIMED_OUT, MACH_RCV_TIMEOUT,
|
||||
MACH_SEND_MSG, mach_msg, mach_msg_body_t, mach_msg_header_t,
|
||||
};
|
||||
use mach2::port::{MACH_PORT_NULL, mach_port_t};
|
||||
use mach2::traps::mach_task_self;
|
||||
use objc2_io_surface::IOSurfaceRef;
|
||||
|
||||
use super::RecvResult;
|
||||
use crate::frames::surface::FrameSurface;
|
||||
use crate::remote::HostConfig;
|
||||
use crate::remote::messages::EventMessage;
|
||||
|
||||
// From libSystem, stable since 10.0, not coverd by mach2
|
||||
unsafe extern "C" {
|
||||
static bootstrap_port: mach_port_t;
|
||||
fn bootstrap_check_in(bp: mach_port_t, service_name: *const std::ffi::c_char, sp: *mut mach_port_t) -> mach2::kern_return::kern_return_t;
|
||||
fn bootstrap_look_up(bp: mach_port_t, service_name: *const std::ffi::c_char, sp: *mut mach_port_t) -> mach2::kern_return::kern_return_t;
|
||||
}
|
||||
|
||||
// `mach_msg_port_descriptor_t` kernel ABI
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct PortDescriptor {
|
||||
name: mach_port_t,
|
||||
pad1: u32,
|
||||
pad2: u16,
|
||||
disposition: u8,
|
||||
descriptor_type: u8,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct FrameDescriptor {
|
||||
seq: u64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: u32,
|
||||
content_x: u32,
|
||||
content_y: u32,
|
||||
content_width: u32,
|
||||
content_height: u32,
|
||||
source_width: u32,
|
||||
source_height: u32,
|
||||
_pad: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct FrameMessage {
|
||||
header: mach_msg_header_t,
|
||||
body: mach_msg_body_t,
|
||||
surface: PortDescriptor,
|
||||
descriptor: FrameDescriptor,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct FrameMessageBuffer {
|
||||
message: FrameMessage,
|
||||
trailer: [u8; 64],
|
||||
}
|
||||
|
||||
struct SendRight(mach_port_t);
|
||||
|
||||
// SAFETY: mach port names are task-wide; rights may be used from any thread.
|
||||
unsafe impl Send for SendRight {}
|
||||
unsafe impl Sync for SendRight {}
|
||||
|
||||
impl Drop for SendRight {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: we own one reference on this send right.
|
||||
unsafe { mach2::mach_port::mach_port_deallocate(mach_task_self(), self.0) };
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_service(name: &str) -> std::io::Result<mach_port_t> {
|
||||
let c_name = CString::new(name).map_err(std::io::Error::other)?;
|
||||
let mut port: mach_port_t = MACH_PORT_NULL;
|
||||
// SAFETY: plain bootstrap call; on success we own the service's receive right.
|
||||
let result = unsafe { bootstrap_check_in(bootstrap_port, c_name.as_ptr(), &mut port) };
|
||||
if result != KERN_SUCCESS {
|
||||
return Err(std::io::Error::other(format!("bootstrap_check_in failed: {result:#x}")));
|
||||
}
|
||||
Ok(port)
|
||||
}
|
||||
|
||||
fn look_up_service(name: &str) -> std::io::Result<SendRight> {
|
||||
let c_name = CString::new(name).map_err(std::io::Error::other)?;
|
||||
let mut port: mach_port_t = MACH_PORT_NULL;
|
||||
// SAFETY: plain bootstrap call; on success we own a send right.
|
||||
let result = unsafe { bootstrap_look_up(bootstrap_port, c_name.as_ptr(), &mut port) };
|
||||
if result != KERN_SUCCESS {
|
||||
return Err(std::io::Error::other(format!("bootstrap_look_up failed: {result:#x}")));
|
||||
}
|
||||
Ok(SendRight(port))
|
||||
}
|
||||
|
||||
pub(crate) struct PlaneSender {
|
||||
service: SendRight,
|
||||
}
|
||||
|
||||
impl PlaneSender {
|
||||
pub(crate) fn from_config(config: &HostConfig, _events: Arc<Mutex<IpcSender<EventMessage>>>) -> Option<Self> {
|
||||
let name = config.frame_service.as_deref()?;
|
||||
match look_up_service(name) {
|
||||
Ok(service) => Some(Self { service }),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to look up the accelerated frame service, falling back to software frames: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn stage(&self, info: &cef::AcceleratedPaintInfo) -> Option<StagedFrame> {
|
||||
let coded_size = &info.extra.coded_size;
|
||||
if coded_size.width <= 0 || coded_size.height <= 0 {
|
||||
tracing::error!("Accelerated paint delivered an invalid coded size: {}x{}", coded_size.width, coded_size.height);
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(surface) = std::ptr::NonNull::new(info.shared_texture_io_surface.cast::<IOSurfaceRef>()) else {
|
||||
tracing::error!("Accelerated paint delivered a null IOSurface");
|
||||
return None;
|
||||
};
|
||||
|
||||
// SAFETY: CEF keeps the surface valid for the `on_accelerated_paint` callback.
|
||||
let port = unsafe { surface.as_ref() }.create_mach_port();
|
||||
if port == MACH_PORT_NULL {
|
||||
tracing::error!("Failed to wrap the IOSurface in a mach port");
|
||||
return None;
|
||||
}
|
||||
|
||||
let content = crate::frames::import::ContentRect::try_from(info).unwrap_or_default();
|
||||
Some(StagedFrame {
|
||||
descriptor: FrameDescriptor {
|
||||
seq: 0,
|
||||
width: coded_size.width as u32,
|
||||
height: coded_size.height as u32,
|
||||
format: *info.format.as_ref() as u32,
|
||||
content_x: content.x,
|
||||
content_y: content.y,
|
||||
content_width: content.width,
|
||||
content_height: content.height,
|
||||
source_width: content.source_width,
|
||||
source_height: content.source_height,
|
||||
_pad: 0,
|
||||
},
|
||||
surface: SendRight(port),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn send(&self, seq: u64, frame: StagedFrame) -> std::io::Result<()> {
|
||||
let mut descriptor = frame.descriptor;
|
||||
descriptor.seq = seq;
|
||||
let mut message = FrameMessage {
|
||||
header: mach_msg_header_t {
|
||||
msgh_bits: MACH_MSG_TYPE_COPY_SEND | MACH_MSGH_BITS_COMPLEX,
|
||||
msgh_size: std::mem::size_of::<FrameMessage>() as u32,
|
||||
msgh_remote_port: self.service.0,
|
||||
msgh_local_port: MACH_PORT_NULL,
|
||||
msgh_voucher_port: MACH_PORT_NULL,
|
||||
msgh_id: 0,
|
||||
},
|
||||
body: mach_msg_body_t { msgh_descriptor_count: 1 },
|
||||
surface: PortDescriptor {
|
||||
name: frame.surface.0,
|
||||
pad1: 0,
|
||||
pad2: 0,
|
||||
disposition: MACH_MSG_TYPE_MOVE_SEND as u8,
|
||||
descriptor_type: MACH_MSG_PORT_DESCRIPTOR as u8,
|
||||
},
|
||||
descriptor,
|
||||
};
|
||||
|
||||
// SAFETY: message is a well-formed complex message of the declared size.
|
||||
let result = unsafe {
|
||||
mach_msg(
|
||||
&mut message.header,
|
||||
MACH_SEND_MSG,
|
||||
std::mem::size_of::<FrameMessage>() as u32,
|
||||
0,
|
||||
MACH_PORT_NULL,
|
||||
MACH_MSG_TIMEOUT_NONE,
|
||||
MACH_PORT_NULL,
|
||||
)
|
||||
};
|
||||
if result != MACH_MSG_SUCCESS {
|
||||
return Err(std::io::Error::other(format!("mach_msg send failed: {result:#x}")));
|
||||
}
|
||||
|
||||
// Kernel took ownership of the surface and moves it to the receiver. We must not drop.
|
||||
std::mem::forget(frame.surface);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct StagedFrame {
|
||||
descriptor: FrameDescriptor,
|
||||
surface: SendRight,
|
||||
}
|
||||
|
||||
pub(crate) struct PlaneReceiver {
|
||||
port: mach_port_t,
|
||||
}
|
||||
|
||||
impl PlaneReceiver {
|
||||
pub(crate) fn new(port: mach_port_t) -> Self {
|
||||
Self { port }
|
||||
}
|
||||
|
||||
pub(crate) fn recv_blocking(&self) -> std::io::Result<RecvResult> {
|
||||
self.recv(false)
|
||||
}
|
||||
|
||||
pub(crate) fn try_recv(&self) -> std::io::Result<RecvResult> {
|
||||
self.recv(true)
|
||||
}
|
||||
|
||||
fn recv(&self, nonblocking: bool) -> std::io::Result<RecvResult> {
|
||||
// SAFETY: zeroed is a valid representation for these plain-data structs.
|
||||
let mut buffer: FrameMessageBuffer = unsafe { std::mem::zeroed() };
|
||||
let (options, timeout) = if nonblocking {
|
||||
(MACH_RCV_MSG | MACH_RCV_TIMEOUT, 0)
|
||||
} else {
|
||||
(MACH_RCV_MSG, MACH_MSG_TIMEOUT_NONE)
|
||||
};
|
||||
|
||||
// SAFETY: the buffer is large enough for the message plus the basic trailer.
|
||||
let result = unsafe {
|
||||
mach_msg(
|
||||
&mut buffer.message.header,
|
||||
options,
|
||||
0,
|
||||
std::mem::size_of::<FrameMessageBuffer>() as u32,
|
||||
self.port,
|
||||
timeout,
|
||||
MACH_PORT_NULL,
|
||||
)
|
||||
};
|
||||
if result == MACH_RCV_TIMED_OUT {
|
||||
return Ok(RecvResult::WouldBlock);
|
||||
}
|
||||
if result != MACH_MSG_SUCCESS {
|
||||
return Err(std::io::Error::other(format!("mach_msg receive failed: {result:#x}")));
|
||||
}
|
||||
|
||||
let received_complex = buffer.message.header.msgh_bits & MACH_MSGH_BITS_COMPLEX != 0;
|
||||
let descriptor_count = if received_complex { buffer.message.body.msgh_descriptor_count } else { 0 };
|
||||
let surface = (descriptor_count == 1 && buffer.message.surface.descriptor_type == MACH_MSG_PORT_DESCRIPTOR as u8).then(|| SendRight(buffer.message.surface.name));
|
||||
|
||||
if buffer.message.header.msgh_size as usize != std::mem::size_of::<FrameMessage>() {
|
||||
return Err(std::io::Error::other(format!("malformed frame message: {} bytes", buffer.message.header.msgh_size)));
|
||||
}
|
||||
let Some(surface) = surface else {
|
||||
return Err(std::io::Error::other("frame message carried no surface port"));
|
||||
};
|
||||
|
||||
Ok(RecvResult::Frame(WireFrame {
|
||||
descriptor: buffer.message.descriptor,
|
||||
surface,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WireFrame {
|
||||
descriptor: FrameDescriptor,
|
||||
surface: SendRight,
|
||||
}
|
||||
|
||||
impl WireFrame {
|
||||
pub(crate) fn seq(&self) -> u64 {
|
||||
self.descriptor.seq
|
||||
}
|
||||
|
||||
pub(crate) fn import(self, surface: &FrameSurface) -> Option<wgpu::Texture> {
|
||||
let WireFrame { descriptor, surface: port } = self;
|
||||
let format = super::wire_color_type(descriptor.format)?;
|
||||
|
||||
// Lookup takes its own reference on the surface, port can be dropped.
|
||||
let io_surface = IOSurfaceRef::lookup_from_mach_port(port.0);
|
||||
drop(port);
|
||||
|
||||
let Some(io_surface) = io_surface else {
|
||||
tracing::error!("Failed to look up the IOSurface for frame {}", descriptor.seq);
|
||||
return None;
|
||||
};
|
||||
let io_surface_ref: &IOSurfaceRef = &io_surface;
|
||||
|
||||
let content = crate::frames::import::ContentRect {
|
||||
x: descriptor.content_x,
|
||||
y: descriptor.content_y,
|
||||
width: descriptor.content_width,
|
||||
height: descriptor.content_height,
|
||||
source_width: descriptor.source_width,
|
||||
source_height: descriptor.source_height,
|
||||
};
|
||||
let importer = crate::frames::import::iosurface::IOSurfaceImporter::from_parts(io_surface_ref as *const _ as *mut std::os::raw::c_void, descriptor.width, descriptor.height, format);
|
||||
surface.import_texture(importer, content)
|
||||
}
|
||||
}
|
||||
175
desktop/ui/src/frames/plane/win.rs
Normal file
175
desktop/ui/src/frames/plane/win.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use windows::Win32::Foundation::{CloseHandle, DUPLICATE_CLOSE_SOURCE, DUPLICATE_SAME_ACCESS, DuplicateHandle, HANDLE};
|
||||
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcess, PROCESS_DUP_HANDLE};
|
||||
|
||||
use crate::frames::import::ContentRect;
|
||||
use crate::frames::surface::FrameSurface;
|
||||
use crate::remote::HostConfig;
|
||||
use crate::remote::messages::EventMessage;
|
||||
|
||||
struct MainProcess(HANDLE);
|
||||
|
||||
// SAFETY: process handles may be used and closed from any thread.
|
||||
unsafe impl Send for MainProcess {}
|
||||
unsafe impl Sync for MainProcess {}
|
||||
|
||||
impl MainProcess {
|
||||
fn open(pid: u32) -> windows::core::Result<Self> {
|
||||
// SAFETY: plain OpenProcess call; on success the handle is ours to close.
|
||||
unsafe { OpenProcess(PROCESS_DUP_HANDLE, false, pid).map(Self) }
|
||||
}
|
||||
|
||||
fn duplicate_into(&self, handle: HANDLE) -> windows::core::Result<u64> {
|
||||
let mut target = HANDLE::default();
|
||||
// SAFETY: both process handles are valid; `target` receives the duplicate.
|
||||
unsafe { DuplicateHandle(GetCurrentProcess(), handle, self.0, &mut target, 0, false, DUPLICATE_SAME_ACCESS)? };
|
||||
Ok(target.0 as u64)
|
||||
}
|
||||
|
||||
fn close_in_main(&self, handle: u64) {
|
||||
let mut reclaimed = HANDLE::default();
|
||||
// SAFETY: `handle` came from `duplicate_into` and is valid.
|
||||
unsafe {
|
||||
if let Err(e) = DuplicateHandle(self.0, HANDLE(handle as _), GetCurrentProcess(), &mut reclaimed, 0, false, DUPLICATE_CLOSE_SOURCE) {
|
||||
tracing::warn!("Failed to reclaim a frame handle from the main process: {e}");
|
||||
}
|
||||
if !reclaimed.is_invalid() {
|
||||
let _ = CloseHandle(reclaimed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MainProcess {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: we own the process handle.
|
||||
unsafe {
|
||||
let _ = CloseHandle(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PlaneSender {
|
||||
main: Arc<MainProcess>,
|
||||
events: Arc<Mutex<IpcSender<EventMessage>>>,
|
||||
}
|
||||
|
||||
impl PlaneSender {
|
||||
pub(crate) fn from_config(config: &HostConfig, events: Arc<Mutex<IpcSender<EventMessage>>>) -> Option<Self> {
|
||||
match MainProcess::open(config.main_pid) {
|
||||
Ok(main) => Some(Self { main: Arc::new(main), events }),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to open the main process for handle duplication, falling back to software frames: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn stage(&self, info: &cef::AcceleratedPaintInfo) -> Option<StagedFrame> {
|
||||
let coded_size = &info.extra.coded_size;
|
||||
if coded_size.width <= 0 || coded_size.height <= 0 {
|
||||
tracing::error!("Accelerated paint delivered an invalid coded size: {}x{}", coded_size.width, coded_size.height);
|
||||
return None;
|
||||
}
|
||||
|
||||
let handle = match self.main.duplicate_into(HANDLE(info.shared_texture_handle)) {
|
||||
Ok(handle) => handle,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to duplicate the shared texture handle into the main process: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(StagedFrame {
|
||||
handle: HandleInMain { handle, main: self.main.clone() },
|
||||
width: coded_size.width as u32,
|
||||
height: coded_size.height as u32,
|
||||
format: *info.format.as_ref() as u32,
|
||||
content: ContentRect::try_from(info).ok(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn send(&self, seq: u64, frame: StagedFrame) -> std::io::Result<()> {
|
||||
let message = EventMessage::AcceleratedFrame {
|
||||
seq,
|
||||
handle: frame.handle.handle,
|
||||
width: frame.width,
|
||||
height: frame.height,
|
||||
format: frame.format,
|
||||
content: frame.content,
|
||||
};
|
||||
let sender = self.events.lock().map_err(|_| std::io::Error::other("the host message sender lock is poisoned"))?;
|
||||
match sender.send(message) {
|
||||
Ok(()) => {
|
||||
// Dropping the handle would reclaim it. We must not drop.
|
||||
std::mem::forget(frame.handle);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(std::io::Error::other(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct StagedFrame {
|
||||
handle: HandleInMain,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: u32,
|
||||
content: Option<ContentRect>,
|
||||
}
|
||||
|
||||
struct HandleInMain {
|
||||
handle: u64,
|
||||
main: Arc<MainProcess>,
|
||||
}
|
||||
|
||||
impl Drop for HandleInMain {
|
||||
fn drop(&mut self) {
|
||||
self.main.close_in_main(self.handle);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WireFrame {
|
||||
seq: u64,
|
||||
handle: ReceivedHandle,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: u32,
|
||||
content: Option<ContentRect>,
|
||||
}
|
||||
|
||||
impl WireFrame {
|
||||
pub(crate) fn new(seq: u64, handle: u64, width: u32, height: u32, format: u32, content: Option<ContentRect>) -> Self {
|
||||
Self {
|
||||
seq,
|
||||
handle: ReceivedHandle(handle),
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn seq(&self) -> u64 {
|
||||
self.seq
|
||||
}
|
||||
|
||||
pub(crate) fn import(self, surface: &FrameSurface) -> Option<wgpu::Texture> {
|
||||
let format = super::wire_color_type(self.format)?;
|
||||
let content = self.content.unwrap_or_default();
|
||||
surface.import_texture(crate::frames::import::d3d11::D3D11Importer::from_parts(self.handle.0, self.width, self.height, format), content)
|
||||
}
|
||||
}
|
||||
|
||||
struct ReceivedHandle(u64);
|
||||
|
||||
impl Drop for ReceivedHandle {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: the host duplicated this handle into our process for us to own.
|
||||
if let Err(e) = unsafe { CloseHandle(HANDLE(self.0 as _)) } {
|
||||
tracing::warn!("Failed to close a remote frame handle: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
131
desktop/ui/src/frames/receive.rs
Normal file
131
desktop/ui/src/frames/receive.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use ipc_channel::ipc::{IpcSender, IpcSharedMemory};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::FrameSurface;
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use super::plane;
|
||||
use super::sink::FrameSink;
|
||||
use crate::UiEvent;
|
||||
use crate::events::EventQueue;
|
||||
use crate::remote::messages::HostControlMessage;
|
||||
|
||||
pub(crate) enum PendingFrame {
|
||||
Software {
|
||||
seq: u64,
|
||||
segment: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
},
|
||||
#[cfg(all(target_os = "windows", feature = "accelerated_paint"))]
|
||||
Accelerated(plane::WireFrame),
|
||||
}
|
||||
|
||||
impl PendingFrame {
|
||||
pub(crate) fn seq(&self) -> u64 {
|
||||
match self {
|
||||
PendingFrame::Software { seq, .. } => *seq,
|
||||
#[cfg(all(target_os = "windows", feature = "accelerated_paint"))]
|
||||
PendingFrame::Accelerated(frame) => frame.seq(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SegmentTable(Vec<Option<IpcSharedMemory>>);
|
||||
|
||||
impl SegmentTable {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
|
||||
pub(crate) fn advertise(&mut self, index: u32, shm: IpcSharedMemory) {
|
||||
let index = index as usize;
|
||||
if self.0.len() <= index {
|
||||
self.0.resize_with(index + 1, || None);
|
||||
}
|
||||
self.0[index] = Some(shm);
|
||||
}
|
||||
|
||||
fn frame(&self, seq: u64, segment: u32, width: u32, height: u32) -> Option<&[u8]> {
|
||||
let frame_bytes = width as usize * height as usize * 4;
|
||||
match self.0.get(segment as usize).and_then(Option::as_ref) {
|
||||
Some(shm) if shm.len() >= frame_bytes => Some(&shm[..frame_bytes]),
|
||||
Some(shm) => {
|
||||
tracing::error!("Frame {seq} needs {frame_bytes} bytes but segment {segment} holds {}", shm.len());
|
||||
None
|
||||
}
|
||||
None => {
|
||||
tracing::error!("Frame {seq} references unadvertised segment {segment}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct FrameConsumer {
|
||||
surface: FrameSurface,
|
||||
events: EventQueue,
|
||||
sender: IpcSender<HostControlMessage>,
|
||||
sink: Arc<FrameSink>,
|
||||
}
|
||||
|
||||
impl FrameConsumer {
|
||||
pub(crate) fn new(surface: FrameSurface, events: EventQueue, sender: IpcSender<HostControlMessage>) -> Self {
|
||||
Self {
|
||||
surface,
|
||||
events,
|
||||
sender,
|
||||
sink: Arc::new(FrameSink::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn deliver(&self, seq: u64, install: impl FnOnce(&FrameSurface) -> Option<wgpu::Texture>) {
|
||||
self.sink.deliver(&self.sender, seq, || match install(&self.surface) {
|
||||
Some(texture) => {
|
||||
self.events.send(UiEvent::Frame(texture));
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn deliver_pending(&self, frame: PendingFrame, segments: &SegmentTable) {
|
||||
match frame {
|
||||
PendingFrame::Software { seq, segment, width, height } => {
|
||||
self.deliver(seq, |surface| {
|
||||
segments.frame(seq, segment, width, height).and_then(|pixels| surface.upload_buffer(pixels, width, height))
|
||||
});
|
||||
}
|
||||
#[cfg(all(target_os = "windows", feature = "accelerated_paint"))]
|
||||
PendingFrame::Accelerated(frame) => self.deliver_accelerated(frame),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) fn deliver_accelerated(&self, frame: plane::WireFrame) {
|
||||
let seq = frame.seq();
|
||||
self.deliver(seq, |surface| frame.import(surface));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(any(target_os = "linux", target_os = "macos"), feature = "accelerated_paint"))]
|
||||
pub(crate) fn plane_receiver_loop(receiver: plane::PlaneReceiver, consumer: FrameConsumer) {
|
||||
loop {
|
||||
let mut frame = loop {
|
||||
match receiver.recv_blocking() {
|
||||
Ok(plane::RecvResult::Frame(frame)) => break frame,
|
||||
Ok(plane::RecvResult::WouldBlock) => continue,
|
||||
Ok(plane::RecvResult::Closed) => return,
|
||||
Err(e) => {
|
||||
tracing::error!("Accelerated frame plane failed: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
// Drain any newer frames that have arrived since the blocking receive
|
||||
while let Ok(plane::RecvResult::Frame(newer)) = receiver.try_recv() {
|
||||
frame = newer;
|
||||
}
|
||||
consumer.deliver_accelerated(frame);
|
||||
}
|
||||
}
|
||||
143
desktop/ui/src/frames/resample.rs
Normal file
143
desktop/ui/src/frames/resample.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct Resampler {
|
||||
device: wgpu::Device,
|
||||
pipeline: Arc<OnceLock<Pipeline>>,
|
||||
}
|
||||
|
||||
struct Pipeline {
|
||||
format: wgpu::TextureFormat,
|
||||
sampler: wgpu::Sampler,
|
||||
layout: wgpu::BindGroupLayout,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
}
|
||||
|
||||
impl Resampler {
|
||||
pub(super) fn new(device: wgpu::Device) -> Self {
|
||||
Self {
|
||||
device,
|
||||
pipeline: Arc::new(OnceLock::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn encode(&self, encoder: &mut wgpu::CommandEncoder, source: &wgpu::Texture, content_origin: wgpu::Origin3d, content_size: wgpu::Extent3d, target: &wgpu::Texture) {
|
||||
let pipeline = self.pipeline.get_or_init(|| Pipeline::new(&self.device, target.format()));
|
||||
debug_assert_eq!(pipeline.format, target.format());
|
||||
|
||||
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("CEF Resample Bind Group"),
|
||||
layout: &pipeline.layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&source.create_view(&Default::default())),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&pipeline.sampler),
|
||||
},
|
||||
],
|
||||
});
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("CEF Resample Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &target.create_view(&Default::default()),
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_pipeline(&pipeline.pipeline);
|
||||
pass.set_immediates(
|
||||
0,
|
||||
bytemuck::bytes_of(&Immediates {
|
||||
content_origin: [content_origin.x as f32, content_origin.y as f32],
|
||||
content_size: [content_size.width as f32, content_size.height as f32],
|
||||
}),
|
||||
);
|
||||
pass.set_bind_group(0, &bind_group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("CEF Resample Sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
..Default::default()
|
||||
});
|
||||
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("CEF Resample Bind Group Layout"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("CEF Resample Pipeline Layout"),
|
||||
bind_group_layouts: &[Some(&layout)],
|
||||
immediate_size: std::mem::size_of::<Immediates>() as u32,
|
||||
});
|
||||
let shader = device.create_shader_module(wgpu::include_wgsl!("resample.wgsl"));
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("CEF Resample Pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
Self { format, sampler, layout, pipeline }
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Immediates {
|
||||
content_origin: [f32; 2],
|
||||
content_size: [f32; 2],
|
||||
}
|
||||
77
desktop/ui/src/frames/resample.wgsl
Normal file
77
desktop/ui/src/frames/resample.wgsl
Normal file
@@ -0,0 +1,77 @@
|
||||
// =============
|
||||
// VERTEX SHADER
|
||||
// =============
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) tex_coords: vec2<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
let pos = array(
|
||||
vec2f(-1.0, -1.0),
|
||||
vec2f(3.0, -1.0),
|
||||
vec2f(-1.0, 3.0),
|
||||
);
|
||||
let xy = pos[vertex_index];
|
||||
out.clip_position = vec4f(xy, 0.0, 1.0);
|
||||
let coords = xy / 2. + 0.5;
|
||||
out.tex_coords = vec2f(coords.x, 1. - coords.y);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ===============
|
||||
// FRAGMENT SHADER
|
||||
// ===============
|
||||
|
||||
struct Immediates {
|
||||
content_origin: vec2<f32>,
|
||||
content_size: vec2<f32>,
|
||||
};
|
||||
|
||||
var<immediate> immediates: Immediates;
|
||||
|
||||
@group(0) @binding(0)
|
||||
var t_frame: texture_2d<f32>;
|
||||
@group(0) @binding(1)
|
||||
var s_frame: sampler;
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let sample_pos = in.tex_coords * immediates.content_size;
|
||||
let nearest = floor(sample_pos - 0.5) + 0.5;
|
||||
let t = sample_pos - nearest;
|
||||
|
||||
// Catmull-Rom spline interpolation based sampeling
|
||||
// See https://gist.github.com/TheRealMJP/c83b8c0f46b63f3a88a5986f4fa982b1
|
||||
|
||||
let weight_before = t * (-0.5 + t * (1.0 - 0.5 * t));
|
||||
let weight_nearest = 1.0 + t * t * (-2.5 + 1.5 * t);
|
||||
let weight_next = t * (0.5 + t * (2.0 - 1.5 * t));
|
||||
let weight_after = t * t * (-0.5 + 0.5 * t);
|
||||
|
||||
let weight_middle = weight_nearest + weight_next;
|
||||
let middle = nearest + weight_next / weight_middle;
|
||||
|
||||
let frame_size = vec2<f32>(textureDimensions(t_frame));
|
||||
let content_min = vec2<f32>(0.5);
|
||||
let content_max = immediates.content_size - 0.5;
|
||||
let uv_before = (immediates.content_origin + clamp(nearest - 1.0, content_min, content_max)) / frame_size;
|
||||
let uv_middle = (immediates.content_origin + clamp(middle, content_min, content_max)) / frame_size;
|
||||
let uv_after = (immediates.content_origin + clamp(nearest + 2.0, content_min, content_max)) / frame_size;
|
||||
|
||||
var color = vec4<f32>(0.0);
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_before.x, uv_before.y), 0.0) * weight_before.x * weight_before.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_middle.x, uv_before.y), 0.0) * weight_middle.x * weight_before.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_after.x, uv_before.y), 0.0) * weight_after.x * weight_before.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_before.x, uv_middle.y), 0.0) * weight_before.x * weight_middle.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_middle.x, uv_middle.y), 0.0) * weight_middle.x * weight_middle.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_after.x, uv_middle.y), 0.0) * weight_after.x * weight_middle.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_before.x, uv_after.y), 0.0) * weight_before.x * weight_after.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_middle.x, uv_after.y), 0.0) * weight_middle.x * weight_after.y;
|
||||
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_after.x, uv_after.y), 0.0) * weight_after.x * weight_after.y;
|
||||
|
||||
return clamp(color, vec4<f32>(0.0), vec4<f32>(1.0));
|
||||
}
|
||||
85
desktop/ui/src/frames/sequence.rs
Normal file
85
desktop/ui/src/frames/sequence.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Condvar, Mutex, PoisonError};
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::consts::FRAMES_IN_FLIGHT_LIMIT;
|
||||
|
||||
pub(crate) struct SequenceState {
|
||||
last_sent: AtomicU64,
|
||||
last_acked: AtomicU64,
|
||||
ack_lock: Mutex<()>,
|
||||
ack_signal: Condvar,
|
||||
}
|
||||
|
||||
impl SequenceState {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
last_sent: AtomicU64::new(0),
|
||||
last_acked: AtomicU64::new(0),
|
||||
ack_lock: Mutex::new(()),
|
||||
ack_signal: Condvar::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn claim(self: &Arc<Self>) -> Option<FrameSequenceClaim> {
|
||||
let last_sent = self.last_sent.load(Ordering::Relaxed);
|
||||
let last_acked = self.last_acked.load(Ordering::Relaxed);
|
||||
if last_sent.saturating_sub(last_acked) >= FRAMES_IN_FLIGHT_LIMIT {
|
||||
return None;
|
||||
}
|
||||
let seq = last_sent + 1;
|
||||
self.last_sent.store(seq, Ordering::Relaxed);
|
||||
Some(FrameSequenceClaim {
|
||||
seq,
|
||||
sequence: self.clone(),
|
||||
commited: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn ack(&self, seq: u64) {
|
||||
self.last_acked.fetch_max(seq, Ordering::Relaxed);
|
||||
drop(self.ack_lock.lock().unwrap_or_else(PoisonError::into_inner));
|
||||
self.ack_signal.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct FrameSequenceClaim {
|
||||
seq: u64,
|
||||
commited: bool,
|
||||
sequence: Arc<SequenceState>,
|
||||
}
|
||||
|
||||
impl FrameSequenceClaim {
|
||||
pub(crate) fn seq(&self) -> u64 {
|
||||
self.seq
|
||||
}
|
||||
|
||||
pub(crate) fn commit(mut self) {
|
||||
self.commited = true;
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) fn wait_for_ack(&self) -> bool {
|
||||
let deadline = Instant::now() + crate::consts::FRAME_ACK_TIMEOUT;
|
||||
let mut guard = self.sequence.ack_lock.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
loop {
|
||||
if self.sequence.last_acked.load(Ordering::Relaxed) >= self.seq {
|
||||
return true;
|
||||
}
|
||||
let Some(remaining) = deadline.checked_duration_since(Instant::now()) else {
|
||||
return false;
|
||||
};
|
||||
guard = self.sequence.ack_signal.wait_timeout(guard, remaining).unwrap_or_else(PoisonError::into_inner).0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FrameSequenceClaim {
|
||||
fn drop(&mut self) {
|
||||
// Roll back the claim if it was never committed to free the sequence number
|
||||
if !self.commited {
|
||||
let _ = self.sequence.last_sent.compare_exchange(self.seq, self.seq - 1, Ordering::Relaxed, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
desktop/ui/src/frames/sink.rs
Normal file
45
desktop/ui/src/frames/sink.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::remote::messages::HostControlMessage;
|
||||
|
||||
pub(super) struct FrameSink {
|
||||
state: Mutex<FrameSinkState>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FrameSinkState {
|
||||
newest_installed: u64,
|
||||
last_acked: u64,
|
||||
}
|
||||
|
||||
impl FrameSink {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
state: Mutex::new(FrameSinkState::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn deliver(&self, sender: &IpcSender<HostControlMessage>, seq: u64, install: impl FnOnce() -> bool) {
|
||||
let Ok(mut state) = self.state.lock() else {
|
||||
tracing::error!("Failed to lock the frame sink");
|
||||
return;
|
||||
};
|
||||
|
||||
if seq > 1 && seq - 1 > state.last_acked {
|
||||
if let Err(e) = sender.send(HostControlMessage::FrameAck { seq: seq - 1 }) {
|
||||
tracing::debug!("Failed to ack superseded frames to CEF host: {e}");
|
||||
}
|
||||
state.last_acked = seq - 1;
|
||||
}
|
||||
if seq > state.newest_installed && install() {
|
||||
state.newest_installed = seq;
|
||||
}
|
||||
if seq > state.last_acked {
|
||||
if let Err(e) = sender.send(HostControlMessage::FrameAck { seq }) {
|
||||
tracing::debug!("Failed to ack frame to CEF host: {e}");
|
||||
}
|
||||
state.last_acked = seq;
|
||||
}
|
||||
}
|
||||
}
|
||||
155
desktop/ui/src/frames/streamer.rs
Normal file
155
desktop/ui/src/frames/streamer.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use ipc_channel::ipc::{IpcSender, IpcSharedMemory};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use super::plane;
|
||||
use super::sequence::{FrameSequenceClaim, SequenceState};
|
||||
use crate::consts::{FRAME_SEGMENT_GRANULARITY, FRAME_SEGMENT_POOL_SIZE};
|
||||
use crate::remote::messages::EventMessage;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct FrameStreamer(Arc<StreamerInner>);
|
||||
|
||||
struct StreamerInner {
|
||||
events: Arc<Mutex<IpcSender<EventMessage>>>,
|
||||
sequence: Arc<SequenceState>,
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
plane: Option<plane::PlaneSender>,
|
||||
staged: Mutex<Staged>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Staged {
|
||||
segments: Vec<IpcSharedMemory>,
|
||||
pending_adverts: Vec<(u32, IpcSharedMemory)>,
|
||||
buffer: Option<StagedBuffer>,
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
accelerated: Option<(FrameSequenceClaim, plane::StagedFrame)>,
|
||||
}
|
||||
|
||||
struct StagedBuffer {
|
||||
claim: FrameSequenceClaim,
|
||||
segment: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl FrameStreamer {
|
||||
pub(crate) fn new(events: Arc<Mutex<IpcSender<EventMessage>>>, sequence: Arc<SequenceState>, #[cfg(feature = "accelerated_paint")] plane: Option<plane::PlaneSender>) -> Self {
|
||||
Self(Arc::new(StreamerInner {
|
||||
events,
|
||||
sequence,
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
plane,
|
||||
staged: Mutex::new(Staged::default()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn stage_buffer(&self, buffer: &[u8], width: u32, height: u32) {
|
||||
debug_assert_eq!(buffer.len(), width as usize * height as usize * 4);
|
||||
if buffer.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(claim) = self.0.sequence.claim() else {
|
||||
return;
|
||||
};
|
||||
let segment = (claim.seq() % FRAME_SEGMENT_POOL_SIZE) as u32;
|
||||
|
||||
let Ok(mut staged) = self.0.staged.lock() else {
|
||||
tracing::error!("Failed to lock the frame staging state");
|
||||
return;
|
||||
};
|
||||
let staged = &mut *staged;
|
||||
if staged.segments.len() < FRAME_SEGMENT_POOL_SIZE as usize {
|
||||
staged.segments.resize_with(FRAME_SEGMENT_POOL_SIZE as usize, || IpcSharedMemory::from_bytes(&[]));
|
||||
}
|
||||
|
||||
let backing = &mut staged.segments[segment as usize];
|
||||
if backing.len() < buffer.len() {
|
||||
let capacity = buffer.len().next_multiple_of(FRAME_SEGMENT_GRANULARITY);
|
||||
*backing = IpcSharedMemory::from_byte(0, capacity);
|
||||
staged.pending_adverts.push((segment, backing.clone()));
|
||||
}
|
||||
|
||||
unsafe { backing.deref_mut()[..buffer.len()].copy_from_slice(buffer) };
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if !staged.pending_adverts.iter().any(|(index, _)| *index == segment) {
|
||||
staged.pending_adverts.push((segment, backing.clone()));
|
||||
}
|
||||
|
||||
staged.buffer = Some(StagedBuffer { claim, segment, width, height });
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) fn stage_texture(&self, info: &cef::AcceleratedPaintInfo) {
|
||||
let Some(plane) = &self.0.plane else {
|
||||
tracing::error!("Accelerated paint delivered without a frame plane");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(claim) = self.0.sequence.claim() else {
|
||||
return;
|
||||
};
|
||||
let Some(frame) = plane.stage(info) else {
|
||||
return;
|
||||
};
|
||||
let Ok(mut staged) = self.0.staged.lock() else {
|
||||
tracing::error!("Failed to lock the frame staging state");
|
||||
return;
|
||||
};
|
||||
staged.accelerated = Some((claim, frame));
|
||||
}
|
||||
|
||||
pub(crate) fn publish(&self) {
|
||||
let Ok(mut staged) = self.0.staged.lock() else {
|
||||
tracing::error!("Failed to lock the frame staging state");
|
||||
return;
|
||||
};
|
||||
let adverts = std::mem::take(&mut staged.pending_adverts);
|
||||
let software = staged.buffer.take();
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
let accelerated = staged.accelerated.take();
|
||||
drop(staged);
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
if let Some((claim, frame)) = accelerated
|
||||
&& let Some(plane) = &self.0.plane
|
||||
{
|
||||
match plane.send(claim.seq(), frame) {
|
||||
Ok(()) => {
|
||||
if !claim.wait_for_ack() {
|
||||
tracing::warn!("Accelerated frame {} was not acked", claim.seq());
|
||||
}
|
||||
claim.commit();
|
||||
}
|
||||
Err(e) => tracing::debug!("Failed to send accelerated frame to main process: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
if adverts.is_empty() && software.is_none() {
|
||||
return;
|
||||
}
|
||||
let Ok(sender) = self.0.events.lock() else {
|
||||
tracing::error!("Failed to lock host message sender");
|
||||
return;
|
||||
};
|
||||
for (index, shm) in adverts {
|
||||
if let Err(e) = sender.send(EventMessage::AdvertiseFrameSegment { index, shm }) {
|
||||
tracing::debug!("Failed to send frame segment to main process: {e}");
|
||||
}
|
||||
}
|
||||
if let Some(StagedBuffer { claim, segment, width, height }) = software {
|
||||
match sender.send(EventMessage::SoftwareFrame {
|
||||
seq: claim.seq(),
|
||||
segment,
|
||||
width,
|
||||
height,
|
||||
}) {
|
||||
Ok(()) => claim.commit(),
|
||||
Err(e) => tracing::debug!("Failed to send frame to main process: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
263
desktop/ui/src/frames/surface.rs
Normal file
263
desktop/ui/src/frames/surface.rs
Normal file
@@ -0,0 +1,263 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use super::import::ContentMapping;
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use super::resample::Resampler;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct FrameSurface {
|
||||
device: wgpu::Device,
|
||||
queue: wgpu_sync::Queue,
|
||||
slot: Arc<Mutex<Option<wgpu::Texture>>>,
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
resampler: Resampler,
|
||||
}
|
||||
|
||||
impl FrameSurface {
|
||||
pub(crate) fn new(device: wgpu::Device, queue: wgpu_sync::Queue) -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
resampler: Resampler::new(device.clone()),
|
||||
device,
|
||||
queue,
|
||||
slot: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn upload_buffer(&self, buffer: &[u8], width: u32, height: u32) -> Option<wgpu::Texture> {
|
||||
debug_assert_eq!(buffer.len(), width as usize * height as usize * 4);
|
||||
|
||||
let Ok(mut slot) = self.slot.lock() else {
|
||||
tracing::error!("Failed to lock the frame surface");
|
||||
return None;
|
||||
};
|
||||
|
||||
if buffer.chunks_exact(4).take(width as usize).all(|pixel| pixel[3] == 0) {
|
||||
tracing::debug!("Skipping fully transparent frame");
|
||||
return None;
|
||||
}
|
||||
|
||||
if slot.as_ref().is_none_or(|texture| texture.width() != width || texture.height() != height) {
|
||||
*slot = Some(self.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("CEF Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Bgra8Unorm,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
}));
|
||||
}
|
||||
let texture = slot.as_ref()?;
|
||||
|
||||
self.queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
buffer,
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(4 * width),
|
||||
rows_per_image: None,
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
|
||||
Some(texture.clone())
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) fn import_texture(&self, importer: impl crate::frames::import::TextureImporter, content_rect: crate::frames::import::ContentRect) -> Option<wgpu::Texture> {
|
||||
let imported = match importer.import_to_wgpu(&self.device) {
|
||||
Ok(texture) => texture,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to import remote accelerated frame: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("CEF Frame Copy Encoder"),
|
||||
});
|
||||
let output = match content_rect.mapping(imported.width(), imported.height()) {
|
||||
ContentMapping::Identity => {
|
||||
let output = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("CEF Imported Frame Copy"),
|
||||
size: imported.size(),
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: imported.format(),
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
});
|
||||
encoder.copy_texture_to_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &imported,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &output,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
imported.size(),
|
||||
);
|
||||
output
|
||||
}
|
||||
ContentMapping::Scaled(content_rect) => {
|
||||
let output = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("CEF Imported Scaled Frame Copy"),
|
||||
size: wgpu::Extent3d {
|
||||
width: content_rect.source_width,
|
||||
height: content_rect.source_height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: imported.format(),
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
let size = wgpu::Extent3d {
|
||||
width: content_rect.width,
|
||||
height: content_rect.height,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
self.resampler.encode(
|
||||
&mut encoder,
|
||||
&imported,
|
||||
wgpu::Origin3d {
|
||||
x: content_rect.x,
|
||||
y: content_rect.y,
|
||||
z: 0,
|
||||
},
|
||||
size,
|
||||
&output,
|
||||
);
|
||||
output
|
||||
}
|
||||
};
|
||||
|
||||
let blank_check = blank_check::encode_readback(&self.device, &mut encoder, &output);
|
||||
|
||||
let submission = self.queue.submit([encoder.finish()]);
|
||||
|
||||
let blank_check = blank_check.map();
|
||||
|
||||
let _ = self.device.poll(wgpu::PollType::Wait {
|
||||
submission_index: Some(submission),
|
||||
timeout: None,
|
||||
});
|
||||
|
||||
if blank_check.check_is_blank() {
|
||||
tracing::debug!("Skipping fully transparent accelerated frame");
|
||||
return None;
|
||||
}
|
||||
|
||||
let Ok(mut slot) = self.slot.lock() else {
|
||||
tracing::error!("Failed to lock the frame surface");
|
||||
return None;
|
||||
};
|
||||
*slot = Some(output.clone());
|
||||
Some(output)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
mod blank_check {
|
||||
use std::sync::mpsc;
|
||||
|
||||
const STRIP_BYTES: u32 = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
||||
const STRIP_TEXELS: u32 = STRIP_BYTES / 4;
|
||||
|
||||
pub(super) fn encode_readback(device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, texture: &wgpu::Texture) -> PendingBlankCheck {
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("CEF Blank Check"),
|
||||
size: STRIP_BYTES as u64,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let width = texture.width().min(STRIP_TEXELS);
|
||||
encoder.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d {
|
||||
x: (texture.width() - width) / 2,
|
||||
y: 0,
|
||||
z: 0,
|
||||
},
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buffer,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(STRIP_BYTES),
|
||||
rows_per_image: None,
|
||||
},
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
PendingBlankCheck { buffer, width }
|
||||
}
|
||||
|
||||
pub(super) struct PendingBlankCheck {
|
||||
buffer: wgpu::Buffer,
|
||||
width: u32,
|
||||
}
|
||||
|
||||
impl PendingBlankCheck {
|
||||
pub(super) fn map(self) -> MappedBlankCheck {
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
self.buffer.slice(..u64::from(self.width) * 4).map_async(wgpu::MapMode::Read, move |result| {
|
||||
let _ = sender.send(result);
|
||||
});
|
||||
MappedBlankCheck {
|
||||
buffer: self.buffer,
|
||||
width: self.width,
|
||||
receiver,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct MappedBlankCheck {
|
||||
buffer: wgpu::Buffer,
|
||||
width: u32,
|
||||
receiver: mpsc::Receiver<Result<(), wgpu::BufferAsyncError>>,
|
||||
}
|
||||
|
||||
impl MappedBlankCheck {
|
||||
pub(super) fn check_is_blank(self) -> bool {
|
||||
match self.receiver.try_recv() {
|
||||
Ok(Ok(())) => {
|
||||
let slice = self.buffer.slice(..u64::from(self.width) * 4);
|
||||
slice.get_mapped_range().chunks_exact(4).all(|texel| texel[3] == 0)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
251
desktop/ui/src/input.rs
Normal file
251
desktop/ui/src/input.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use cef::sys::{cef_key_event_type_t, cef_mouse_button_type_t};
|
||||
use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, MouseEvent};
|
||||
use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
||||
|
||||
mod keymap;
|
||||
use keymap::{ToCharRepresentation, ToNativeKeycode, ToVKBits};
|
||||
|
||||
mod state;
|
||||
pub(crate) use state::{CefModifiers, InputState};
|
||||
|
||||
use super::consts::{PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y};
|
||||
|
||||
/// A window input translated into the plain data CEF consumes — no winit types, so it can
|
||||
/// be applied to a browser living in another process.
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) enum InputEvent {
|
||||
MouseMove { data: MouseData, leave: bool },
|
||||
MouseClick { data: MouseData, button: MouseButtonKind, up: bool, click_count: i32 },
|
||||
MouseWheel { data: MouseData, delta_x: i32, delta_y: i32 },
|
||||
Key(KeyData),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct MouseData {
|
||||
pub(crate) x: i32,
|
||||
pub(crate) y: i32,
|
||||
pub(crate) modifiers: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) enum MouseButtonKind {
|
||||
Left,
|
||||
Right,
|
||||
Middle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) enum KeyEventKind {
|
||||
RawKeyDown,
|
||||
KeyUp,
|
||||
Char,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct KeyData {
|
||||
pub(crate) kind: KeyEventKind,
|
||||
pub(crate) modifiers: u32,
|
||||
pub(crate) windows_key_code: i32,
|
||||
pub(crate) native_key_code: i32,
|
||||
pub(crate) character: u16,
|
||||
pub(crate) unmodified_character: u16,
|
||||
}
|
||||
|
||||
/// Turns a winit event into zero or more [`InputEvent`]s, updating the tracked input state
|
||||
/// (cursor position, click counting, modifiers) along the way.
|
||||
pub(crate) fn translate(input_state: &mut InputState, event: &WindowEvent) -> Vec<InputEvent> {
|
||||
match event {
|
||||
WindowEvent::PointerMoved { position, .. } => {
|
||||
if !input_state.cursor_move(position) {
|
||||
return Vec::new();
|
||||
}
|
||||
vec![InputEvent::MouseMove {
|
||||
data: input_state.mouse_data(),
|
||||
leave: false,
|
||||
}]
|
||||
}
|
||||
WindowEvent::PointerEntered { position, .. } => {
|
||||
let _ = input_state.cursor_move(position);
|
||||
vec![InputEvent::MouseMove {
|
||||
data: input_state.mouse_data(),
|
||||
leave: false,
|
||||
}]
|
||||
}
|
||||
WindowEvent::PointerLeft { position, .. } => {
|
||||
if let Some(position) = position {
|
||||
let _ = input_state.cursor_move(position);
|
||||
}
|
||||
vec![InputEvent::MouseMove {
|
||||
data: input_state.mouse_data(),
|
||||
leave: true,
|
||||
}]
|
||||
}
|
||||
WindowEvent::PointerButton { state, button, position, .. } => {
|
||||
let mouse_button = match button {
|
||||
ButtonSource::Mouse(mouse_button) => mouse_button,
|
||||
_ => {
|
||||
return Vec::new(); // TODO: Handle touch input
|
||||
}
|
||||
};
|
||||
|
||||
let _ = input_state.cursor_move(position);
|
||||
let click_count = input_state.mouse_input(mouse_button, state).into();
|
||||
let up = matches!(state, ElementState::Released);
|
||||
let button = match mouse_button {
|
||||
MouseButton::Left => MouseButtonKind::Left,
|
||||
MouseButton::Right => MouseButtonKind::Right,
|
||||
MouseButton::Middle => MouseButtonKind::Middle,
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
|
||||
vec![InputEvent::MouseClick {
|
||||
data: input_state.mouse_data(),
|
||||
button,
|
||||
up,
|
||||
click_count,
|
||||
}]
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, phase: _, device_id: _, .. } => {
|
||||
let (mut delta_x, mut delta_y) = match delta {
|
||||
MouseScrollDelta::LineDelta(x, y) => (x * SCROLL_LINE_WIDTH as f32, y * SCROLL_LINE_HEIGHT as f32),
|
||||
MouseScrollDelta::PixelDelta(physical_position) => (physical_position.x as f32, physical_position.y as f32),
|
||||
};
|
||||
delta_x *= SCROLL_SPEED_X;
|
||||
delta_y *= SCROLL_SPEED_Y;
|
||||
|
||||
vec![InputEvent::MouseWheel {
|
||||
data: input_state.mouse_data(),
|
||||
delta_x: delta_x as i32,
|
||||
delta_y: delta_y as i32,
|
||||
}]
|
||||
}
|
||||
WindowEvent::ModifiersChanged(modifiers) => {
|
||||
input_state.modifiers_changed(&modifiers.state());
|
||||
Vec::new()
|
||||
}
|
||||
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
|
||||
input_state.modifiers_apply_key_event(&event.logical_key, &event.state);
|
||||
|
||||
let mut kind = match (event.state, &event.logical_key) {
|
||||
(ElementState::Pressed, winit::keyboard::Key::Character(_)) => KeyEventKind::Char,
|
||||
(ElementState::Pressed, _) => KeyEventKind::RawKeyDown,
|
||||
(ElementState::Released, _) => KeyEventKind::KeyUp,
|
||||
};
|
||||
|
||||
let modifiers = input_state.cef_modifiers(&event.location, event.repeat).into();
|
||||
|
||||
let windows_key_code = match &event.logical_key {
|
||||
winit::keyboard::Key::Named(named) => named.to_vk_bits(),
|
||||
winit::keyboard::Key::Character(char) => char.chars().next().unwrap_or_default().to_vk_bits(),
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let native_key_code = event.physical_key.to_native_keycode();
|
||||
|
||||
let char_representation = event.logical_key.to_char_representation();
|
||||
#[allow(unused_mut)]
|
||||
let mut character = char_representation as u16;
|
||||
|
||||
if event.state == ElementState::Pressed && character != 0 {
|
||||
kind = KeyEventKind::Char;
|
||||
}
|
||||
|
||||
let unmodified_character = event.key_without_modifiers.to_char_representation() as u16;
|
||||
|
||||
#[cfg(target_os = "macos")] // See https://www.magpcss.org/ceforum/viewtopic.php?start=10&t=11650
|
||||
if character == 0 && unmodified_character == 0 && event.text_with_all_modifiers.is_some() {
|
||||
character = 1;
|
||||
}
|
||||
|
||||
let key = KeyData {
|
||||
kind,
|
||||
modifiers,
|
||||
windows_key_code,
|
||||
native_key_code,
|
||||
character,
|
||||
unmodified_character,
|
||||
};
|
||||
|
||||
if kind == KeyEventKind::Char {
|
||||
// CEF expects a raw key-down before the character event it produces.
|
||||
vec![
|
||||
InputEvent::Key(KeyData {
|
||||
kind: KeyEventKind::RawKeyDown,
|
||||
..key
|
||||
}),
|
||||
InputEvent::Key(KeyData {
|
||||
windows_key_code: char_representation as i32,
|
||||
..key
|
||||
}),
|
||||
]
|
||||
} else {
|
||||
vec![InputEvent::Key(key)]
|
||||
}
|
||||
}
|
||||
WindowEvent::PinchGesture { delta, .. } => {
|
||||
if !delta.is_normal() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let data = MouseData {
|
||||
modifiers: CefModifiers::PINCH_MODIFIERS.into(),
|
||||
..input_state.mouse_data()
|
||||
};
|
||||
|
||||
vec![InputEvent::MouseWheel {
|
||||
data,
|
||||
delta_x: 0,
|
||||
delta_y: (delta * PINCH_ZOOM_SPEED).round() as i32,
|
||||
}]
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a translated [`InputEvent`] to the browser. Must run on the thread owning the browser.
|
||||
pub(crate) fn apply(browser: &Browser, event: &InputEvent) {
|
||||
let Some(host) = browser.host() else { return };
|
||||
match event {
|
||||
InputEvent::MouseMove { data, leave } => {
|
||||
host.send_mouse_move_event(Some(&data.into()), *leave as i32);
|
||||
}
|
||||
InputEvent::MouseClick { data, button, up, click_count } => {
|
||||
let cef_button = cef::MouseButtonType::from(match button {
|
||||
MouseButtonKind::Left => cef_mouse_button_type_t::MBT_LEFT,
|
||||
MouseButtonKind::Right => cef_mouse_button_type_t::MBT_RIGHT,
|
||||
MouseButtonKind::Middle => cef_mouse_button_type_t::MBT_MIDDLE,
|
||||
});
|
||||
host.send_mouse_click_event(Some(&data.into()), cef_button, *up as i32, *click_count);
|
||||
}
|
||||
InputEvent::MouseWheel { data, delta_x, delta_y } => {
|
||||
host.send_mouse_wheel_event(Some(&data.into()), *delta_x, *delta_y);
|
||||
}
|
||||
InputEvent::Key(key) => {
|
||||
let key_event = KeyEvent {
|
||||
type_: match key.kind {
|
||||
KeyEventKind::RawKeyDown => cef_key_event_type_t::KEYEVENT_RAWKEYDOWN,
|
||||
KeyEventKind::KeyUp => cef_key_event_type_t::KEYEVENT_KEYUP,
|
||||
KeyEventKind::Char => cef_key_event_type_t::KEYEVENT_CHAR,
|
||||
}
|
||||
.into(),
|
||||
modifiers: key.modifiers,
|
||||
windows_key_code: key.windows_key_code,
|
||||
native_key_code: key.native_key_code,
|
||||
character: key.character,
|
||||
unmodified_character: key.unmodified_character,
|
||||
..Default::default()
|
||||
};
|
||||
host.send_key_event(Some(&key_event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&MouseData> for MouseEvent {
|
||||
fn from(data: &MouseData) -> Self {
|
||||
MouseEvent {
|
||||
x: data.x,
|
||||
y: data.y,
|
||||
modifiers: data.modifiers,
|
||||
}
|
||||
}
|
||||
}
|
||||
275
desktop/ui/src/input/keymap.rs
Normal file
275
desktop/ui/src/input/keymap.rs
Normal file
@@ -0,0 +1,275 @@
|
||||
use winit::keyboard::{Key, NamedKey, PhysicalKey};
|
||||
|
||||
pub(crate) trait ToCharRepresentation {
|
||||
fn to_char_representation(&self) -> char;
|
||||
}
|
||||
|
||||
impl ToCharRepresentation for Key {
|
||||
fn to_char_representation(&self) -> char {
|
||||
match self {
|
||||
Key::Named(named) => match named {
|
||||
NamedKey::Tab => '\t',
|
||||
NamedKey::Enter => '\r',
|
||||
NamedKey::Backspace => '\x08',
|
||||
NamedKey::Escape => '\x1b',
|
||||
_ => '\0',
|
||||
},
|
||||
Key::Character(char) => char.chars().next().unwrap_or_default(),
|
||||
_ => '\0',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ToNativeKeycode {
|
||||
fn to_native_keycode(&self) -> i32;
|
||||
}
|
||||
|
||||
impl ToNativeKeycode for PhysicalKey {
|
||||
fn to_native_keycode(&self) -> i32 {
|
||||
use winit::platform::scancode::PhysicalKeyExtScancode;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
self.to_scancode().map(|evdev| (evdev + 8) as i32).unwrap_or_default()
|
||||
}
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
{
|
||||
self.to_scancode().map(|c| c as i32).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ToVKBits {
|
||||
fn to_vk_bits(&self) -> i32;
|
||||
}
|
||||
|
||||
macro_rules! map_enum {
|
||||
($target:expr, $enum:ident, $( ($code:expr, $variant:ident), )+ ) => {
|
||||
match $target {
|
||||
$(
|
||||
$enum::$variant => $code,
|
||||
)+
|
||||
_ => 0,
|
||||
}
|
||||
};
|
||||
}
|
||||
impl ToVKBits for winit::keyboard::NamedKey {
|
||||
fn to_vk_bits(&self) -> i32 {
|
||||
map_enum!(
|
||||
self,
|
||||
NamedKey,
|
||||
(0x12, Alt),
|
||||
(0xA5, AltGraph),
|
||||
(0x14, CapsLock),
|
||||
(0x11, Control),
|
||||
(0x90, NumLock),
|
||||
(0x91, ScrollLock),
|
||||
(0x10, Shift),
|
||||
(0x5B, Meta),
|
||||
(0x0D, Enter),
|
||||
(0x09, Tab),
|
||||
(0x25, ArrowLeft),
|
||||
(0x26, ArrowUp),
|
||||
(0x27, ArrowRight),
|
||||
(0x28, ArrowDown),
|
||||
(0x23, End),
|
||||
(0x24, Home),
|
||||
(0x22, PageDown),
|
||||
(0x21, PageUp),
|
||||
(0x08, Backspace),
|
||||
(0x0C, Clear),
|
||||
(0xF7, CrSel),
|
||||
(0x2E, Delete),
|
||||
(0xF9, EraseEof),
|
||||
(0xF8, ExSel),
|
||||
(0x2D, Insert),
|
||||
(0x1E, Accept),
|
||||
(0xF6, Attn),
|
||||
(0x03, Cancel),
|
||||
(0x5D, ContextMenu),
|
||||
(0x1B, Escape),
|
||||
(0x2B, Execute),
|
||||
(0x2F, Help),
|
||||
(0x13, Pause),
|
||||
(0xFA, Play),
|
||||
(0x5D, Props),
|
||||
(0x29, Select),
|
||||
(0xFB, ZoomIn),
|
||||
(0xFB, ZoomOut),
|
||||
(0x2C, PrintScreen),
|
||||
(0x5F, Standby),
|
||||
(0x1C, Convert),
|
||||
(0x18, FinalMode),
|
||||
(0x1F, ModeChange),
|
||||
(0x1D, NonConvert),
|
||||
(0xE5, Process),
|
||||
(0x15, HangulMode),
|
||||
(0x19, HanjaMode),
|
||||
(0x17, JunjaMode),
|
||||
(0x15, KanaMode),
|
||||
(0x19, KanjiMode),
|
||||
(0xB0, MediaFastForward),
|
||||
(0xB3, MediaPause),
|
||||
(0xB3, MediaPlay),
|
||||
(0xB3, MediaPlayPause),
|
||||
(0xB1, MediaRewind),
|
||||
(0xB2, MediaStop),
|
||||
(0xB0, MediaTrackNext),
|
||||
(0xB1, MediaTrackPrevious),
|
||||
(0x2A, Print),
|
||||
(0xAE, AudioVolumeDown),
|
||||
(0xAF, AudioVolumeUp),
|
||||
(0xAD, AudioVolumeMute),
|
||||
(0xB6, LaunchApplication1),
|
||||
(0xB7, LaunchApplication2),
|
||||
(0xB4, LaunchMail),
|
||||
(0xB5, LaunchMediaPlayer),
|
||||
(0xB5, LaunchMusicPlayer),
|
||||
(0xA6, BrowserBack),
|
||||
(0xAB, BrowserFavorites),
|
||||
(0xA7, BrowserForward),
|
||||
(0xAC, BrowserHome),
|
||||
(0xA8, BrowserRefresh),
|
||||
(0xAA, BrowserSearch),
|
||||
(0xA9, BrowserStop),
|
||||
(0xFB, ZoomToggle),
|
||||
(0x70, F1),
|
||||
(0x71, F2),
|
||||
(0x72, F3),
|
||||
(0x73, F4),
|
||||
(0x74, F5),
|
||||
(0x75, F6),
|
||||
(0x76, F7),
|
||||
(0x77, F8),
|
||||
(0x78, F9),
|
||||
(0x79, F10),
|
||||
(0x7A, F11),
|
||||
(0x7B, F12),
|
||||
(0x7C, F13),
|
||||
(0x7D, F14),
|
||||
(0x7E, F15),
|
||||
(0x7F, F16),
|
||||
(0x80, F17),
|
||||
(0x81, F18),
|
||||
(0x82, F19),
|
||||
(0x83, F20),
|
||||
(0x84, F21),
|
||||
(0x85, F22),
|
||||
(0x86, F23),
|
||||
(0x87, F24),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! map {
|
||||
($target:expr, $( ($code:expr, $variant:literal), )+ ) => {
|
||||
match $target {
|
||||
$(
|
||||
$variant => $code,
|
||||
)+
|
||||
_ => 0,
|
||||
}
|
||||
};
|
||||
}
|
||||
impl ToVKBits for char {
|
||||
fn to_vk_bits(&self) -> i32 {
|
||||
map!(
|
||||
self,
|
||||
(0x41, 'a'),
|
||||
(0x42, 'b'),
|
||||
(0x43, 'c'),
|
||||
(0x44, 'd'),
|
||||
(0x45, 'e'),
|
||||
(0x46, 'f'),
|
||||
(0x47, 'g'),
|
||||
(0x48, 'h'),
|
||||
(0x49, 'i'),
|
||||
(0x4a, 'j'),
|
||||
(0x4b, 'k'),
|
||||
(0x4c, 'l'),
|
||||
(0x4d, 'm'),
|
||||
(0x4e, 'n'),
|
||||
(0x4f, 'o'),
|
||||
(0x50, 'p'),
|
||||
(0x51, 'q'),
|
||||
(0x52, 'r'),
|
||||
(0x53, 's'),
|
||||
(0x54, 't'),
|
||||
(0x55, 'u'),
|
||||
(0x56, 'v'),
|
||||
(0x57, 'w'),
|
||||
(0x58, 'x'),
|
||||
(0x59, 'y'),
|
||||
(0x5a, 'z'),
|
||||
(0x41, 'A'),
|
||||
(0x42, 'B'),
|
||||
(0x43, 'C'),
|
||||
(0x44, 'D'),
|
||||
(0x45, 'E'),
|
||||
(0x46, 'F'),
|
||||
(0x47, 'G'),
|
||||
(0x48, 'H'),
|
||||
(0x49, 'I'),
|
||||
(0x4a, 'J'),
|
||||
(0x4b, 'K'),
|
||||
(0x4c, 'L'),
|
||||
(0x4d, 'M'),
|
||||
(0x4e, 'N'),
|
||||
(0x4f, 'O'),
|
||||
(0x50, 'P'),
|
||||
(0x51, 'Q'),
|
||||
(0x52, 'R'),
|
||||
(0x53, 'S'),
|
||||
(0x54, 'T'),
|
||||
(0x55, 'U'),
|
||||
(0x56, 'V'),
|
||||
(0x57, 'W'),
|
||||
(0x58, 'X'),
|
||||
(0x59, 'Y'),
|
||||
(0x5a, 'Z'),
|
||||
(0x31, '1'),
|
||||
(0x32, '2'),
|
||||
(0x33, '3'),
|
||||
(0x34, '4'),
|
||||
(0x35, '5'),
|
||||
(0x36, '6'),
|
||||
(0x37, '7'),
|
||||
(0x38, '8'),
|
||||
(0x39, '9'),
|
||||
(0x30, '0'),
|
||||
(0x31, '!'),
|
||||
(0x32, '@'),
|
||||
(0x33, '#'),
|
||||
(0x34, '$'),
|
||||
(0x35, '%'),
|
||||
(0x36, '^'),
|
||||
(0x37, '&'),
|
||||
(0x38, '*'),
|
||||
(0x39, '('),
|
||||
(0x30, ')'),
|
||||
(0xC0, '`'),
|
||||
(0xC0, '~'),
|
||||
(0xBD, '-'),
|
||||
(0xBD, '_'),
|
||||
(0xBB, '='),
|
||||
(0xBB, '+'),
|
||||
(0xDB, '['),
|
||||
(0xDB, '{'),
|
||||
(0xDD, ']'),
|
||||
(0xDD, '}'),
|
||||
(0xDC, '\\'),
|
||||
(0xDC, '|'),
|
||||
(0xBA, ';'),
|
||||
(0xBA, ':'),
|
||||
(0xBC, ','),
|
||||
(0xBC, '<'),
|
||||
(0xBE, '.'),
|
||||
(0xBE, '>'),
|
||||
(0xDE, '\''),
|
||||
(0xDE, '"'),
|
||||
(0xBF, '/'),
|
||||
(0xBF, '?'),
|
||||
(0x20, ' '),
|
||||
)
|
||||
}
|
||||
}
|
||||
256
desktop/ui/src/input/state.rs
Normal file
256
desktop/ui/src/input/state.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
use cef::sys::cef_event_flags_t;
|
||||
use std::time::Instant;
|
||||
use winit::dpi::PhysicalPosition;
|
||||
use winit::event::{ElementState, MouseButton};
|
||||
use winit::keyboard::{Key, KeyLocation, ModifiersState, NamedKey};
|
||||
|
||||
use super::MouseData;
|
||||
use crate::consts::{MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct InputState {
|
||||
modifiers: ModifiersState,
|
||||
mouse_position: MousePosition,
|
||||
mouse_state: MouseState,
|
||||
mouse_click_tracker: ClickTracker,
|
||||
}
|
||||
impl InputState {
|
||||
pub(crate) fn modifiers_changed(&mut self, modifiers: &ModifiersState) {
|
||||
self.modifiers = *modifiers;
|
||||
}
|
||||
|
||||
pub(crate) fn modifiers_apply_key_event(&mut self, key: &Key, state: &ElementState) {
|
||||
let bits = match key {
|
||||
Key::Named(NamedKey::Shift) => ModifiersState::SHIFT,
|
||||
Key::Named(NamedKey::Control) => ModifiersState::CONTROL,
|
||||
Key::Named(NamedKey::Alt) => ModifiersState::ALT,
|
||||
Key::Named(NamedKey::Meta) => ModifiersState::META,
|
||||
_ => return,
|
||||
};
|
||||
let is_pressed = matches!(state, ElementState::Pressed);
|
||||
self.modifiers.set(bits, is_pressed);
|
||||
}
|
||||
|
||||
pub(crate) fn cursor_move(&mut self, position: &PhysicalPosition<f64>) -> bool {
|
||||
let new = position.into();
|
||||
if self.mouse_position == new {
|
||||
return false;
|
||||
}
|
||||
self.mouse_position = new;
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn mouse_input(&mut self, button: &MouseButton, state: &ElementState) -> ClickCount {
|
||||
self.mouse_state.update(button, state);
|
||||
self.mouse_click_tracker.input(button, state, self.mouse_position)
|
||||
}
|
||||
|
||||
pub(crate) fn cef_modifiers(&self, location: &KeyLocation, is_repeat: bool) -> CefModifiers {
|
||||
CefModifiers::new(self, location, is_repeat)
|
||||
}
|
||||
|
||||
pub(crate) fn cef_mouse_modifiers(&self) -> CefModifiers {
|
||||
self.cef_modifiers(&KeyLocation::Standard, false)
|
||||
}
|
||||
|
||||
pub(crate) fn mouse_data(&self) -> MouseData {
|
||||
MouseData {
|
||||
x: self.mouse_position.x,
|
||||
y: self.mouse_position.y,
|
||||
modifiers: self.cef_mouse_modifiers().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Eq, PartialEq)]
|
||||
pub(crate) struct MousePosition {
|
||||
x: i32,
|
||||
y: i32,
|
||||
}
|
||||
impl From<&PhysicalPosition<f64>> for MousePosition {
|
||||
fn from(position: &PhysicalPosition<f64>) -> Self {
|
||||
Self {
|
||||
x: position.x as i32,
|
||||
y: position.y as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct MouseState {
|
||||
left: bool,
|
||||
right: bool,
|
||||
middle: bool,
|
||||
}
|
||||
impl MouseState {
|
||||
pub(crate) fn update(&mut self, button: &MouseButton, state: &ElementState) {
|
||||
match state {
|
||||
ElementState::Pressed => match button {
|
||||
MouseButton::Left => self.left = true,
|
||||
MouseButton::Right => self.right = true,
|
||||
MouseButton::Middle => self.middle = true,
|
||||
_ => {}
|
||||
},
|
||||
ElementState::Released => match button {
|
||||
MouseButton::Left => self.left = false,
|
||||
MouseButton::Right => self.right = false,
|
||||
MouseButton::Middle => self.middle = false,
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClickTracker {
|
||||
left: Option<ClickRecord>,
|
||||
middle: Option<ClickRecord>,
|
||||
right: Option<ClickRecord>,
|
||||
}
|
||||
impl ClickTracker {
|
||||
fn input(&mut self, button: &MouseButton, state: &ElementState, position: MousePosition) -> ClickCount {
|
||||
let record = match button {
|
||||
MouseButton::Left => &mut self.left,
|
||||
MouseButton::Right => &mut self.right,
|
||||
MouseButton::Middle => &mut self.middle,
|
||||
_ => return ClickCount::Single,
|
||||
};
|
||||
|
||||
let Some(record) = record else {
|
||||
*record = Some(ClickRecord {
|
||||
down_position: position,
|
||||
up_position: position,
|
||||
..Default::default()
|
||||
});
|
||||
return ClickCount::Single;
|
||||
};
|
||||
|
||||
let now = Instant::now();
|
||||
let within_time = now.saturating_duration_since(record.time) <= MULTICLICK_TIMEOUT;
|
||||
|
||||
let (prev_count, prev_position) = match state {
|
||||
ElementState::Pressed => (record.down_count, record.down_position),
|
||||
ElementState::Released => (record.up_count, record.up_position),
|
||||
};
|
||||
|
||||
let dx = position.x.abs_diff(prev_position.x) as usize;
|
||||
let dy = position.y.abs_diff(prev_position.y) as usize;
|
||||
let within_dist = dx <= MULTICLICK_ALLOWED_TRAVEL && dy <= MULTICLICK_ALLOWED_TRAVEL;
|
||||
|
||||
let count = match (prev_count, within_time, within_dist) {
|
||||
(ClickCount::Single, true, true) => ClickCount::Double,
|
||||
(ClickCount::Double, true, true) => ClickCount::Triple,
|
||||
(ClickCount::Triple, true, true) => ClickCount::Double,
|
||||
_ => ClickCount::Single,
|
||||
};
|
||||
|
||||
record.time = now;
|
||||
|
||||
match state {
|
||||
ElementState::Pressed => {
|
||||
record.down_position = position;
|
||||
record.down_count = count;
|
||||
}
|
||||
ElementState::Released => {
|
||||
record.up_position = position;
|
||||
record.up_count = count;
|
||||
}
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Default)]
|
||||
pub(crate) enum ClickCount {
|
||||
#[default]
|
||||
Single,
|
||||
Double,
|
||||
Triple,
|
||||
}
|
||||
impl From<ClickCount> for i32 {
|
||||
fn from(count: ClickCount) -> i32 {
|
||||
match count {
|
||||
ClickCount::Single => 1,
|
||||
ClickCount::Double => 2,
|
||||
ClickCount::Triple => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ClickRecord {
|
||||
time: Instant,
|
||||
down_position: MousePosition,
|
||||
up_position: MousePosition,
|
||||
down_count: ClickCount,
|
||||
up_count: ClickCount,
|
||||
}
|
||||
|
||||
impl Default for ClickRecord {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
time: Instant::now(),
|
||||
down_position: Default::default(),
|
||||
up_position: Default::default(),
|
||||
down_count: Default::default(),
|
||||
up_count: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct CefModifiers(cef_event_flags_t);
|
||||
impl CefModifiers {
|
||||
fn new(input_state: &InputState, location: &KeyLocation, is_repeat: bool) -> Self {
|
||||
let mut inner = cef_event_flags_t::EVENTFLAG_NONE;
|
||||
|
||||
if input_state.modifiers.shift_key() {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_SHIFT_DOWN;
|
||||
}
|
||||
if input_state.modifiers.control_key() {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN;
|
||||
}
|
||||
if input_state.modifiers.alt_key() {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_ALT_DOWN;
|
||||
}
|
||||
if input_state.modifiers.meta_key() {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_COMMAND_DOWN;
|
||||
}
|
||||
|
||||
if input_state.mouse_state.left {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON;
|
||||
}
|
||||
if input_state.mouse_state.right {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON;
|
||||
}
|
||||
if input_state.mouse_state.middle {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON;
|
||||
}
|
||||
|
||||
if is_repeat {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_IS_REPEAT;
|
||||
}
|
||||
|
||||
inner |= match location {
|
||||
KeyLocation::Left => cef_event_flags_t::EVENTFLAG_IS_LEFT,
|
||||
KeyLocation::Right => cef_event_flags_t::EVENTFLAG_IS_RIGHT,
|
||||
KeyLocation::Numpad => cef_event_flags_t::EVENTFLAG_IS_KEY_PAD,
|
||||
KeyLocation::Standard => cef_event_flags_t::EVENTFLAG_NONE,
|
||||
};
|
||||
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
pub(super) const PINCH_MODIFIERS: Self = Self(cef_event_flags_t(
|
||||
cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0 | cef_event_flags_t::EVENTFLAG_PRECISION_SCROLLING_DELTA.0,
|
||||
));
|
||||
}
|
||||
|
||||
impl From<CefModifiers> for u32 {
|
||||
fn from(val: CefModifiers) -> Self {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
return val.0.0;
|
||||
#[cfg(target_os = "windows")]
|
||||
return val.0.0 as u32;
|
||||
}
|
||||
}
|
||||
25
desktop/ui/src/internal.rs
Normal file
25
desktop/ui/src/internal.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
mod browser_process_app;
|
||||
mod browser_process_client;
|
||||
mod browser_process_handler;
|
||||
|
||||
mod render_process_app;
|
||||
mod render_process_handler;
|
||||
mod render_process_v8_handler;
|
||||
|
||||
mod context_menu_handler;
|
||||
mod display_handler;
|
||||
mod life_span_handler;
|
||||
mod load_handler;
|
||||
mod request_handler;
|
||||
mod resource_handler;
|
||||
mod resource_request_handler;
|
||||
mod scheme_handler_factory;
|
||||
|
||||
pub(super) mod render_handler;
|
||||
|
||||
pub(super) mod task;
|
||||
|
||||
pub(super) use browser_process_app::BrowserProcessAppImpl;
|
||||
pub(super) use browser_process_client::BrowserProcessClientImpl;
|
||||
pub(super) use render_process_app::RenderProcessAppImpl;
|
||||
pub(super) use scheme_handler_factory::SchemeHandlerFactoryImpl;
|
||||
134
desktop/ui/src/internal/browser_process_app.rs
Normal file
134
desktop/ui/src/internal/browser_process_app.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
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 super::browser_process_handler::BrowserProcessHandlerImpl;
|
||||
use super::scheme_handler_factory::register_schemes;
|
||||
|
||||
pub(crate) struct BrowserProcessAppImpl {
|
||||
object: *mut RcImpl<_cef_app_t, Self>,
|
||||
accelerated_paint: bool,
|
||||
}
|
||||
impl BrowserProcessAppImpl {
|
||||
pub(crate) fn new(accelerated_paint: bool) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
accelerated_paint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplApp for BrowserProcessAppImpl {
|
||||
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
|
||||
Some(BrowserProcessHandler::new(BrowserProcessHandlerImpl::new()))
|
||||
}
|
||||
|
||||
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
|
||||
register_schemes(registrar);
|
||||
}
|
||||
|
||||
fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) {
|
||||
if let Some(cmd) = command_line {
|
||||
cmd.append_switch_with_value(Some(&"renderer-process-limit".into()), Some(&"1".into()));
|
||||
cmd.append_switch_with_value(Some(&"password-store".into()), Some(&"basic".into()));
|
||||
cmd.append_switch_with_value(Some(&"disk-cache-size".into()), Some(&"0".into()));
|
||||
cmd.append_switch(Some(&"no-sandbox".into()));
|
||||
cmd.append_switch(Some(&"no-first-run".into()));
|
||||
cmd.append_switch(Some(&"noerrdialogs".into()));
|
||||
cmd.append_switch(Some(&"no-default-browser-check".into()));
|
||||
cmd.append_switch(Some(&"mute-audio".into()));
|
||||
cmd.append_switch(Some(&"use-fake-device-for-media-stream".into()));
|
||||
cmd.append_switch(Some(&"incognito".into()));
|
||||
cmd.append_switch(Some(&"disable-sync".into()));
|
||||
cmd.append_switch(Some(&"disable-file-system".into()));
|
||||
cmd.append_switch(Some(&"disable-component-update".into()));
|
||||
cmd.append_switch(Some(&"disable-geolocation".into()));
|
||||
cmd.append_switch(Some(&"disable-notifications".into()));
|
||||
cmd.append_switch(Some(&"disable-background-networking".into()));
|
||||
cmd.append_switch(Some(&"disable-default-apps".into()));
|
||||
cmd.append_switch(Some(&"disable-breakpad".into()));
|
||||
cmd.append_switch_with_value(Some(&"disable-blink-features".into()), Some(&"WebBluetooth,WebUSB,Serial".into()));
|
||||
|
||||
let extra_disabled_features = ["OptimizationHints", "OnDeviceModelService", "TranslateUI"];
|
||||
let disabled_features_switch = Some(&"disable-features".into());
|
||||
let mut disabled_features: Vec<String> = CefString::from(&cmd.switch_value(disabled_features_switch))
|
||||
.to_string()
|
||||
.split(',')
|
||||
.filter(|feature| !feature.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect();
|
||||
disabled_features.extend(extra_disabled_features.into_iter().map(ToOwned::to_owned));
|
||||
cmd.append_switch_with_value(disabled_features_switch, Some(&disabled_features.join(",").as_str().into()));
|
||||
|
||||
if self.accelerated_paint {
|
||||
cmd.append_switch(Some(&"enable-gpu".into()));
|
||||
cmd.append_switch(Some(&"enable-gpu-compositing".into()));
|
||||
cmd.append_switch(Some(&"enable-begin-frame-scheduling".into()));
|
||||
cmd.append_switch(Some(&"off-screen-rendering-enabled".into()));
|
||||
cmd.append_switch(Some(&"enable-accelerated-2d-canvas".into()));
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
cmd.append_switch_with_value(Some(&"use-angle".into()), Some(&"gl-egl".into()));
|
||||
|
||||
let use_wayland = std::env::var("WAYLAND_DISPLAY")
|
||||
.ok()
|
||||
.filter(|var| !var.is_empty())
|
||||
.or_else(|| std::env::var("WAYLAND_SOCKET").ok())
|
||||
.filter(|var| !var.is_empty())
|
||||
.is_some();
|
||||
if use_wayland {
|
||||
cmd.append_switch_with_value(Some(&"ozone-platform".into()), Some(&"wayland".into()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cmd.append_switch(Some(&"disable-gpu".into()));
|
||||
cmd.append_switch(Some(&"disable-gpu-compositing".into()));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Hide user prompt asking for keychain access
|
||||
cmd.append_switch(Some(&"use-mock-keychain".into()));
|
||||
}
|
||||
|
||||
// Enable browser debugging via environment variable
|
||||
if let Some(env) = std::env::var("GRAPHITE_BROWSER_DEBUG_PORT").ok()
|
||||
&& let Some(port) = env.parse::<u16>().ok()
|
||||
{
|
||||
cmd.append_switch_with_value(Some(&"remote-debugging-port".into()), Some(&port.to_string().as_str().into()));
|
||||
cmd.append_switch_with_value(Some(&"remote-allow-origins".into()), Some(&"*".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_app_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for BrowserProcessAppImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
accelerated_paint: self.accelerated_paint,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for BrowserProcessAppImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapApp for BrowserProcessAppImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
121
desktop/ui/src/internal/browser_process_client.rs
Normal file
121
desktop/ui/src/internal/browser_process_client.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_client_t, cef_base_ref_counted_t};
|
||||
use cef::{ContextMenuHandler, DisplayHandler, ImplClient, LifeSpanHandler, LoadHandler, RenderHandler, RequestHandler, WrapClient};
|
||||
|
||||
use crate::delegate::BrowserDelegate;
|
||||
use crate::frames::FrameStreamer;
|
||||
use crate::ipc::{MessageType, UnpackMessage, UnpackedMessage};
|
||||
|
||||
use super::context_menu_handler::ContextMenuHandlerImpl;
|
||||
use super::display_handler::DisplayHandlerImpl;
|
||||
use super::life_span_handler::LifeSpanHandlerImpl;
|
||||
use super::load_handler::LoadHandlerImpl;
|
||||
use super::render_handler::RenderHandlerImpl;
|
||||
use super::request_handler::RequestHandlerImpl;
|
||||
|
||||
pub(crate) struct BrowserProcessClientImpl {
|
||||
object: *mut RcImpl<_cef_client_t, Self>,
|
||||
delegate: BrowserDelegate,
|
||||
load_handler: LoadHandler,
|
||||
render_handler: RenderHandler,
|
||||
display_handler: DisplayHandler,
|
||||
request_handler: RequestHandler,
|
||||
}
|
||||
impl BrowserProcessClientImpl {
|
||||
pub(crate) fn new(delegate: &BrowserDelegate, frames: FrameStreamer) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
delegate: delegate.clone(),
|
||||
load_handler: LoadHandler::new(LoadHandlerImpl::new(delegate.clone())),
|
||||
render_handler: RenderHandler::new(RenderHandlerImpl::new(delegate.clone(), frames)),
|
||||
display_handler: DisplayHandler::new(DisplayHandlerImpl::new(delegate.clone())),
|
||||
request_handler: RequestHandler::new(RequestHandlerImpl::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplClient for BrowserProcessClientImpl {
|
||||
fn on_process_message_received(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
_frame: Option<&mut cef::Frame>,
|
||||
_source_process: cef::ProcessId,
|
||||
message: Option<&mut cef::ProcessMessage>,
|
||||
) -> std::ffi::c_int {
|
||||
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
|
||||
match unpacked_message {
|
||||
Some(UnpackedMessage {
|
||||
message_type: MessageType::Initialized,
|
||||
data: _,
|
||||
}) => self.delegate.initialized_web_communication(),
|
||||
Some(UnpackedMessage {
|
||||
message_type: MessageType::SendToNative,
|
||||
data,
|
||||
}) => self.delegate.receive_web_message(data),
|
||||
|
||||
_ => {
|
||||
tracing::error!("Unexpected message type received in browser process");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
fn load_handler(&self) -> Option<cef::LoadHandler> {
|
||||
Some(self.load_handler.clone())
|
||||
}
|
||||
|
||||
fn render_handler(&self) -> Option<RenderHandler> {
|
||||
Some(self.render_handler.clone())
|
||||
}
|
||||
|
||||
fn life_span_handler(&self) -> Option<cef::LifeSpanHandler> {
|
||||
Some(LifeSpanHandler::new(LifeSpanHandlerImpl::new()))
|
||||
}
|
||||
|
||||
fn display_handler(&self) -> Option<cef::DisplayHandler> {
|
||||
Some(self.display_handler.clone())
|
||||
}
|
||||
|
||||
fn request_handler(&self) -> Option<cef::RequestHandler> {
|
||||
Some(self.request_handler.clone())
|
||||
}
|
||||
|
||||
fn context_menu_handler(&self) -> Option<cef::ContextMenuHandler> {
|
||||
Some(ContextMenuHandler::new(ContextMenuHandlerImpl::new()))
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_client_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for BrowserProcessClientImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
delegate: self.delegate.clone(),
|
||||
load_handler: self.load_handler.clone(),
|
||||
render_handler: self.render_handler.clone(),
|
||||
display_handler: self.display_handler.clone(),
|
||||
request_handler: self.request_handler.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for BrowserProcessClientImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapClient for BrowserProcessClientImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_client_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
45
desktop/ui/src/internal/browser_process_handler.rs
Normal file
45
desktop/ui/src/internal/browser_process_handler.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_browser_process_handler_t, cef_base_ref_counted_t, cef_browser_process_handler_t};
|
||||
use cef::{CefString, ImplBrowserProcessHandler, WrapBrowserProcessHandler};
|
||||
|
||||
pub(crate) struct BrowserProcessHandlerImpl {
|
||||
object: *mut RcImpl<cef_browser_process_handler_t, Self>,
|
||||
}
|
||||
impl BrowserProcessHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplBrowserProcessHandler for BrowserProcessHandlerImpl {
|
||||
fn on_already_running_app_relaunch(&self, _command_line: Option<&mut cef::CommandLine>, _current_directory: Option<&CefString>) -> std::ffi::c_int {
|
||||
1 // Return 1 to prevent default behavior of opening a empty browser window
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_browser_process_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for BrowserProcessHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for BrowserProcessHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapBrowserProcessHandler for BrowserProcessHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_browser_process_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
66
desktop/ui/src/internal/context_menu_handler.rs
Normal file
66
desktop/ui/src/internal/context_menu_handler.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_context_menu_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{ImplContextMenuHandler, WrapContextMenuHandler};
|
||||
|
||||
pub(crate) struct ContextMenuHandlerImpl {
|
||||
object: *mut RcImpl<_cef_context_menu_handler_t, Self>,
|
||||
}
|
||||
impl ContextMenuHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplContextMenuHandler for ContextMenuHandlerImpl {
|
||||
fn run_context_menu(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
_frame: Option<&mut cef::Frame>,
|
||||
_params: Option<&mut cef::ContextMenuParams>,
|
||||
_model: Option<&mut cef::MenuModel>,
|
||||
_callback: Option<&mut cef::RunContextMenuCallback>,
|
||||
) -> std::ffi::c_int {
|
||||
// Prevent context menu
|
||||
1
|
||||
}
|
||||
|
||||
fn run_quick_menu(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
_frame: Option<&mut cef::Frame>,
|
||||
_location: Option<&cef::Point>,
|
||||
_size: Option<&cef::Size>,
|
||||
_edit_state_flags: cef::QuickMenuEditStateFlags,
|
||||
_callback: Option<&mut cef::RunQuickMenuCallback>,
|
||||
) -> std::ffi::c_int {
|
||||
// Prevent quick menu
|
||||
1
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_context_menu_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ContextMenuHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for ContextMenuHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapContextMenuHandler for ContextMenuHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_context_menu_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
155
desktop/ui/src/internal/display_handler.rs
Normal file
155
desktop/ui/src/internal/display_handler.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_display_handler_t, cef_base_ref_counted_t, cef_cursor_type_t::*, cef_log_severity_t::*};
|
||||
use cef::{CefString, ImplDisplayHandler, Point, Size, WrapDisplayHandler};
|
||||
use winit::cursor::CursorIcon;
|
||||
|
||||
use crate::delegate::BrowserDelegate;
|
||||
|
||||
pub(crate) struct DisplayHandlerImpl {
|
||||
object: *mut RcImpl<_cef_display_handler_t, Self>,
|
||||
delegate: BrowserDelegate,
|
||||
}
|
||||
|
||||
impl DisplayHandlerImpl {
|
||||
pub fn new(delegate: BrowserDelegate) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
delegate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
type CefCursorHandle = cef::CursorHandle;
|
||||
#[cfg(target_os = "macos")]
|
||||
type CefCursorHandle = *mut u8;
|
||||
|
||||
impl ImplDisplayHandler for DisplayHandlerImpl {
|
||||
fn on_cursor_change(&self, _browser: Option<&mut cef::Browser>, _cursor: CefCursorHandle, cursor_type: cef::CursorType, custom_cursor_info: Option<&cef::CursorInfo>) -> std::ffi::c_int {
|
||||
if let Some(custom_cursor_info) = custom_cursor_info {
|
||||
let Size { width, height } = custom_cursor_info.size;
|
||||
let Point { x: hotspot_x, y: hotspot_y } = custom_cursor_info.hotspot;
|
||||
let buffer_size = (width * height * 4) as usize;
|
||||
let buffer_ptr = custom_cursor_info.buffer as *const u8;
|
||||
|
||||
if !buffer_ptr.is_null() && buffer_ptr.align_offset(std::mem::align_of::<u8>()) == 0 {
|
||||
let buffer = unsafe { std::slice::from_raw_parts(buffer_ptr, buffer_size) }.to_vec();
|
||||
self.delegate.cursor_change(crate::Cursor::Custom {
|
||||
rgba: buffer,
|
||||
width: width as u16,
|
||||
height: height as u16,
|
||||
hotspot_x: hotspot_x as u16,
|
||||
hotspot_y: hotspot_y as u16,
|
||||
});
|
||||
return 1; // We handled the cursor change.
|
||||
}
|
||||
}
|
||||
|
||||
let cursor = match cursor_type.into() {
|
||||
CT_POINTER => CursorIcon::Default,
|
||||
CT_CROSS => CursorIcon::Crosshair,
|
||||
CT_HAND => CursorIcon::Pointer,
|
||||
CT_IBEAM => CursorIcon::Text,
|
||||
CT_WAIT => CursorIcon::Wait,
|
||||
CT_HELP => CursorIcon::Help,
|
||||
CT_EASTRESIZE => CursorIcon::EResize,
|
||||
CT_NORTHRESIZE => CursorIcon::NResize,
|
||||
CT_NORTHEASTRESIZE => CursorIcon::NeResize,
|
||||
CT_NORTHWESTRESIZE => CursorIcon::NwResize,
|
||||
CT_SOUTHRESIZE => CursorIcon::SResize,
|
||||
CT_SOUTHEASTRESIZE => CursorIcon::SeResize,
|
||||
CT_SOUTHWESTRESIZE => CursorIcon::SwResize,
|
||||
CT_WESTRESIZE => CursorIcon::WResize,
|
||||
CT_NORTHSOUTHRESIZE => CursorIcon::NsResize,
|
||||
CT_EASTWESTRESIZE => CursorIcon::EwResize,
|
||||
CT_NORTHEASTSOUTHWESTRESIZE => CursorIcon::NeswResize,
|
||||
CT_NORTHWESTSOUTHEASTRESIZE => CursorIcon::NwseResize,
|
||||
CT_COLUMNRESIZE => CursorIcon::ColResize,
|
||||
CT_ROWRESIZE => CursorIcon::RowResize,
|
||||
CT_MIDDLEPANNING => CursorIcon::AllScroll,
|
||||
CT_EASTPANNING => CursorIcon::AllScroll,
|
||||
CT_NORTHPANNING => CursorIcon::AllScroll,
|
||||
CT_NORTHEASTPANNING => CursorIcon::AllScroll,
|
||||
CT_NORTHWESTPANNING => CursorIcon::AllScroll,
|
||||
CT_SOUTHPANNING => CursorIcon::AllScroll,
|
||||
CT_SOUTHEASTPANNING => CursorIcon::AllScroll,
|
||||
CT_SOUTHWESTPANNING => CursorIcon::AllScroll,
|
||||
CT_WESTPANNING => CursorIcon::AllScroll,
|
||||
CT_MOVE => CursorIcon::Move,
|
||||
CT_VERTICALTEXT => CursorIcon::VerticalText,
|
||||
CT_CELL => CursorIcon::Cell,
|
||||
CT_CONTEXTMENU => CursorIcon::ContextMenu,
|
||||
CT_ALIAS => CursorIcon::Alias,
|
||||
CT_PROGRESS => CursorIcon::Progress,
|
||||
CT_NODROP => CursorIcon::NoDrop,
|
||||
CT_COPY => CursorIcon::Copy,
|
||||
CT_NOTALLOWED => CursorIcon::NotAllowed,
|
||||
CT_ZOOMIN => CursorIcon::ZoomIn,
|
||||
CT_ZOOMOUT => CursorIcon::ZoomOut,
|
||||
CT_GRAB => CursorIcon::Grab,
|
||||
CT_GRABBING => CursorIcon::Grabbing,
|
||||
CT_MIDDLE_PANNING_VERTICAL => CursorIcon::AllScroll,
|
||||
CT_MIDDLE_PANNING_HORIZONTAL => CursorIcon::AllScroll,
|
||||
CT_DND_NONE => CursorIcon::Default,
|
||||
CT_DND_MOVE => CursorIcon::Move,
|
||||
CT_DND_COPY => CursorIcon::Copy,
|
||||
CT_DND_LINK => CursorIcon::Alias,
|
||||
CT_NUM_VALUES => CursorIcon::Default,
|
||||
CT_NONE => {
|
||||
self.delegate.cursor_change(crate::Cursor::None);
|
||||
return 1; // We handled the cursor change.
|
||||
}
|
||||
_ => CursorIcon::Default,
|
||||
};
|
||||
|
||||
self.delegate.cursor_change(cursor.into());
|
||||
|
||||
1 // We handled the cursor change.
|
||||
}
|
||||
|
||||
fn on_console_message(&self, _browser: Option<&mut cef::Browser>, level: cef::LogSeverity, message: Option<&CefString>, source: Option<&CefString>, line: std::ffi::c_int) -> std::ffi::c_int {
|
||||
let message = message.map(|m| m.to_string()).unwrap_or_default();
|
||||
let source = source.map(|s| s.to_string()).unwrap_or_default();
|
||||
let line = line as i64;
|
||||
let browser_source = format!("{source}:{line}");
|
||||
static BROWSER: &str = "browser";
|
||||
match level.as_ref() {
|
||||
LOGSEVERITY_FATAL | LOGSEVERITY_ERROR => tracing::error!(target: BROWSER, "{browser_source} {message}"),
|
||||
LOGSEVERITY_WARNING => tracing::warn!(target: BROWSER, "{browser_source} {message}"),
|
||||
LOGSEVERITY_INFO => tracing::info!(target: BROWSER, "{browser_source} {message}"),
|
||||
LOGSEVERITY_DEFAULT | LOGSEVERITY_VERBOSE => tracing::debug!(target: BROWSER, "{browser_source} {message}"),
|
||||
_ => tracing::trace!(target: BROWSER, "{browser_source} {message}"),
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_display_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for DisplayHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
delegate: self.delegate.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for DisplayHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapDisplayHandler for DisplayHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_display_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
64
desktop/ui/src/internal/life_span_handler.rs
Normal file
64
desktop/ui/src/internal/life_span_handler.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_life_span_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{ImplLifeSpanHandler, WrapLifeSpanHandler};
|
||||
|
||||
pub(crate) struct LifeSpanHandlerImpl {
|
||||
object: *mut RcImpl<_cef_life_span_handler_t, Self>,
|
||||
}
|
||||
impl LifeSpanHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplLifeSpanHandler for LifeSpanHandlerImpl {
|
||||
fn on_before_popup(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
_frame: Option<&mut cef::Frame>,
|
||||
_popup_id: std::ffi::c_int,
|
||||
target_url: Option<&cef::CefString>,
|
||||
_target_frame_name: Option<&cef::CefString>,
|
||||
_target_disposition: cef::WindowOpenDisposition,
|
||||
_user_gesture: std::ffi::c_int,
|
||||
_popup_features: Option<&cef::PopupFeatures>,
|
||||
_window_info: Option<&mut cef::WindowInfo>,
|
||||
_client: Option<&mut Option<cef::Client>>,
|
||||
_settings: Option<&mut cef::BrowserSettings>,
|
||||
_extra_info: Option<&mut Option<cef::DictionaryValue>>,
|
||||
_no_javascript_access: Option<&mut std::ffi::c_int>,
|
||||
) -> std::ffi::c_int {
|
||||
let target = target_url.map(|url| url.to_string()).unwrap_or("unknown".to_string());
|
||||
tracing::error!("Browser tried to open a popup at URL: {}", target);
|
||||
|
||||
// Deny any popup by returning 1
|
||||
1
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_life_span_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for LifeSpanHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for LifeSpanHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapLifeSpanHandler for LifeSpanHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_life_span_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
60
desktop/ui/src/internal/load_handler.rs
Normal file
60
desktop/ui/src/internal/load_handler.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_load_handler_t, cef_base_ref_counted_t, cef_load_handler_t};
|
||||
use cef::{ImplBrowser, ImplBrowserHost, ImplLoadHandler, WrapLoadHandler};
|
||||
|
||||
use crate::delegate::BrowserDelegate;
|
||||
|
||||
pub(crate) struct LoadHandlerImpl {
|
||||
object: *mut RcImpl<cef_load_handler_t, Self>,
|
||||
delegate: BrowserDelegate,
|
||||
}
|
||||
impl LoadHandlerImpl {
|
||||
pub(crate) fn new(delegate: BrowserDelegate) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
delegate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplLoadHandler for LoadHandlerImpl {
|
||||
fn on_loading_state_change(&self, browser: Option<&mut cef::Browser>, is_loading: std::ffi::c_int, _can_go_back: std::ffi::c_int, _can_go_forward: std::ffi::c_int) {
|
||||
let view_info = self.delegate.view_info();
|
||||
|
||||
if let Some(browser) = browser
|
||||
&& is_loading == 0
|
||||
{
|
||||
browser.host().unwrap().set_zoom_level(view_info.zoom());
|
||||
}
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_load_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for LoadHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
delegate: self.delegate.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for LoadHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapLoadHandler for LoadHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_load_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
92
desktop/ui/src/internal/render_handler.rs
Normal file
92
desktop/ui/src/internal/render_handler.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_render_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{Browser, ImplRenderHandler, PaintElementType, Rect, WrapRenderHandler};
|
||||
|
||||
use crate::delegate::BrowserDelegate;
|
||||
use crate::frames::FrameStreamer;
|
||||
|
||||
pub(crate) struct RenderHandlerImpl {
|
||||
object: *mut RcImpl<_cef_render_handler_t, Self>,
|
||||
delegate: BrowserDelegate,
|
||||
frames: FrameStreamer,
|
||||
}
|
||||
impl RenderHandlerImpl {
|
||||
pub(crate) fn new(delegate: BrowserDelegate, frames: FrameStreamer) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
delegate,
|
||||
frames,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplRenderHandler for RenderHandlerImpl {
|
||||
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
|
||||
if let Some(rect) = rect {
|
||||
let view_info = self.delegate.view_info();
|
||||
*rect = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: view_info.width() as i32,
|
||||
height: view_info.height() as i32,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn on_paint(&self, _browser: Option<&mut Browser>, type_: PaintElementType, _dirty_rects: Option<&[Rect]>, buffer: *const u8, width: std::ffi::c_int, height: std::ffi::c_int) {
|
||||
if type_ != PaintElementType::default() {
|
||||
return;
|
||||
}
|
||||
|
||||
let buffer_size = (width * height * 4) as usize;
|
||||
let buffer_slice = unsafe { std::slice::from_raw_parts(buffer, buffer_size) };
|
||||
|
||||
self.frames.stage_buffer(buffer_slice, width as u32, height as u32);
|
||||
self.frames.publish();
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
fn on_accelerated_paint(&self, _browser: Option<&mut Browser>, type_: PaintElementType, _dirty_rects: Option<&[Rect]>, info: Option<&cef::AcceleratedPaintInfo>) {
|
||||
if type_ != PaintElementType::default() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(info) = info else {
|
||||
tracing::error!("Accelerated paint callback received no info about the painted frame");
|
||||
return;
|
||||
};
|
||||
self.frames.stage_texture(info);
|
||||
self.frames.publish();
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_render_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RenderHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
delegate: self.delegate.clone(),
|
||||
frames: self.frames.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for RenderHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapRenderHandler for RenderHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
59
desktop/ui/src/internal/render_process_app.rs
Normal file
59
desktop/ui/src/internal/render_process_app.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
|
||||
use cef::{App, ImplApp, RenderProcessHandler, SchemeRegistrar, WrapApp};
|
||||
|
||||
use super::render_process_handler::RenderProcessHandlerImpl;
|
||||
use super::scheme_handler_factory::register_schemes;
|
||||
|
||||
pub(crate) struct RenderProcessAppImpl {
|
||||
object: *mut RcImpl<_cef_app_t, Self>,
|
||||
render_process_handler: RenderProcessHandler,
|
||||
}
|
||||
impl RenderProcessAppImpl {
|
||||
pub(crate) fn app() -> App {
|
||||
App::new(Self {
|
||||
object: std::ptr::null_mut(),
|
||||
render_process_handler: RenderProcessHandler::new(RenderProcessHandlerImpl::new()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplApp for RenderProcessAppImpl {
|
||||
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
|
||||
register_schemes(registrar);
|
||||
}
|
||||
|
||||
fn render_process_handler(&self) -> Option<RenderProcessHandler> {
|
||||
Some(self.render_process_handler.clone())
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_app_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RenderProcessAppImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
render_process_handler: self.render_process_handler.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for RenderProcessAppImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapApp for RenderProcessAppImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
127
desktop/ui/src/internal/render_process_handler.rs
Normal file
127
desktop/ui/src/internal/render_process_handler.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use cef::rc::{ConvertReturnValue, Rc, RcImpl};
|
||||
use cef::sys::{_cef_render_process_handler_t, cef_base_ref_counted_t, cef_render_process_handler_t, cef_v8_propertyattribute_t, cef_v8_value_create_array_buffer_with_copy};
|
||||
use cef::{ImplFrame, ImplRenderProcessHandler, ImplV8Context, ImplV8Value, V8Handler, V8Propertyattribute, V8Value, WrapRenderProcessHandler, v8_value_create_function};
|
||||
|
||||
use crate::ipc::{MessageType, UnpackMessage, UnpackedMessage};
|
||||
|
||||
use super::render_process_v8_handler::RenderProcessV8HandlerImpl;
|
||||
|
||||
pub(crate) struct RenderProcessHandlerImpl {
|
||||
object: *mut RcImpl<cef_render_process_handler_t, Self>,
|
||||
}
|
||||
impl RenderProcessHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplRenderProcessHandler for RenderProcessHandlerImpl {
|
||||
fn on_process_message_received(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
frame: Option<&mut cef::Frame>,
|
||||
_source_process: cef::ProcessId,
|
||||
message: Option<&mut cef::ProcessMessage>,
|
||||
) -> std::ffi::c_int {
|
||||
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
|
||||
match unpacked_message {
|
||||
Some(UnpackedMessage {
|
||||
message_type: MessageType::SendToJS,
|
||||
data,
|
||||
}) => {
|
||||
let Some(frame) = frame else {
|
||||
tracing::error!("Frame is not available");
|
||||
return 0;
|
||||
};
|
||||
let Some(context) = frame.v8_context() else {
|
||||
tracing::error!("V8 context is not available");
|
||||
return 0;
|
||||
};
|
||||
if context.enter() == 0 {
|
||||
tracing::error!("Failed to enter V8 context");
|
||||
return 0;
|
||||
}
|
||||
let mut value: V8Value = unsafe { cef_v8_value_create_array_buffer_with_copy(data.as_ptr() as *mut std::ffi::c_void, data.len()) }.wrap_result();
|
||||
let Some(global) = context.global() else {
|
||||
tracing::error!("Global object is not available in V8 context");
|
||||
return 0;
|
||||
};
|
||||
|
||||
let function_name = "receiveNativeMessage";
|
||||
let property_name = "receiveNativeMessageData";
|
||||
|
||||
let function_call = format!("window.{function_name}(window.{property_name})");
|
||||
|
||||
global.set_value_bykey(Some(&property_name.into()), Some(&mut value), cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_READONLY.wrap_result());
|
||||
|
||||
if global.value_bykey(Some(&function_name.into())).is_some() {
|
||||
frame.execute_java_script(Some(&function_call.as_str().into()), None, 0);
|
||||
}
|
||||
|
||||
if context.exit() == 0 {
|
||||
tracing::error!("Failed to exit V8 context");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::error!("Unexpected message type received in render process");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
fn on_context_created(&self, _browser: Option<&mut cef::Browser>, _frame: Option<&mut cef::Frame>, context: Option<&mut cef::V8Context>) {
|
||||
let register_js_function = |context: &mut cef::V8Context, name: &'static str| {
|
||||
let mut v8_handler = V8Handler::new(RenderProcessV8HandlerImpl::new());
|
||||
let Some(mut function) = v8_value_create_function(Some(&name.into()), Some(&mut v8_handler)) else {
|
||||
tracing::error!("Failed to create V8 function {name}");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(global) = context.global() else {
|
||||
tracing::error!("Global object is not available in V8 context");
|
||||
return;
|
||||
};
|
||||
global.set_value_bykey(Some(&name.into()), Some(&mut function), V8Propertyattribute::default());
|
||||
};
|
||||
|
||||
let Some(context) = context else {
|
||||
tracing::error!("V8 context is not available");
|
||||
return;
|
||||
};
|
||||
|
||||
let initialized_function_name = "initializeNativeCommunication";
|
||||
let send_function_name = "sendNativeMessage";
|
||||
|
||||
register_js_function(context, initialized_function_name);
|
||||
register_js_function(context, send_function_name);
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_render_process_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RenderProcessHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for RenderProcessHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapRenderProcessHandler for RenderProcessHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_process_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
86
desktop/ui/src/internal/render_process_v8_handler.rs
Normal file
86
desktop/ui/src/internal/render_process_v8_handler.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use cef::{ImplV8Handler, ImplV8Value, V8Value, WrapV8Handler, rc::Rc, v8_context_get_current_context};
|
||||
|
||||
use crate::ipc::{MessageType, SendMessage};
|
||||
|
||||
pub struct RenderProcessV8HandlerImpl {
|
||||
object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>,
|
||||
}
|
||||
impl RenderProcessV8HandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplV8Handler for RenderProcessV8HandlerImpl {
|
||||
fn execute(
|
||||
&self,
|
||||
name: Option<&cef::CefString>,
|
||||
_object: Option<&mut V8Value>,
|
||||
arguments: Option<&[Option<V8Value>]>,
|
||||
_retval: Option<&mut Option<V8Value>>,
|
||||
_exception: Option<&mut cef::CefString>,
|
||||
) -> std::ffi::c_int {
|
||||
match name.map(|s| s.to_string()).unwrap_or_default().as_str() {
|
||||
"initializeNativeCommunication" => {
|
||||
v8_context_get_current_context().send_message(MessageType::Initialized, vec![0u8].as_slice());
|
||||
}
|
||||
"sendNativeMessage" => {
|
||||
let Some(args) = arguments else {
|
||||
tracing::error!("No arguments provided to sendNativeMessage");
|
||||
return 0;
|
||||
};
|
||||
let Some(arg1) = args.first() else {
|
||||
tracing::error!("No arguments provided to sendNativeMessage");
|
||||
return 0;
|
||||
};
|
||||
let Some(arg1) = arg1.as_ref() else {
|
||||
tracing::error!("First argument to sendNativeMessage is not an ArrayBuffer");
|
||||
return 0;
|
||||
};
|
||||
if arg1.is_array_buffer() == 0 {
|
||||
tracing::error!("First argument to sendNativeMessage is not an ArrayBuffer");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let size = arg1.array_buffer_byte_length();
|
||||
let ptr = arg1.array_buffer_data();
|
||||
let data = unsafe { std::slice::from_raw_parts_mut(ptr as *mut u8, size) };
|
||||
|
||||
v8_context_get_current_context().send_message(MessageType::SendToNative, data);
|
||||
|
||||
return 1;
|
||||
}
|
||||
name => {
|
||||
tracing::error!("Unknown V8 function called: {}", name);
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut cef::sys::_cef_v8_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RenderProcessV8HandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for RenderProcessV8HandlerImpl {
|
||||
fn as_base(&self) -> &cef::sys::cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapV8Handler for RenderProcessV8HandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
84
desktop/ui/src/internal/request_handler.rs
Normal file
84
desktop/ui/src/internal/request_handler.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_request_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{AuthCallback, Browser, CefString, Frame, ImplRequest, ImplRequestHandler, Request, ResourceRequestHandler, WrapRequestHandler};
|
||||
use std::ffi::c_int;
|
||||
|
||||
use super::resource_request_handler::ResourceRequestHandlerImpl;
|
||||
use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
|
||||
|
||||
pub(crate) struct RequestHandlerImpl {
|
||||
object: *mut RcImpl<_cef_request_handler_t, Self>,
|
||||
}
|
||||
|
||||
impl RequestHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplRequestHandler for RequestHandlerImpl {
|
||||
fn on_before_browse(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, request: Option<&mut Request>, _user_gesture: c_int, _is_redirect: c_int) -> c_int {
|
||||
let Some(request) = request else { return 1 };
|
||||
let url = CefString::from(&request.url()).to_string();
|
||||
if url.starts_with(&format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/")) {
|
||||
0
|
||||
} else {
|
||||
tracing::warn!("Blocked navigation to: {}", url);
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_request_handler(
|
||||
&self,
|
||||
_browser: Option<&mut Browser>,
|
||||
_frame: Option<&mut Frame>,
|
||||
_request: Option<&mut Request>,
|
||||
_is_navigation: c_int,
|
||||
_is_download: c_int,
|
||||
_request_initiator: Option<&CefString>,
|
||||
_disable_default_handling: Option<&mut c_int>,
|
||||
) -> Option<ResourceRequestHandler> {
|
||||
Some(ResourceRequestHandler::new(ResourceRequestHandlerImpl::new()))
|
||||
}
|
||||
|
||||
fn auth_credentials(
|
||||
&self,
|
||||
_browser: Option<&mut Browser>,
|
||||
_origin_url: Option<&CefString>,
|
||||
_is_proxy: c_int,
|
||||
_host: Option<&CefString>,
|
||||
_port: c_int,
|
||||
_realm: Option<&CefString>,
|
||||
_scheme: Option<&CefString>,
|
||||
_callback: Option<&mut AuthCallback>,
|
||||
) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_request_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RequestHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for RequestHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapRequestHandler for RequestHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_request_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
107
desktop/ui/src/internal/resource_handler.rs
Normal file
107
desktop/ui/src/internal/resource_handler.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_resource_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{Callback, CefString, ImplResourceHandler, ImplResponse, Request, ResourceReadCallback, Response, WrapResourceHandler};
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::c_int;
|
||||
use std::io::Read;
|
||||
|
||||
use crate::resources::{Resource, ResourceReader};
|
||||
|
||||
pub(crate) struct ResourceHandlerImpl {
|
||||
object: *mut RcImpl<_cef_resource_handler_t, Self>,
|
||||
reader: Option<RefCell<ResourceReader>>,
|
||||
mimetype: Option<String>,
|
||||
}
|
||||
|
||||
impl ResourceHandlerImpl {
|
||||
pub fn new(resource: Option<Resource>) -> Self {
|
||||
if let Some(resource) = resource {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
reader: Some(resource.reader.into()),
|
||||
mimetype: resource.mimetype,
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
reader: None,
|
||||
mimetype: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplResourceHandler for ResourceHandlerImpl {
|
||||
fn open(&self, _request: Option<&mut Request>, handle_request: Option<&mut c_int>, _callback: Option<&mut Callback>) -> c_int {
|
||||
if let Some(handle_request) = handle_request {
|
||||
*handle_request = 1;
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
fn response_headers(&self, response: Option<&mut Response>, response_length: Option<&mut i64>, _redirect_url: Option<&mut CefString>) {
|
||||
if let Some(response_length) = response_length {
|
||||
*response_length = -1; // Indicating that the length is unknown
|
||||
}
|
||||
if let Some(response) = response {
|
||||
if self.reader.is_some() {
|
||||
if let Some(mimetype) = &self.mimetype {
|
||||
response.set_mime_type(Some(&mimetype.as_str().into()));
|
||||
} else {
|
||||
response.set_mime_type(None);
|
||||
}
|
||||
response.set_status(200);
|
||||
} else {
|
||||
response.set_status(404);
|
||||
response.set_mime_type(Some(&"text/plain".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&self, data_out: *mut u8, bytes_to_read: c_int, bytes_read: Option<&mut c_int>, _callback: Option<&mut ResourceReadCallback>) -> c_int {
|
||||
let Some(bytes_read) = bytes_read else { unreachable!() };
|
||||
let out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read as usize) };
|
||||
if let Some(reader) = &self.reader {
|
||||
if let Ok(read) = reader.borrow_mut().read(out) {
|
||||
*bytes_read = read as i32;
|
||||
if read > 0 {
|
||||
return 1; // Indicating that data was read
|
||||
}
|
||||
} else {
|
||||
*bytes_read = -2; // Indicating ERR_FAILED
|
||||
}
|
||||
}
|
||||
0 // Indicating no data was read
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_resource_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ResourceHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
reader: self.reader.clone(),
|
||||
mimetype: self.mimetype.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for ResourceHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapResourceHandler for ResourceHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_resource_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
60
desktop/ui/src/internal/resource_request_handler.rs
Normal file
60
desktop/ui/src/internal/resource_request_handler.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_resource_request_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{Browser, Callback, CefString, Frame, ImplRequest, ImplResourceRequestHandler, Request, ReturnValue, WrapResourceRequestHandler};
|
||||
|
||||
use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
|
||||
|
||||
// TODO: Deny all external requests once we stop relying on google fonts for font preview
|
||||
fn is_allowed_url(url: &str) -> bool {
|
||||
url.starts_with(&format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/")) || url.starts_with("https://fonts.googleapis.com/css2") || url.starts_with("https://fonts.gstatic.com/")
|
||||
}
|
||||
|
||||
pub(crate) struct ResourceRequestHandlerImpl {
|
||||
object: *mut RcImpl<_cef_resource_request_handler_t, Self>,
|
||||
}
|
||||
|
||||
impl ResourceRequestHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplResourceRequestHandler for ResourceRequestHandlerImpl {
|
||||
fn on_before_resource_load(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, request: Option<&mut Request>, _callback: Option<&mut Callback>) -> ReturnValue {
|
||||
let Some(request) = request else { return ReturnValue::CANCEL };
|
||||
let url = CefString::from(&request.url()).to_string();
|
||||
if is_allowed_url(&url) {
|
||||
ReturnValue::CONTINUE
|
||||
} else {
|
||||
tracing::error!("Blocked resource load: {}", url);
|
||||
ReturnValue::CANCEL
|
||||
}
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_resource_request_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ResourceRequestHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for ResourceRequestHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapResourceRequestHandler for ResourceRequestHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_resource_request_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
74
desktop/ui/src/internal/scheme_handler_factory.rs
Normal file
74
desktop/ui/src/internal/scheme_handler_factory.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_scheme_handler_factory_t, cef_base_ref_counted_t, cef_scheme_options_t};
|
||||
use cef::{Browser, CefString, Frame, ImplRequest, ImplSchemeHandlerFactory, ImplSchemeRegistrar, Request, ResourceHandler, SchemeRegistrar, WrapSchemeHandlerFactory};
|
||||
|
||||
use super::resource_handler::ResourceHandlerImpl;
|
||||
use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
|
||||
use crate::delegate::BrowserDelegate;
|
||||
|
||||
pub(crate) struct SchemeHandlerFactoryImpl {
|
||||
object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>,
|
||||
delegate: BrowserDelegate,
|
||||
}
|
||||
impl SchemeHandlerFactoryImpl {
|
||||
pub(crate) fn new(delegate: BrowserDelegate) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
delegate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn register_schemes(registrar: Option<&mut SchemeRegistrar>) {
|
||||
if let Some(registrar) = registrar {
|
||||
let mut scheme_options = 0;
|
||||
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32;
|
||||
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32;
|
||||
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32;
|
||||
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32;
|
||||
registrar.add_custom_scheme(Some(&RESOURCE_SCHEME.into()), scheme_options);
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplSchemeHandlerFactory for SchemeHandlerFactoryImpl {
|
||||
fn create(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, _scheme_name: Option<&CefString>, request: Option<&mut Request>) -> Option<ResourceHandler> {
|
||||
if let Some(request) = request {
|
||||
let url = CefString::from(&request.url()).to_string();
|
||||
let path = url
|
||||
.strip_prefix(&format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/"))
|
||||
.expect("CEF should only call this for our custom scheme and domain that we registered this factory for");
|
||||
let resource = self.delegate.load_resource(path.to_string().into());
|
||||
return Some(ResourceHandler::new(ResourceHandlerImpl::new(resource)));
|
||||
}
|
||||
None
|
||||
}
|
||||
fn get_raw(&self) -> *mut _cef_scheme_handler_factory_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for SchemeHandlerFactoryImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
delegate: self.delegate.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for SchemeHandlerFactoryImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapSchemeHandlerFactory for SchemeHandlerFactoryImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
61
desktop/ui/src/internal/task.rs
Normal file
61
desktop/ui/src/internal/task.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_task_t, cef_base_ref_counted_t};
|
||||
use cef::{ImplTask, WrapTask};
|
||||
use std::cell::RefCell;
|
||||
|
||||
// Closure-based task wrapper following CEF patterns
|
||||
pub struct ClosureTask<F> {
|
||||
pub(crate) object: *mut RcImpl<_cef_task_t, Self>,
|
||||
pub(crate) closure: RefCell<Option<F>>,
|
||||
}
|
||||
|
||||
impl<F: FnOnce() + Send + 'static> ClosureTask<F> {
|
||||
pub fn new(closure: F) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
closure: RefCell::new(Some(closure)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnOnce() + Send + 'static> ImplTask for ClosureTask<F> {
|
||||
fn execute(&self) {
|
||||
if let Some(closure) = self.closure.borrow_mut().take() {
|
||||
closure();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_task_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnOnce() + Send + 'static> Clone for ClosureTask<F> {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
if !self.object.is_null() {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
closure: RefCell::new(None), // Closure can only be executed once
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnOnce() + Send + 'static> Rc for ClosureTask<F> {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnOnce() + Send + 'static> WrapTask for ClosureTask<F> {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_task_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
114
desktop/ui/src/ipc.rs
Normal file
114
desktop/ui/src/ipc.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
// TODO: Consider inlining this file into internal
|
||||
|
||||
use cef::{Frame, ImplBinaryValue, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Context, ProcessId, V8Context, sys::cef_process_id_t};
|
||||
|
||||
pub(crate) enum MessageType {
|
||||
Initialized,
|
||||
SendToJS,
|
||||
SendToNative,
|
||||
}
|
||||
impl From<MessageType> for MessageInfo {
|
||||
fn from(val: MessageType) -> Self {
|
||||
match val {
|
||||
MessageType::Initialized => MessageInfo {
|
||||
name: "initialized".to_string(),
|
||||
target: cef_process_id_t::PID_BROWSER.into(),
|
||||
},
|
||||
MessageType::SendToJS => MessageInfo {
|
||||
name: "send_to_js".to_string(),
|
||||
target: cef_process_id_t::PID_RENDERER.into(),
|
||||
},
|
||||
MessageType::SendToNative => MessageInfo {
|
||||
name: "send_to_native".to_string(),
|
||||
target: cef_process_id_t::PID_BROWSER.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
impl TryFrom<String> for MessageType {
|
||||
type Error = ();
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
match value.as_str() {
|
||||
"initialized" => Ok(MessageType::Initialized),
|
||||
"send_to_js" => Ok(MessageType::SendToJS),
|
||||
"send_to_native" => Ok(MessageType::SendToNative),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct MessageInfo {
|
||||
name: String,
|
||||
target: ProcessId,
|
||||
}
|
||||
|
||||
pub(crate) trait SendMessage {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]);
|
||||
}
|
||||
impl SendMessage for Option<V8Context> {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||
let Some(context) = self else {
|
||||
tracing::error!("Current V8 context is not available, cannot send message");
|
||||
return;
|
||||
};
|
||||
|
||||
context.send_message(message_type, message);
|
||||
}
|
||||
}
|
||||
impl SendMessage for V8Context {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||
let Some(frame) = self.frame() else {
|
||||
tracing::error!("Current V8 context does not have a frame, cannot send message");
|
||||
return;
|
||||
};
|
||||
|
||||
frame.send_message(message_type, message);
|
||||
}
|
||||
}
|
||||
impl SendMessage for Frame {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||
let MessageInfo { name, target } = message_type.into();
|
||||
|
||||
let Some(mut process_message) = cef::process_message_create(Some(&name.as_str().into())) else {
|
||||
tracing::error!("Failed to create process message: {}", name);
|
||||
return;
|
||||
};
|
||||
let Some(arg_list) = process_message.argument_list() else { return };
|
||||
let mut value = ::cef::binary_value_create(Some(message));
|
||||
arg_list.set_binary(0, value.as_mut());
|
||||
|
||||
self.send_process_message(target, Some(&mut process_message));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct UnpackedMessage<'a> {
|
||||
pub(crate) message_type: MessageType,
|
||||
pub(crate) data: &'a [u8],
|
||||
}
|
||||
|
||||
trait Sealed {}
|
||||
impl Sealed for cef::ProcessMessage {}
|
||||
#[allow(private_bounds)]
|
||||
pub(crate) trait UnpackMessage: Sealed {
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that the message is valid.
|
||||
/// Message should come from cef.
|
||||
unsafe fn unpack(&self) -> Option<UnpackedMessage<'_>>;
|
||||
}
|
||||
impl UnpackMessage for cef::ProcessMessage {
|
||||
unsafe fn unpack(&self) -> Option<UnpackedMessage<'_>> {
|
||||
let pointer: *mut cef::sys::_cef_string_utf16_t = self.name().into();
|
||||
let message = unsafe { super::utility::pointer_to_string(pointer) };
|
||||
let Ok(message_type) = message.try_into() else {
|
||||
tracing::error!("Failed to get message type from process message");
|
||||
return None;
|
||||
};
|
||||
let arglist = self.argument_list()?;
|
||||
let binary = arglist.binary(0)?;
|
||||
let size = binary.size();
|
||||
let ptr = binary.raw_data();
|
||||
let buffer = unsafe { std::slice::from_raw_parts(ptr as *const u8, size) };
|
||||
Some(UnpackedMessage { message_type, data: buffer })
|
||||
}
|
||||
}
|
||||
251
desktop/ui/src/lib.rs
Normal file
251
desktop/ui/src/lib.rs
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
78
desktop/ui/src/platform.rs
Normal file
78
desktop/ui/src/platform.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) mod mac;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) mod win;
|
||||
|
||||
pub(crate) fn accelerated_paint(disable_gpu_acceleration: bool) -> bool {
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
{
|
||||
!disable_gpu_acceleration && should_enable_hardware_acceleration()
|
||||
}
|
||||
#[cfg(not(feature = "accelerated_paint"))]
|
||||
{
|
||||
let _ = disable_gpu_acceleration;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
fn should_enable_hardware_acceleration() -> bool {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Check if running on Wayland or X11
|
||||
let has_wayland = std::env::var("WAYLAND_DISPLAY")
|
||||
.ok()
|
||||
.filter(|var| !var.is_empty())
|
||||
.or_else(|| std::env::var("WAYLAND_SOCKET").ok())
|
||||
.filter(|var| !var.is_empty())
|
||||
.is_some();
|
||||
|
||||
let has_x11 = std::env::var("DISPLAY").ok().filter(|var| !var.is_empty()).is_some();
|
||||
|
||||
if !has_wayland && !has_x11 {
|
||||
tracing::warn!("No display server detected, disabling hardware acceleration");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for NVIDIA proprietary driver (known to have issues)
|
||||
if let Ok(driver_info) = std::fs::read_to_string("/proc/driver/nvidia/version")
|
||||
&& driver_info.contains("NVIDIA")
|
||||
{
|
||||
tracing::warn!("NVIDIA proprietary driver detected, hardware acceleration may be unstable");
|
||||
// Still return true but with warning
|
||||
}
|
||||
|
||||
// Check for basic GPU capabilities
|
||||
if has_wayland {
|
||||
tracing::info!("Wayland detected, enabling hardware acceleration");
|
||||
true
|
||||
} else if has_x11 {
|
||||
tracing::info!("X11 detected, enabling hardware acceleration");
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Windows generally has good D3D11 support
|
||||
tracing::info!("Windows detected, enabling hardware acceleration");
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// macOS has good Metal/IOSurface support
|
||||
tracing::info!("macOS detected, enabling hardware acceleration");
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
|
||||
{
|
||||
tracing::warn!("Unsupported platform for hardware acceleration");
|
||||
false
|
||||
}
|
||||
}
|
||||
35
desktop/ui/src/platform/linux.rs
Normal file
35
desktop/ui/src/platform/linux.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use crate::frames::plane;
|
||||
|
||||
pub(crate) fn setup_command(command: &mut Command, #[cfg(feature = "accelerated_paint")] host_frame_fd: Option<std::os::fd::RawFd>) {
|
||||
let main_pid = std::process::id() as libc::pid_t;
|
||||
// SAFETY: the closure runs in the forked child before exec and only makes async-signal-safe calls
|
||||
unsafe {
|
||||
command.pre_exec(move || {
|
||||
// Tie the host lifetime to the main process
|
||||
if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) != 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
if libc::getppid() != main_pid {
|
||||
return Err(std::io::Error::from_raw_os_error(libc::ESRCH));
|
||||
}
|
||||
|
||||
// Move the host end of the frame socket to its advertised fd
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
if let Some(fd) = host_frame_fd {
|
||||
let target = plane::FRAME_SOCKET_CHILD_FD;
|
||||
if fd == target {
|
||||
if libc::fcntl(target, libc::F_SETFD, 0) != 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
} else if libc::dup2(fd, target) == -1 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
63
desktop/ui/src/platform/mac.rs
Normal file
63
desktop/ui/src/platform/mac.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use objc2::rc::Retained;
|
||||
use objc2::runtime::Bool;
|
||||
use objc2::{ClassType, define_class, msg_send};
|
||||
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSEvent, NSResponder};
|
||||
use objc2_foundation::NSObject;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use cef::application_mac::{CefAppProtocol, CrAppControlProtocol, CrAppProtocol};
|
||||
|
||||
static HANDLING_SEND_EVENT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
define_class!(
|
||||
#[unsafe(super(NSApplication, NSResponder, NSObject))]
|
||||
#[name = "GraphiteCefHostApplication"]
|
||||
struct CefHostApplication;
|
||||
|
||||
unsafe impl CrAppProtocol for CefHostApplication {
|
||||
#[unsafe(method(isHandlingSendEvent))]
|
||||
fn is_handling_send_event(&self) -> Bool {
|
||||
Bool::new(HANDLING_SEND_EVENT.load(Ordering::Relaxed))
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl CrAppControlProtocol for CefHostApplication {
|
||||
#[unsafe(method(setHandlingSendEvent:))]
|
||||
fn set_handling_send_event(&self, handling: Bool) {
|
||||
HANDLING_SEND_EVENT.store(handling.as_bool(), Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl CefAppProtocol for CefHostApplication {}
|
||||
|
||||
impl CefHostApplication {
|
||||
#[unsafe(method(sendEvent:))]
|
||||
fn send_event(&self, event: &NSEvent) {
|
||||
let was_handling = HANDLING_SEND_EVENT.swap(true, Ordering::Relaxed);
|
||||
let _: () = unsafe { msg_send![super(self), sendEvent: event] };
|
||||
HANDLING_SEND_EVENT.store(was_handling, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
pub(crate) fn install_application() {
|
||||
let app: Retained<NSApplication> = unsafe { msg_send![CefHostApplication::class(), sharedApplication] };
|
||||
app.setActivationPolicy(NSApplicationActivationPolicy::Prohibited);
|
||||
}
|
||||
|
||||
pub(crate) fn spawn_parent_watchdog(main_pid: u32) {
|
||||
let result = std::thread::Builder::new().name("parent-watchdog".to_string()).spawn(move || {
|
||||
loop {
|
||||
// SAFETY: getppid is always safe to call.
|
||||
if unsafe { libc::getppid() } as u32 != main_pid {
|
||||
tracing::warn!("Parent process is gone, exiting...");
|
||||
std::process::exit(0);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
});
|
||||
if let Err(e) = result {
|
||||
tracing::error!("Failed to spawn parent watchdog: {e}");
|
||||
}
|
||||
}
|
||||
39
desktop/ui/src/platform/win.rs
Normal file
39
desktop/ui/src/platform/win.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
||||
use windows::Win32::System::JobObjects::{
|
||||
AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation, SetInformationJobObject,
|
||||
};
|
||||
use windows::core::PCWSTR;
|
||||
|
||||
pub(crate) struct KillOnCloseJob(HANDLE);
|
||||
|
||||
// SAFETY: job object handles may be used and closed from any thread.
|
||||
unsafe impl Send for KillOnCloseJob {}
|
||||
unsafe impl Sync for KillOnCloseJob {}
|
||||
|
||||
impl KillOnCloseJob {
|
||||
pub(crate) fn assign(child: &std::process::Child) -> windows::core::Result<Self> {
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
unsafe {
|
||||
let job = CreateJobObjectW(None, PCWSTR::null())?;
|
||||
let job = Self(job);
|
||||
let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
|
||||
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
SetInformationJobObject(
|
||||
job.0,
|
||||
JobObjectExtendedLimitInformation,
|
||||
&info as *const _ as *const core::ffi::c_void,
|
||||
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
|
||||
)?;
|
||||
AssignProcessToJobObject(job.0, HANDLE(child.as_raw_handle()))?;
|
||||
Ok(job)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for KillOnCloseJob {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let _ = CloseHandle(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
desktop/ui/src/remote.rs
Normal file
34
desktop/ui/src/remote.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use crate::consts::BROWSER_HOST_CONFIG_FLAG;
|
||||
|
||||
pub(crate) mod host;
|
||||
pub(crate) mod messages;
|
||||
pub(crate) mod spawn;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct HostConfig {
|
||||
pub(crate) server: String,
|
||||
pub(crate) main_pid: u32,
|
||||
pub(crate) acceleration: bool,
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) frame_socket_fd: Option<std::os::fd::RawFd>,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) frame_service: Option<String>,
|
||||
}
|
||||
|
||||
impl HostConfig {
|
||||
pub(crate) fn to_arg(&self) -> String {
|
||||
let json = serde_json::to_string(self).expect("HostConfig always serializes");
|
||||
format!("{BROWSER_HOST_CONFIG_FLAG}{json}")
|
||||
}
|
||||
|
||||
pub(crate) fn from_args(args: &[String]) -> Option<Self> {
|
||||
let json = args.iter().find_map(|arg| arg.strip_prefix(BROWSER_HOST_CONFIG_FLAG))?;
|
||||
match serde_json::from_str(json) {
|
||||
Ok(config) => Some(config),
|
||||
Err(e) => {
|
||||
tracing::error!("Malformed host config on the command line: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
115
desktop/ui/src/remote/host.rs
Normal file
115
desktop/ui/src/remote/host.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use ipc_channel::ipc::{IpcReceiver, IpcSender};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::HostConfig;
|
||||
use super::messages::{EventMessage, HostControlMessage};
|
||||
use crate::context::{CefContext, CefContextHandle};
|
||||
use crate::delegate::BrowserDelegate;
|
||||
use crate::frames::FrameStreamer;
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use crate::frames::plane::PlaneSender;
|
||||
use crate::frames::sequence::SequenceState;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::platform::mac;
|
||||
|
||||
pub(crate) fn run() {
|
||||
// Ignore SIGINT, the controlling process is responsible for shutting down the host
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
unsafe {
|
||||
libc::signal(libc::SIGINT, libc::SIG_IGN);
|
||||
}
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let config = HostConfig::from_args(&args).expect("CEF host started without a valid host config argument");
|
||||
let acceleration_requested = config.acceleration;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mac::spawn_parent_watchdog(config.main_pid);
|
||||
|
||||
let bootstrap = IpcSender::<EventMessage>::connect(config.server.clone()).expect("Failed to connect to the main process bootstrap server");
|
||||
let event_sender = Arc::new(Mutex::new(bootstrap));
|
||||
let (control_sender, control_receiver) = ipc_channel::ipc::channel::<HostControlMessage>().expect("Failed to create control channel");
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
let plane = if acceleration_requested { PlaneSender::from_config(&config, event_sender.clone()) } else { None };
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
let acceleration = plane.is_some();
|
||||
#[cfg(not(feature = "accelerated_paint"))]
|
||||
let acceleration = {
|
||||
if acceleration_requested {
|
||||
tracing::error!("UI acceleration requested but the accelerated_paint feature is disabled; using software frames");
|
||||
}
|
||||
false
|
||||
};
|
||||
|
||||
event_sender
|
||||
.lock()
|
||||
.expect("The host message sender cannot be poisoned before threads exist")
|
||||
.send(EventMessage::Hello {
|
||||
pid: std::process::id(),
|
||||
control_sender,
|
||||
acceleration,
|
||||
})
|
||||
.expect("Failed to send Hello to the main process");
|
||||
|
||||
let sequence = Arc::new(SequenceState::new());
|
||||
let frames = FrameStreamer::new(
|
||||
event_sender.clone(),
|
||||
sequence.clone(),
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
plane,
|
||||
);
|
||||
let (view_info_sender, view_info_receiver) = std::sync::mpsc::channel();
|
||||
let delegate = BrowserDelegate::new(event_sender.clone(), view_info_receiver);
|
||||
|
||||
let context = match CefContext::create(delegate, frames, view_info_sender, acceleration) {
|
||||
Ok(context) => {
|
||||
if let Ok(sender) = event_sender.lock() {
|
||||
let _ = sender.send(EventMessage::BrowserCreated);
|
||||
}
|
||||
context
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("CEF initialization failed in host process: {e}");
|
||||
if let Ok(sender) = event_sender.lock() {
|
||||
let _ = sender.send(EventMessage::InitFailed(e));
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let outcome = context.run(move |handle| control_loop(&control_receiver, &handle, sequence.as_ref()));
|
||||
|
||||
match outcome {
|
||||
ControlOutcome::Shutdown => {
|
||||
tracing::debug!("Shut down CEF host");
|
||||
if let Ok(sender) = event_sender.lock() {
|
||||
let _ = sender.send(EventMessage::ShutdownComplete);
|
||||
}
|
||||
}
|
||||
ControlOutcome::Disconnected => std::process::exit(0),
|
||||
}
|
||||
}
|
||||
|
||||
enum ControlOutcome {
|
||||
Shutdown,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
fn control_loop(receiver: &IpcReceiver<HostControlMessage>, context: &CefContextHandle, sequence: &SequenceState) -> ControlOutcome {
|
||||
loop {
|
||||
match receiver.recv() {
|
||||
Ok(HostControlMessage::Input(events)) => context.apply_input(events),
|
||||
Ok(HostControlMessage::UpdateViewInfo(update)) => context.update_view_info(update),
|
||||
Ok(HostControlMessage::RefreshViewInfo) => context.refresh_view_info(),
|
||||
Ok(HostControlMessage::SendWebMessage(message)) => context.send_web_message(message),
|
||||
Ok(HostControlMessage::FrameAck { seq }) => sequence.ack(seq),
|
||||
Ok(HostControlMessage::Shutdown) => return ControlOutcome::Shutdown,
|
||||
Err(ipc_channel::IpcError::Io(ref io)) if io.kind() == std::io::ErrorKind::Interrupted => {}
|
||||
Err(e) => {
|
||||
tracing::warn!("Control channel closed ({e:?}), shutting down CEF host");
|
||||
return ControlOutcome::Disconnected;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
desktop/ui/src/remote/messages.rs
Normal file
51
desktop/ui/src/remote/messages.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use ipc_channel::ipc::{IpcSender, IpcSharedMemory};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::Cursor;
|
||||
use crate::context::InitError;
|
||||
use crate::input::InputEvent;
|
||||
use crate::view::ViewInfoUpdate;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) enum HostControlMessage {
|
||||
Input(Vec<InputEvent>),
|
||||
UpdateViewInfo(ViewInfoUpdate),
|
||||
RefreshViewInfo,
|
||||
SendWebMessage(Vec<u8>),
|
||||
FrameAck { seq: u64 },
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) enum EventMessage {
|
||||
Hello {
|
||||
pid: u32,
|
||||
control_sender: IpcSender<HostControlMessage>,
|
||||
acceleration: bool,
|
||||
},
|
||||
BrowserCreated,
|
||||
InitFailed(InitError),
|
||||
WebCommunicationInitialized,
|
||||
WebMessage(Vec<u8>),
|
||||
CursorChange(Cursor),
|
||||
AdvertiseFrameSegment {
|
||||
index: u32,
|
||||
shm: IpcSharedMemory,
|
||||
},
|
||||
SoftwareFrame {
|
||||
seq: u64,
|
||||
segment: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
},
|
||||
#[cfg(all(target_os = "windows", feature = "accelerated_paint"))]
|
||||
AcceleratedFrame {
|
||||
seq: u64,
|
||||
handle: u64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: u32,
|
||||
content: Option<crate::frames::import::ContentRect>,
|
||||
},
|
||||
ShutdownComplete,
|
||||
}
|
||||
401
desktop/ui/src/remote/spawn.rs
Normal file
401
desktop/ui/src/remote/spawn.rs
Normal file
@@ -0,0 +1,401 @@
|
||||
use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
|
||||
use std::process::{Child, Command};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::{self, RecvTimeoutError};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::HostConfig;
|
||||
use super::messages::{EventMessage, HostControlMessage};
|
||||
use crate::consts::{HOST_HELLO_TIMEOUT, HOST_SHUTDOWN_TIMEOUT};
|
||||
use crate::events::EventQueue;
|
||||
use crate::frames::FrameSurface;
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use crate::frames::plane;
|
||||
use crate::frames::receive::{FrameConsumer, PendingFrame, SegmentTable};
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
use crate::platform;
|
||||
use crate::{UiError, UiEvent};
|
||||
|
||||
pub(crate) struct HostHandle {
|
||||
sender: IpcSender<HostControlMessage>,
|
||||
receivers: Mutex<Option<InstanceReceivers>>,
|
||||
child: Arc<Mutex<Child>>,
|
||||
shutting_down: Arc<AtomicBool>,
|
||||
died_reported: Arc<AtomicBool>,
|
||||
#[cfg_attr(any(not(feature = "accelerated_paint"), target_os = "windows"), expect(dead_code))]
|
||||
host_acceleration: bool,
|
||||
#[cfg(target_os = "windows")]
|
||||
_job: Option<platform::win::KillOnCloseJob>,
|
||||
}
|
||||
|
||||
struct InstanceReceivers {
|
||||
events: IpcReceiver<EventMessage>,
|
||||
#[cfg(all(any(target_os = "linux", target_os = "macos"), feature = "accelerated_paint"))]
|
||||
frame_plane: Option<plane::PlaneReceiver>,
|
||||
}
|
||||
|
||||
impl HostHandle {
|
||||
pub(crate) fn send(&self, message: HostControlMessage) {
|
||||
if let Err(e) = self.sender.send(message) {
|
||||
tracing::debug!("Failed to send message to CEF host: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown(&self, shutdown_complete_receiver: &mpsc::Receiver<()>) {
|
||||
self.shutting_down.store(true, Ordering::SeqCst);
|
||||
let deadline = Instant::now() + HOST_SHUTDOWN_TIMEOUT;
|
||||
|
||||
if self.sender.send(HostControlMessage::Shutdown).is_ok() {
|
||||
match shutdown_complete_receiver.recv_timeout(HOST_SHUTDOWN_TIMEOUT) {
|
||||
Ok(()) => tracing::debug!("CEF host completed shutdown"),
|
||||
Err(RecvTimeoutError::Timeout) => tracing::warn!("Timed out waiting for the CEF host to shut down"),
|
||||
Err(RecvTimeoutError::Disconnected) => tracing::debug!("CEF host connection closed during shutdown"),
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
match self.child.lock() {
|
||||
Ok(mut child) => match child.try_wait() {
|
||||
Ok(None) => {}
|
||||
Ok(Some(_)) | Err(_) => return,
|
||||
},
|
||||
Err(_) => return,
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(25));
|
||||
}
|
||||
|
||||
tracing::warn!("CEF host did not exit in time, killing it");
|
||||
if let Ok(mut child) = self.child.lock() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HostHandle {
|
||||
fn drop(&mut self) {
|
||||
if !self.shutting_down.load(Ordering::SeqCst) {
|
||||
let _ = self.sender.send(HostControlMessage::Shutdown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn spawn_host(acceleration: bool) -> Result<HostHandle, UiError> {
|
||||
let (server, server_name) = IpcOneShotServer::<EventMessage>::new().map_err(|e| UiError::Bootstrap(format!("failed to create the bootstrap server: {e}")))?;
|
||||
|
||||
let executable = std::env::current_exe().map_err(|e| UiError::Bootstrap(format!("failed to get the current executable path: {e}")))?;
|
||||
let mut command = Command::new(executable);
|
||||
|
||||
#[cfg_attr(not(all(any(target_os = "linux", target_os = "macos"), feature = "accelerated_paint")), expect(unused_mut))]
|
||||
let mut config = HostConfig {
|
||||
server: server_name,
|
||||
main_pid: std::process::id(),
|
||||
acceleration,
|
||||
#[cfg(target_os = "linux")]
|
||||
frame_socket_fd: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
frame_service: None,
|
||||
};
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "accelerated_paint"))]
|
||||
let frame_socket = if acceleration {
|
||||
match plane::socketpair() {
|
||||
Ok((main_end, host_end)) => {
|
||||
config.frame_socket_fd = Some(plane::FRAME_SOCKET_CHILD_FD);
|
||||
Some((main_end, host_end))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create the accelerated frame socket, falling back to software frames: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
#[cfg(all(target_os = "macos", feature = "accelerated_paint"))]
|
||||
let frame_service = if acceleration {
|
||||
let name = format!("art.graphite.Graphite.cef-frames.{}.{:x}", std::process::id(), rand::random::<u64>());
|
||||
match plane::create_service(&name) {
|
||||
Ok(port) => {
|
||||
config.frame_service = Some(name);
|
||||
Some(port)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create the accelerated frame service, falling back to software frames: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
command.arg(config.to_arg());
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
platform::linux::setup_command(
|
||||
&mut command,
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
frame_socket.as_ref().map(|(_, host_end)| {
|
||||
use std::os::fd::AsRawFd;
|
||||
host_end.as_raw_fd()
|
||||
}),
|
||||
);
|
||||
|
||||
let mut child = command.spawn().map_err(UiError::Spawn)?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let job = match platform::win::KillOnCloseJob::assign(&child) {
|
||||
Ok(job) => Some(job),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to assign the CEF host to a job object (orphan prevention degraded): {e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "accelerated_paint"))]
|
||||
let frame_plane = frame_socket.map(|(main_end, host_end)| {
|
||||
drop(host_end);
|
||||
plane::PlaneReceiver::new(main_end)
|
||||
});
|
||||
|
||||
let (hello_tx, hello_rx) = mpsc::channel();
|
||||
let accept_thread = std::thread::Builder::new().name("cef-host-accept".to_string()).spawn(move || {
|
||||
let _ = hello_tx.send(server.accept());
|
||||
});
|
||||
if let Err(e) = accept_thread {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(UiError::Bootstrap(format!("failed to spawn the host accept thread: {e}")));
|
||||
}
|
||||
|
||||
let deadline = Instant::now() + HOST_HELLO_TIMEOUT;
|
||||
let (event_receiver, hello) = loop {
|
||||
match hello_rx.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(Ok(accepted)) => break accepted,
|
||||
Ok(Err(e)) => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(UiError::Handshake(format!("failed to accept the host connection: {e}")));
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => {
|
||||
if let Ok(Some(status)) = child.try_wait() {
|
||||
return Err(UiError::HostExited(status.to_string()));
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(UiError::HandshakeTimeout);
|
||||
}
|
||||
}
|
||||
Err(RecvTimeoutError::Disconnected) => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(UiError::Handshake("the accept thread disappeared".to_string()));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let EventMessage::Hello {
|
||||
pid,
|
||||
control_sender,
|
||||
acceleration: host_acceleration,
|
||||
} = hello
|
||||
else {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(UiError::Handshake("the first message from the host was not Hello".to_string()));
|
||||
};
|
||||
tracing::info!("CEF host process connected (pid {pid})");
|
||||
if acceleration && !host_acceleration {
|
||||
tracing::warn!("UI acceleration was requested but the CEF host could not set up its frame plane; falling back to software frames");
|
||||
}
|
||||
|
||||
Ok(HostHandle {
|
||||
sender: control_sender,
|
||||
receivers: Mutex::new(Some(InstanceReceivers {
|
||||
events: event_receiver,
|
||||
#[cfg(all(target_os = "linux", feature = "accelerated_paint"))]
|
||||
frame_plane,
|
||||
#[cfg(all(target_os = "macos", feature = "accelerated_paint"))]
|
||||
frame_plane: frame_service.map(plane::PlaneReceiver::new),
|
||||
})),
|
||||
child: Arc::new(Mutex::new(child)),
|
||||
shutting_down: Arc::new(AtomicBool::new(false)),
|
||||
died_reported: Arc::new(AtomicBool::new(false)),
|
||||
host_acceleration,
|
||||
#[cfg(target_os = "windows")]
|
||||
_job: job,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn start_instance(handle: &HostHandle, surface: FrameSurface, events: EventQueue) -> Result<mpsc::Receiver<()>, UiError> {
|
||||
let receive_side = match handle.receivers.lock() {
|
||||
Ok(mut receive_side) => receive_side.take(),
|
||||
Err(_) => None,
|
||||
};
|
||||
let Some(receive_side) = receive_side else {
|
||||
return Err(UiError::InstanceLimit);
|
||||
};
|
||||
|
||||
let (shutdown_complete_sender, shutdown_complete_receiver) = mpsc::channel();
|
||||
let consumer = FrameConsumer::new(surface, events.clone(), handle.sender.clone());
|
||||
|
||||
#[cfg(all(any(target_os = "linux", target_os = "macos"), feature = "accelerated_paint"))]
|
||||
if let Some(receiver) = receive_side.frame_plane
|
||||
&& handle.host_acceleration
|
||||
{
|
||||
let consumer = consumer.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("cef-frames".to_string())
|
||||
.spawn(move || crate::frames::receive::plane_receiver_loop(receiver, consumer))
|
||||
.map_err(|e| UiError::Bootstrap(format!("failed to spawn the frame receiver thread: {e}")))?;
|
||||
}
|
||||
|
||||
{
|
||||
let receiver = receive_side.events;
|
||||
let shutting_down = handle.shutting_down.clone();
|
||||
let died_reported = handle.died_reported.clone();
|
||||
let events = events.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("cef-host".to_string())
|
||||
.spawn(move || event_receiver_loop(receiver, consumer, events, shutting_down, died_reported, shutdown_complete_sender))
|
||||
.map_err(|e| UiError::Bootstrap(format!("failed to spawn the host event receiver thread: {e}")))?;
|
||||
}
|
||||
|
||||
{
|
||||
let child = handle.child.clone();
|
||||
let shutting_down = handle.shutting_down.clone();
|
||||
let died_reported = handle.died_reported.clone();
|
||||
let events = events.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("cef-host-supervisor".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
let status = match child.lock() {
|
||||
Ok(mut child) => child.try_wait(),
|
||||
Err(_) => return,
|
||||
};
|
||||
match status {
|
||||
Ok(None) => std::thread::sleep(Duration::from_millis(100)),
|
||||
Ok(Some(status)) => {
|
||||
if shutting_down.load(Ordering::SeqCst) {
|
||||
tracing::debug!("CEF host exited during shutdown: {status}");
|
||||
} else {
|
||||
report_host_died(&died_reported, &events, &format!("CEF host process exited unexpectedly: {status}"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
Err(_) => return,
|
||||
}
|
||||
}
|
||||
})
|
||||
.map_err(|e| UiError::Bootstrap(format!("failed to spawn the host supervisor thread: {e}")))?;
|
||||
}
|
||||
|
||||
Ok(shutdown_complete_receiver)
|
||||
}
|
||||
|
||||
fn report_host_died(died_reported: &AtomicBool, events: &EventQueue, message: &str) {
|
||||
if died_reported.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
tracing::error!("{message}");
|
||||
events.terminate(UiEvent::Crashed);
|
||||
}
|
||||
|
||||
fn event_receiver_loop(
|
||||
receiver: IpcReceiver<EventMessage>,
|
||||
consumer: FrameConsumer,
|
||||
events: EventQueue,
|
||||
shutting_down: Arc<AtomicBool>,
|
||||
died_reported: Arc<AtomicBool>,
|
||||
shutdown_complete_sender: mpsc::Sender<()>,
|
||||
) {
|
||||
let mut newest_frame: Option<PendingFrame> = None;
|
||||
let mut segments = SegmentTable::new();
|
||||
|
||||
loop {
|
||||
let message = match receiver.recv() {
|
||||
Ok(message) => message,
|
||||
Err(ipc_channel::IpcError::Io(ref io)) if io.kind() == std::io::ErrorKind::Interrupted => continue,
|
||||
Err(e) => {
|
||||
if !shutting_down.load(Ordering::SeqCst) {
|
||||
report_host_died(&died_reported, &events, &format!("Lost connection to the CEF host process: {e:?}"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
handle_message(message, &events, &shutting_down, &died_reported, &shutdown_complete_sender, &mut newest_frame, &mut segments);
|
||||
|
||||
loop {
|
||||
match receiver.try_recv() {
|
||||
Ok(message) => handle_message(message, &events, &shutting_down, &died_reported, &shutdown_complete_sender, &mut newest_frame, &mut segments),
|
||||
Err(ipc_channel::TryRecvError::Empty) => break,
|
||||
Err(ipc_channel::TryRecvError::IpcError(ipc_channel::IpcError::Io(ref io))) if io.kind() == std::io::ErrorKind::Interrupted => break,
|
||||
Err(ipc_channel::TryRecvError::IpcError(e)) => {
|
||||
if !shutting_down.load(Ordering::SeqCst) {
|
||||
report_host_died(&died_reported, &events, &format!("Lost connection to the CEF host process: {e:?}"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(frame) = newest_frame.take() {
|
||||
consumer.deliver_pending(frame, &segments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_message(
|
||||
message: EventMessage,
|
||||
events: &EventQueue,
|
||||
shutting_down: &AtomicBool,
|
||||
died_reported: &AtomicBool,
|
||||
shutdown_complete_sender: &mpsc::Sender<()>,
|
||||
newest_frame: &mut Option<PendingFrame>,
|
||||
segments: &mut SegmentTable,
|
||||
) {
|
||||
match message {
|
||||
EventMessage::Hello { .. } => tracing::error!("Unexpected second Hello from the CEF host"),
|
||||
EventMessage::BrowserCreated => tracing::info!("CEF host created the browser"),
|
||||
EventMessage::InitFailed(e) => {
|
||||
tracing::error!("CEF initialization failed in the host process: {e}");
|
||||
died_reported.store(true, Ordering::SeqCst);
|
||||
events.terminate(UiEvent::Failure(e.to_string()));
|
||||
}
|
||||
EventMessage::WebCommunicationInitialized => events.send(UiEvent::Ready),
|
||||
EventMessage::WebMessage(message) => events.send(UiEvent::Message(message)),
|
||||
EventMessage::CursorChange(cursor) => events.send(UiEvent::Cursor(cursor)),
|
||||
EventMessage::AdvertiseFrameSegment { index, shm } => segments.advertise(index, shm),
|
||||
EventMessage::SoftwareFrame { seq, segment, width, height } => {
|
||||
let frame = PendingFrame::Software { seq, segment, width, height };
|
||||
if newest_frame.as_ref().is_none_or(|newest| newest.seq() < frame.seq()) {
|
||||
*newest_frame = Some(frame);
|
||||
}
|
||||
}
|
||||
#[cfg(all(target_os = "windows", feature = "accelerated_paint"))]
|
||||
EventMessage::AcceleratedFrame {
|
||||
seq,
|
||||
handle,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
content,
|
||||
} => {
|
||||
let frame = PendingFrame::Accelerated(plane::WireFrame::new(seq, handle, width, height, format, content));
|
||||
if newest_frame.as_ref().is_none_or(|newest| newest.seq() < frame.seq()) {
|
||||
*newest_frame = Some(frame);
|
||||
}
|
||||
}
|
||||
EventMessage::ShutdownComplete => {
|
||||
shutting_down.store(true, Ordering::SeqCst);
|
||||
let _ = shutdown_complete_sender.send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
111
desktop/ui/src/resources.rs
Normal file
111
desktop/ui/src/resources.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use std::fs::File;
|
||||
#[cfg(feature = "embedded_resources")]
|
||||
use std::io;
|
||||
use std::io::Read;
|
||||
use std::path::{Component, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Resource {
|
||||
pub(crate) reader: ResourceReader,
|
||||
pub(crate) mimetype: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ResourceReader {
|
||||
#[cfg(feature = "embedded_resources")]
|
||||
Embedded(io::Cursor<&'static [u8]>),
|
||||
File(Arc<File>),
|
||||
}
|
||||
impl Read for ResourceReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
match self {
|
||||
#[cfg(feature = "embedded_resources")]
|
||||
ResourceReader::Embedded(cursor) => cursor.read(buf),
|
||||
ResourceReader::File(file) => file.as_ref().read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum WebResources {
|
||||
Embedded,
|
||||
External(PathBuf),
|
||||
}
|
||||
|
||||
pub(crate) fn load(path: PathBuf) -> Option<Resource> {
|
||||
if path.components().any(|component| matches!(component, Component::ParentDir)) {
|
||||
tracing::error!("Rejected resource path with a parent directory component: {path:?}");
|
||||
return None;
|
||||
}
|
||||
|
||||
let resources = if cfg!(feature = "embedded_resources") {
|
||||
WebResources::Embedded
|
||||
} else {
|
||||
match std::env::var("GRAPHITE_RESOURCES") {
|
||||
Ok(dir) => WebResources::External(dir.into()),
|
||||
Err(_) => {
|
||||
tracing::error!("GRAPHITE_RESOURCES must point to the frontend assets when embedded resources are disabled");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
match resources {
|
||||
WebResources::Embedded => {
|
||||
#[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(io::Cursor::new(file.contents())),
|
||||
mimetype,
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
#[cfg(not(feature = "embedded_resources"))]
|
||||
{
|
||||
tracing::error!("Embedded resources requested but the embedded_resources feature is disabled");
|
||||
None
|
||||
}
|
||||
}
|
||||
WebResources::External(dir) => {
|
||||
let file_path = dir.join(path.strip_prefix("/").unwrap_or(&path));
|
||||
if file_path.exists()
|
||||
&& file_path.is_file()
|
||||
&& let Ok(file) = std::fs::File::open(file_path)
|
||||
{
|
||||
return Some(Resource {
|
||||
reader: ResourceReader::File(file.into()),
|
||||
mimetype,
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
6
desktop/ui/src/utility.rs
Normal file
6
desktop/ui/src/utility.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub unsafe fn pointer_to_string(pointer: *mut cef::sys::_cef_string_utf16_t) -> String {
|
||||
let str = unsafe { (*pointer).str_ };
|
||||
let len = unsafe { (*pointer).length };
|
||||
let slice = unsafe { std::slice::from_raw_parts(str, len) };
|
||||
String::from_utf16(slice).unwrap()
|
||||
}
|
||||
69
desktop/ui/src/view.rs
Normal file
69
desktop/ui/src/view.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct ViewInfo {
|
||||
width: u32,
|
||||
height: u32,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl ViewInfo {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { width: 1, height: 1, scale: 1. }
|
||||
}
|
||||
|
||||
pub(crate) fn apply_update(&mut self, update: ViewInfoUpdate) {
|
||||
match update {
|
||||
ViewInfoUpdate::Size { width, height } if width > 0 && height > 0 => {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
ViewInfoUpdate::Scale(scale) if scale > 0. => {
|
||||
self.scale = scale;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn zoom(&self) -> f64 {
|
||||
self.scale.ln() / 1.2_f64.ln()
|
||||
}
|
||||
|
||||
pub(crate) fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
pub(crate) fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ViewInfo {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) enum ViewInfoUpdate {
|
||||
Size { width: u32, height: u32 },
|
||||
Scale(f64),
|
||||
}
|
||||
|
||||
pub(super) struct ViewInfoReceiver {
|
||||
view_info: ViewInfo,
|
||||
receiver: Receiver<ViewInfoUpdate>,
|
||||
}
|
||||
|
||||
impl ViewInfoReceiver {
|
||||
pub(super) fn new(receiver: Receiver<ViewInfoUpdate>) -> Self {
|
||||
Self { view_info: ViewInfo::new(), receiver }
|
||||
}
|
||||
|
||||
pub(super) fn current(&mut self) -> ViewInfo {
|
||||
for update in self.receiver.try_iter() {
|
||||
self.view_info.apply_update(update);
|
||||
}
|
||||
self.view_info
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user