Make the data model use Item and List types universally, with nodes authored as rank-polymorphic kernels (#4335)

* Add rank polymorphism node audit classifying all 271 nodes

* Implement StaticType for Item<T>

* Generate Item and mapped List wire variants for nodes declaring an Item<T> primary input

* Migrate nine nodes to Item element-wise kernels, dissolving the blending trait boilerplate

* Document the Item kernel implementation and staging plan

* Route Item<Vector> through TaggedValue::TypeDefault

* Add executor integration tests covering the Item and List wire variants

* Collapse element-wise Item/List wire pairs to the List form for conversion insertion

* Migrate sixteen vector modifier nodes to Item element-wise kernels

* Migrate Sample Image, Extend Image to Bounds, and Dehaze to Item element-wise kernels

* Fix bevel_with_transform test to actually exercise the transform attribute

* Implement From<T> for Item<T>

* Register PromoteNode rank adapters wrapping bare values into Item wires

* Insert PromoteNode adapters for Item/List wire pair fields in the preprocessor

* Define a real promote node backing the PromoteNode registry identifiers

* Zip ranked Item connectors by frame slot in the mapped element-wise variant

* Register ItemToListNode singleton raise adapters

* Resolve Item wires against List connectors by inserting promotion adapters at construction

* Rank the Offset Points distance connector and prove mixed-rank resolution end-to-end

* Implement Clampable for Item and List wires with per-variant clamp bounds

* Rank the Round Corners radius connector, exercising hard bounds on a ranked wire

* Implement ApplyTransform for Item

* Add Item wire implementations to the Transform node, keeping rank-0 chains rank 0

* Detect element-wise nodes by lazy primary connectors declaring Output = Item

* Convert Transform to an Item kernel with ranked parameters, delivering the broadcast milestone

* Rename Apply Transform to Bake Transform, baking item transforms on Vector, DAffine2, and DVec2

* Promote bare wires onto Item connectors at resolution via WrapItemNode adapters

* Rank the numeric, vector, and boolean parameters across the migrated element-wise nodes

* Rank the enum, integer, and seed parameters, registering their rank adapters via a consolidated macro

* Amend the audit with the DashPattern value type resolution

* Migrate the string family to Item element-wise kernels

* Unwrap Item wires into bare legacy connectors at resolution via UnwrapItemNode adapters

* Shadow owned node parameters in bodies instead of mut in signatures

* Migrate the math family and string measure nodes to Item element-wise kernels

* Convert the comparison and clamp nodes to Item kernels, dropping unreachable &str rows

* Flat-map expander kernels returning List under the mapped variant's frame

* Migrate the expander nodes to Item kernels flat-mapping under the frame

* Remove the unused peel_list helper

* Rank the raster adjustment and blending kernels, recontextualizing shader nodes onto an Item stand-in

Migrate the 16 adjustment nodes, Mix, Color Overlay, and Gradient Map from whole-List kernels to rank-0 Item kernels, letting the macro derive the List-mapped (zip) variants. Move the Adjust and Blend per-element seams off List onto the element types (add the Raster<CPU> impls, drop the now-dead List impls).

Shader nodes keep their bodies verbatim: PerPixelAdjust re-emits the identical kernel against a transparent no_std Item stand-in, so every Item<T> connector and .element() call resolves to a zero-cost identity on the GPU while the uniform buffer stays bare repr(C). The macro peels Item off ranked uniform params, wraps the fetched texel and uniforms at the entry point, and unwraps the result. This drops the shader_node/Item incompatibility guard. Register rank adapters for the adjustment enums.

* Update the rank polymorphism roadmap for the landed shader-node and adjustments chunk

* Rename the GPU Item stand-in to ShaderItem, aliased as Item at its shader-node import sites

* Flip the vector shape generators to emit rank-0 Item<Vector>

The shape generators (Rectangle, Circle, Ellipse, Arc, Spiral, Polygon, Star, Arrow, Line, Grid, QR Code) each produced exactly one shape wrapped in a singleton List<Vector>. Emit Item<Vector> directly so they connect to the rank-0 content connector of the migrated Transform node. Downstream List consumers receive the value through the existing Item to List promotion.

Relax the element-wise validation so a `()` (generator) primary may return Item<T> without being element-wise. Adapt the Repeat on Points test, which still takes a List content connector, by raising the generator's Item output through a singleton wrapper node.

* Parse ranked Item<T> parameter defaults against the bare element type

A ranked `Item<T>` parameter's default value is a bare, unranked `T` (promoted to the wire at resolution), but the preprocessor was handed the wrapped `Item<T>` type and could not parse the literal, flooding the console with warnings and dropping the defaults. Key the field's default_type metadata off the peeled element type for concrete ranked parameters, leaving generic `Item<T>` primaries and skip_impl nodes untouched.

* Parse an element-wise primary's scalar default against the bare element type

An element-wise node's primary reports its default_type as the List wire form so an unconnected primary defaults to an empty list. But when the primary carries a scalar `#[default]` (such as Root's radicand), that literal must parse as a bare element, not a List. Key the primary's default_type off the bare element type when it has a Default value source, keeping the List form otherwise.

* Add the DashPattern value type for stroke dash sequences

Introduce a rank-0 DashPattern value type (a Vec<f64> of alternating dash and gap lengths) so a stroke's dash pattern is a single frameable value rather than a rank-1 List<f64>. Register it as an auto-generated TaggedValue variant, parse its default from a comma or space separated string, and register its rank adapters. Not yet wired into the Stroke node.

* Rank the Fill and Stroke nodes element-wise and give Stroke a DashPattern connector

Migrate Fill and Stroke to element-wise Item<V> primaries (over Vector and Graphic element types) via a new element-level VectorItemMut trait, so styling one shape yields one shape and rank is preserved instead of promoting the input to a singleton List and emitting a List. The macro derives the List-mapped variant for genuine collections.

Wire the Stroke dash sequence to the new rank-0 DashPattern value type, collapsing the old content x paint x dash cartesian and dropping the IntoF64Vec trait. Update the stroke properties dash widget, the drawing tool, and graph-operation plumbing to read and write DashPattern, and migrate legacy F64Array, F64, and String dash inputs on document open.

Assign Colors stays a whole-collection node: each element's gradient position depends on its index among all siblings, which the element frame does not expose, so it keeps its List primary and the VectorListIterMut trait.

* Register rank adapters for the ranked Stroke enum parameters

The element-wise Stroke node ranks its align, cap, and paint order parameters as Item<StrokeAlign>, Item<StrokeCap>, and Item<PaintOrder>, but those enums lacked promotion adapters, so a bare default enum value could not be promoted to its Item wire and no Stroke variant resolved ("No construct found for node"). Register their rank adapters alongside StrokeJoin.

* Display Item wires in the Data panel without a List's ID column

Add a TableItemLayout impl for Item<T> and recognize Item wire types when introspecting graph data. An Item holds a single element, so it renders as a one-row table of the element plus its attributes with no leading index column, and it labels as its element type T rather than a List's T[]. Add ItemAttributeValues::get_any for the attribute widget dispatch.

* Register MonitorNode for Item wire types so the Data panel introspects them directly

Graph introspection wraps the inspected output in a generic MonitorNode typed to the wire. Without Item<T> monitor registrations, an Item<Vector> output could only be monitored after an Item to List promotion, so the Data panel captured and displayed a List<Vector> despite the connector being Item<Vector>. Register monitors for the Item types the element-wise nodes emit, and add the matching Data panel downcast entries.

* Color and double Item/List wires and cleave layer-stack connectors in the node graph

* Route wire color and rank through hidden nodes and refresh them on type changes

* Rework the DashPattern connector conversions with element-wise promotion and an explicit reducer node

* Rank the remaining value, context, aggregation, and transform nodes onto Item<T> wires

* Back DashPattern with a List<f64> so the Data panel can introspect its lengths

* Carry a single Item<T> through varargs so the Read context nodes emit Item<T> not List<T>

* Relax rank validation for aggregation shapes, add element adapters, and match variants by fewest promotions

* Rank the remaining bare and unnecessarily-List connectors across the node catalog

* Add Graphic::None and the FillChoice paint value, making colors and gradients plain values

* Rename GradientStops to Gradient and the legacy Gradient/Fill structs to LegacyGradient/LegacyFill

* Restore generator frame-from-params ranking to the roadmap as a planned stage

* Rename the ranked-field adapter identifier from PromoteNode to FieldAdapterNode to reflect its full contract

* Unload only the wires whose displayed style changed when types update

* Peel wire rank in the editor's semantic type checks so rank-0 layers are recognized

* Restore the whole-List Transform variant so rank-1 content wires resolve again

* Register the Item wire forms for the Memoize and Context Modification infrastructure nodes

* Give every ranked connector a field adapter and add numeric cast variants for legacy wires

* Key a ranked param's type default off its Item wire form when no literal default exists

* Inherit the layer's content value when splicing a node into an empty chain

* Migrate stale List-form TypeDefault inputs to the definition's current default

* Generate the mapped wire variant only when the element-wise node has a frame source

* Let a bare wire feed a List connector via a wrap-raise adapter, costed as two rank steps

* Add a zip companion to the whole-List Transform so ranked List parameters pair per slot

* Add the Sum, Average, Minimum, Maximum, Any, and All list reducers

* Convert the measure family to element-wise Item kernels per the audit classification

* Prefer the bare element value over the Item type default so ranked params keep their widgets

* Rename GradientStopsUI to GradientUI

* Split Fill's optional transform into a _has_transform bool and a ranked _transform matrix

* Rename the migration-only OptionalDAffine2 TaggedValue to LegacyOptionalDAffine2

* Flow byte buffers as Item<Resource> instead of List<u8> across the byte nodes

* Macro-generate the list-content wire variant, retiring the hand-written Transform-zip, Area, and Centroid companions

* Let ()-primary generators take ranked params and frame over them via the mapped variant, ranking Circle's radius

* Rank the vector shape generators' params to Item, adding a rank-aware input grab to the introspection harness

* Rank the value, color, and text generator params to Item

* Rank the raster, web-request, and context-reader generator params to Item

* Fix the repeat and brush test wirings left behind by the param-ranking sweeps

* Delete the vestigial Some, Unwrap Option, and Size Of debug nodes

* Delete the Attach Attribute node, folding its role into Write Attribute

* Add the Filter and Sort list companion nodes

* Guard the removed-definition migration swap target with a test

* Add the Box Corners value type in place of the rectangle corner radius list

* Split Text to Vector's per-glyph mode into a Text to Vector Glyphs node

* Rank the Combine Channels node's channel connectors to Item

* Make Map Points an element-wise node

* Delete the deprecated Upload Texture node

* Update the implementation roadmap to reflect the landed stages

* Let monitor introspection read rank-0 wires, locking in the layer coercion promotion path

* Prefer the rank-0 default when disconnecting a rank-capable input

* Make Path Modify an element-wise node

* Wrap node paths in a NodeIdPath newtype so they flow as a single Item

* Give Item<Raster<CPU>> a default so an unconnected Brush background resolves

* Stop the Brush node from setting layer attributes its paint operation doesn't produce

* Present-gate Flatten Path's adopted layer path like its fill and stroke

* Gate carried layer attributes on static column presence, not runtime values

* Give the remaining graphic Item<T> types a default so unconnected primaries resolve

* Dispatch a ranked param's Properties widget from its rank-0 element type

* Make Extract Transform an element-wise node, restoring the Origins to Polyline body

* Rename Flatten Path to Combine Paths

* Stamp Legacy Layer Extend's adopted layer path as a readable NodeIdPath

* Drop the dead List<u8> and List<NodeId> wire rows

* Rank Flatten Graphic's Fully Flatten toggle to Item

* Update the implementation roadmap with the endgame scope

* Make Combine Paths a reducer that collapses the whole frame into one path

* Stop type-converter nodes from carrying the source's unrelated attributes

* Format the Origins to Polyline regression test

* Wrap the Brush node's trace in a BrushTrace newtype so it flows as one value

* Make Switch a framed element-wise select, bundling whole collections

* Widen and align element-type coverage across the list and graphic nodes

* Register the compiler's cache chain pair for every ranked enum and newtype wire

* Fix wire colors for Passthrough outputs, bundled lists, and bools, and widen list wires

* Represent List wire types structurally with Type::List, replacing name-parsed rank promotion

* Treat scope and data fields as environment, rank scope wires as Item, and feed the render boundary through a context vararg

* Delete the vestigial Clone debug node

* Reinstate Upload Texture as an element-wise node and fix the GPU variants' scope executor and rank adapters

* Rename Combine Paths back to Flatten Path, deferring that rename to its own PR

* Deduplicate the promotion adapter registrations into the field adapter macro

* Rank Write Attribute's value connector to Item<AttributeValueDyn>, retiring the UnwrapItem bridge

* Vertical wire styling

* Store the editor layer path attribute as a bare NodeIdPath, not an Item<NodeIdPath>

* Rank Context Modification's features connector to Item<ContextFeatures>, dropping the dead memoize row

* Rank Path Modify's modification parameter to Item<Box<VectorModification>>

* Rename the field adapter node family to input adapter

* Drop the dead bare scalar rows from Context Modification's implementations list

* Move the dynamic executor's test module into its own file

* Drop the registry's unreachable bare rows for Memoize, the cache chain, and ConvertNode

* Materialize stored TaggedValues as ranked Item wires at the source

* Remove the bare-wire promotion and adapter machinery made dead by ranked value materialization

* Plant the input adapter for List-only inputs, composing position conversion from standard rows

* Consolidate Into/Convert conversions into the input adapter umbrella and rename the rank adapter identifiers

* Fix grouped layers gaining a phantom None stack element from the FillChoice default hijacking every List<Graphic> disconnect

* Enforce ranked node inputs in the macro, rejecting bare wire declarations

* Remove the unit Context => () machinery rows, leaving () purely as the no-primary sentinel

* Add a --signatures rank-audit mode to node-docs for the ranked-wire migration

* Remove the node-docs --signatures rank-audit mode now that ranked wires are enforced

* Migrate legacy no-color values on the Black & White, Color Overlay, and Empty Image color inputs

* Rewrite the element-wise accessor wire type at the primary input, not raw index 0

* Register the cache chain for Resource wires, replacing the lone hand-written Monitor row

* Gate the remaining Raster<GPU> registry rows behind the gpu feature

* Let List<DVec2> wires erase to ListDyn for the attribute reader and element counter

* Rename Extract Element to Item at Index, Count Elements to List Length, and Omit Element to Remove at Index

* Store paint picks as plain color/gradient values, removing the FillChoice value type

* Code review restructuring

* Sort by the consumed sort_key attribute or natural element order, adding the Sort Key node

* Remove the new list-combinator and reducer nodes to defer them to a follow-up PR

* Parse Fill and Stroke color defaults through the paint wire's Graphic element

* Emit ranked implementation-row default types structurally so their element TypeIds survive to default-literal parsing

* Exempt the deliberate no-paint choice from the stale List-form TypeDefault migration

* Migrate the legacy 4-input Fill directly to the split has-transform shape

* Upgrade the demo artwork

* Fix the valid AI review findings: Item eq/hash contract, table-era no-paint migration, quantize List rows, and other smaller issues

* Remove the rank polymorphism working documents

* Hash Item attribute values directly instead of debug-formatting them, speeding up cached evaluation

* Replace the data panel's dead bare-wire downcast arms with full coverage of the ranked monitor row types

* Derive PartialEq for Item now that attributes participate in equality

* Extend the data panel's attribute dispatchers with the newly supported scalar and choice enum types

* Add List monitor rows for the framed numeric conversion outputs so inspecting them resolves, with matching data panel arms
This commit is contained in:
Keavon Chambers
2026-09-08 16:03:01 +00:00
committed by Timon
parent 296185b7fc
commit a708a54492
3257 changed files with 766342 additions and 1829 deletions
+74
View File
@@ -0,0 +1,74 @@
[package]
name = "graphite-desktop"
version = "0.1.0"
description = "Graphite Desktop"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[[bin]]
name = "graphite"
path = "src/main.rs"
[features]
default = ["recommended", "embedded_resources"]
recommended = ["gpu", "accelerated_paint"]
embedded_resources = ["graphite-desktop-ui/embedded_resources"]
gpu = ["graphite-desktop-wrapper/gpu"]
accelerated_paint = ["graphite-desktop-ui/accelerated_paint"]
[dependencies]
# Local dependencies
graphite-desktop-wrapper = { path = "wrapper" }
graphite-desktop-ui = { path = "ui" }
wgpu = { workspace = true }
winit = { workspace = true, features = [
"wayland-csd-adwaita-notitlebar",
"serde",
] }
thiserror = { workspace = true }
futures = { workspace = true }
tokio = { workspace = true }
tracing-subscriber = { workspace = true }
tracing = { workspace = true }
dirs = { workspace = true }
ron = { workspace = true }
bytemuck = { workspace = true }
glam = { workspace = true }
vello = { workspace = true }
derivative = { workspace = true }
rfd = { workspace = true }
open = { workspace = true }
lzma-rust2 = { workspace = true }
serde = { workspace = true }
rand = { workspace = true, features = ["thread_rng"] }
clap = { workspace = true, features = ["derive"] }
interprocess = "2.4.2"
fd-lock = "4.0.4"
ctrlc = "3.5.1"
window_clipboard = "0.5"
# Windows-specific dependencies
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.62.2", features = [
"Win32_Foundation",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
"Win32_System_Com",
"Win32_System_Console",
"Win32_UI_Controls",
"Win32_UI_WindowsAndMessaging",
"Win32_UI_HiDpi",
"Win32_UI_Shell",
] }
# macOS-specific dependencies
[target.'cfg(target_os = "macos")'.dependencies]
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 }
muda = { git = "https://github.com/timon-schelling/muda.git", rev = "e5bc28bbd6781b18afbfc237981f9ef47eddf863", default-features = false }
@@ -0,0 +1,11 @@
[Desktop Entry]
Name=Graphite
GenericName=Vector & Raster Graphics Editor
Comment=Open-source vector & raster graphics editor. Featuring node based procedural nondestructive editing workflow.
Exec=graphite
Terminal=false
Type=Application
Icon=art.graphite.Graphite
Categories=Graphics;VectorGraphics;RasterGraphics;
Keywords=graphite;editor;vector;raster;procedural;design;
StartupWMClass=art.graphite.Graphite
@@ -0,0 +1,16 @@
[package]
name = "graphite-desktop-bundle"
version = "0.0.0"
description = "Graphite Desktop Bundle"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[dependencies]
cef-dll-sys = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
serde = { workspace = true }
plist = { version = "*" }
@@ -0,0 +1,10 @@
fn main() {
println!("cargo:rerun-if-env-changed=CARGO_PROFILE");
println!("cargo:rerun-if-env-changed=PROFILE");
let profile = std::env::var("CARGO_PROFILE").or_else(|_| std::env::var("PROFILE")).unwrap();
println!("cargo:rustc-env=CARGO_PROFILE={profile}");
println!("cargo:rerun-if-env-changed=DEP_CEF_DLL_WRAPPER_CEF_DIR");
let cef_dir = std::env::var("DEP_CEF_DLL_WRAPPER_CEF_DIR").unwrap();
println!("cargo:rustc-env=CEF_PATH={cef_dir}");
}
@@ -0,0 +1,76 @@
#![cfg_attr(target_os = "linux", allow(unused))] // TODO: Remove this when bundling for linux is implemented
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
pub(crate) const APP_NAME: &str = "Graphite";
pub(crate) const APP_BIN: &str = "graphite";
pub(crate) fn workspace_path() -> PathBuf {
PathBuf::from(env!("CARGO_WORKSPACE_DIR"))
}
fn profile_name() -> &'static str {
let mut profile = env!("CARGO_PROFILE");
if profile == "debug" {
profile = "dev";
}
profile
}
pub(crate) fn profile_path() -> PathBuf {
workspace_path().join(format!("target/{}", env!("CARGO_PROFILE")))
}
pub(crate) fn cef_path() -> PathBuf {
PathBuf::from(env!("CEF_PATH"))
}
pub(crate) fn build_bin(package: &str, bin: Option<&str>, features: Option<&str>) -> Result<PathBuf, Box<dyn Error>> {
let mut args = vec!["build", "--package", package, "--profile", profile_name()];
if let Some(bin) = bin {
args.push("--bin");
args.push(bin);
}
if let Some(features) = features {
args.push("--features");
args.push(features);
}
run_command("cargo", &args)?;
let profile_path = profile_path();
let mut bin_path = if let Some(bin) = bin { profile_path.join(bin) } else { profile_path.join(APP_BIN) };
if cfg!(target_os = "windows") {
bin_path.set_extension("exe");
}
Ok(bin_path)
}
pub(crate) fn run_command(program: &str, args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
let status = Command::new(program).args(args).stdout(Stdio::inherit()).stderr(Stdio::inherit()).status()?;
if !status.success() {
return Err(format!("Command '{}' with args {:?} failed with status: {}", program, args, status).into());
}
Ok(())
}
pub(crate) fn clean_dir(dir: &Path) {
if dir.exists() {
fs::remove_dir_all(dir).unwrap();
}
fs::create_dir_all(dir).unwrap();
}
pub(crate) fn copy_dir(src: &Path, dst: &Path) {
fs::create_dir_all(dst).unwrap();
for entry in fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let dst_path = dst.join(entry.file_name());
if entry.file_type().unwrap().is_dir() {
copy_dir(&entry.path(), &dst_path);
} else {
fs::copy(entry.path(), &dst_path).unwrap();
}
}
}
@@ -0,0 +1,20 @@
use crate::common::*;
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let app_bin = build_bin("graphite-desktop-platform-linux", None, None)?;
// TODO: Implement bundling for linux
// TODO: Consider adding more useful cli
let args: Vec<String> = std::env::args().collect();
if let Some(pos) = args.iter().position(|a| a == "open") {
let extra_args: Vec<&str> = args[pos + 1..].iter().map(|s| s.as_str()).collect();
run_command(&app_bin.to_string_lossy(), &extra_args).expect("failed to open app");
} else {
eprintln!("Binary built and placed at {}", app_bin.to_string_lossy());
eprintln!("Bundling for Linux is not yet implemented.");
eprintln!("You can still start the app with the `open` subcommand. `cargo run -p graphite-desktop-bundle -- open`");
}
Ok(())
}
@@ -0,0 +1,222 @@
use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
use crate::common::*;
const APP_ID: &str = "art.graphite.Graphite";
const ICONS_FILE_NAME: &str = "graphite.icns";
const EXEC_PATH: &str = "Contents/MacOS";
const FRAMEWORKS_PATH: &str = "Contents/Frameworks";
const RESOURCES_PATH: &str = "Contents/Resources";
const CEF_FRAMEWORK: &str = "Chromium Embedded Framework.framework";
const GRAPHITE_DOCUMENT_TYPE: &str = "art.graphite.document";
const GRAPHITE_FILE_EXTENSION: &str = "graphite";
const GRAPHITE_MIME_TYPE: &str = "application/graphite+json";
pub fn main() -> Result<(), Box<dyn Error>> {
let app_bin = build_bin("graphite-desktop-platform-mac", None, Some("main"))?;
let helper_bin = build_bin("graphite-desktop-platform-mac", Some("helper"), Some("helper"))?;
let profile_path = profile_path();
let app_dir = bundle(&profile_path, &app_bin, &helper_bin);
// TODO: Consider adding more useful cli
let args: Vec<String> = std::env::args().collect();
if let Some(pos) = args.iter().position(|a| a == "open") {
let executable = app_dir.join(EXEC_PATH).join(APP_NAME);
let extra_args: Vec<&str> = args[pos + 1..].iter().map(|s| s.as_str()).collect();
run_command(&executable.to_string_lossy(), &extra_args).expect("failed to open app");
}
Ok(())
}
fn bundle(out_dir: &Path, app_bin: &Path, helper_bin: &Path) -> PathBuf {
let app_dir = out_dir.join(APP_NAME).with_extension("app");
clean_dir(&app_dir);
create_app(&app_dir, APP_ID, APP_NAME, app_bin, false);
for helper_type in [None, Some("GPU"), Some("Renderer")] {
let helper_id_suffix = helper_type.map(|t| format!(".{t}")).unwrap_or_default();
let helper_id = format!("{APP_ID}.helper{helper_id_suffix}");
let helper_name_suffix = helper_type.map(|t| format!(" ({t})")).unwrap_or_default();
let helper_name = format!("{APP_NAME} Helper{helper_name_suffix}");
let helper_app_dir = app_dir.join(FRAMEWORKS_PATH).join(&helper_name).with_extension("app");
create_app(&helper_app_dir, &helper_id, &helper_name, helper_bin, true);
}
copy_dir(&cef_path().join(CEF_FRAMEWORK), &app_dir.join(FRAMEWORKS_PATH).join(CEF_FRAMEWORK));
let resource_dir = app_dir.join(RESOURCES_PATH);
fs::create_dir_all(&resource_dir).expect("failed to create app resource dir");
let icon_file = workspace_path().join("branding/app-icons").join(ICONS_FILE_NAME);
fs::copy(icon_file, resource_dir.join(ICONS_FILE_NAME)).expect("failed to copy icon file");
app_dir
}
fn create_app(app_dir: &Path, id: &str, name: &str, bin: &Path, is_helper: bool) {
fs::create_dir_all(app_dir.join(EXEC_PATH)).unwrap();
let app_contents_dir: &Path = &app_dir.join("Contents");
create_info_plist(app_contents_dir, id, name, is_helper).unwrap();
fs::copy(bin, app_dir.join(EXEC_PATH).join(name)).unwrap();
}
fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) -> Result<(), Box<dyn std::error::Error>> {
let info = InfoPlist {
cf_bundle_name: exec_name.to_string(),
cf_bundle_identifier: id.to_string(),
cf_bundle_display_name: exec_name.to_string(),
cf_bundle_executable: exec_name.to_string(),
cf_bundle_icon_file: if is_helper { None } else { Some(ICONS_FILE_NAME.to_string()) },
cf_bundle_info_dictionary_version: "6.0".to_string(),
cf_bundle_package_type: "APPL".to_string(),
cf_bundle_signature: "????".to_string(),
cf_bundle_version: "0.0.0".to_string(),
cf_bundle_short_version_string: "0.0".to_string(),
cf_bundle_development_region: "en".to_string(),
ls_environment: [("MallocNanoZone".to_string(), "0".to_string())].iter().cloned().collect(),
ls_file_quarantine_enabled: true,
ls_minimum_system_version: "11.0".to_string(),
ls_ui_element: if is_helper { Some("1".to_string()) } else { None },
ns_supports_automatic_graphics_switching: true,
cf_bundle_document_types: (!is_helper).then(document_types),
ut_exported_type_declarations: (!is_helper).then(exported_type_declarations),
};
let plist_file = dir.join("Info.plist");
plist::to_file_xml(plist_file, &info)?;
Ok(())
}
fn document_types() -> Vec<DocumentType> {
vec![
DocumentType {
cf_bundle_type_name: "Graphite Document".to_string(),
cf_bundle_type_role: "Editor".to_string(),
cf_bundle_type_extensions: Some(vec![GRAPHITE_FILE_EXTENSION.to_string()]),
cf_bundle_type_icon_file: Some(ICONS_FILE_NAME.to_string()),
ls_handler_rank: Some("Owner".to_string()),
ls_item_content_types: vec![GRAPHITE_DOCUMENT_TYPE.to_string()],
},
DocumentType {
cf_bundle_type_name: "SVG Image".to_string(),
cf_bundle_type_role: "Editor".to_string(),
cf_bundle_type_extensions: Some(vec!["svg".to_string()]),
cf_bundle_type_icon_file: None,
ls_handler_rank: Some("Alternate".to_string()),
ls_item_content_types: vec!["public.svg-image".to_string()],
},
DocumentType {
cf_bundle_type_name: "Image".to_string(),
cf_bundle_type_role: "Editor".to_string(),
cf_bundle_type_extensions: None,
cf_bundle_type_icon_file: None,
ls_handler_rank: Some("Alternate".to_string()),
ls_item_content_types: vec!["public.image".to_string()],
},
]
}
fn exported_type_declarations() -> Vec<ExportedTypeDeclaration> {
vec![ExportedTypeDeclaration {
ut_type_identifier: GRAPHITE_DOCUMENT_TYPE.to_string(),
ut_type_description: "Graphite Document".to_string(),
ut_type_conforms_to: vec!["public.json".to_string()],
ut_type_tag_specification: TypeTagSpecification {
public_filename_extension: vec![GRAPHITE_FILE_EXTENSION.to_string()],
public_mime_type: GRAPHITE_MIME_TYPE.to_string(),
},
}]
}
#[derive(serde::Serialize)]
struct InfoPlist {
#[serde(rename = "CFBundleName")]
cf_bundle_name: String,
#[serde(rename = "CFBundleIdentifier")]
cf_bundle_identifier: String,
#[serde(rename = "CFBundleDisplayName")]
cf_bundle_display_name: String,
#[serde(rename = "CFBundleExecutable")]
cf_bundle_executable: String,
#[serde(rename = "CFBundleIconFile")]
#[serde(skip_serializing_if = "Option::is_none")]
cf_bundle_icon_file: Option<String>,
#[serde(rename = "CFBundleInfoDictionaryVersion")]
cf_bundle_info_dictionary_version: String,
#[serde(rename = "CFBundlePackageType")]
cf_bundle_package_type: String,
#[serde(rename = "CFBundleSignature")]
cf_bundle_signature: String,
#[serde(rename = "CFBundleVersion")]
cf_bundle_version: String,
#[serde(rename = "CFBundleShortVersionString")]
cf_bundle_short_version_string: String,
#[serde(rename = "CFBundleDevelopmentRegion")]
cf_bundle_development_region: String,
#[serde(rename = "LSEnvironment")]
ls_environment: HashMap<String, String>,
#[serde(rename = "LSFileQuarantineEnabled")]
ls_file_quarantine_enabled: bool,
#[serde(rename = "LSMinimumSystemVersion")]
ls_minimum_system_version: String,
#[serde(rename = "LSUIElement")]
#[serde(skip_serializing_if = "Option::is_none")]
ls_ui_element: Option<String>,
#[serde(rename = "NSSupportsAutomaticGraphicsSwitching")]
ns_supports_automatic_graphics_switching: bool,
#[serde(rename = "CFBundleDocumentTypes")]
#[serde(skip_serializing_if = "Option::is_none")]
cf_bundle_document_types: Option<Vec<DocumentType>>,
#[serde(rename = "UTExportedTypeDeclarations")]
#[serde(skip_serializing_if = "Option::is_none")]
ut_exported_type_declarations: Option<Vec<ExportedTypeDeclaration>>,
}
#[derive(serde::Serialize)]
struct DocumentType {
#[serde(rename = "CFBundleTypeName")]
cf_bundle_type_name: String,
#[serde(rename = "CFBundleTypeRole")]
cf_bundle_type_role: String,
#[serde(rename = "CFBundleTypeExtensions")]
#[serde(skip_serializing_if = "Option::is_none")]
cf_bundle_type_extensions: Option<Vec<String>>,
#[serde(rename = "CFBundleTypeIconFile")]
#[serde(skip_serializing_if = "Option::is_none")]
cf_bundle_type_icon_file: Option<String>,
#[serde(rename = "LSHandlerRank")]
#[serde(skip_serializing_if = "Option::is_none")]
ls_handler_rank: Option<String>,
#[serde(rename = "LSItemContentTypes")]
ls_item_content_types: Vec<String>,
}
#[derive(serde::Serialize)]
struct ExportedTypeDeclaration {
#[serde(rename = "UTTypeIdentifier")]
ut_type_identifier: String,
#[serde(rename = "UTTypeDescription")]
ut_type_description: String,
#[serde(rename = "UTTypeConformsTo")]
ut_type_conforms_to: Vec<String>,
#[serde(rename = "UTTypeTagSpecification")]
ut_type_tag_specification: TypeTagSpecification,
}
#[derive(serde::Serialize)]
struct TypeTagSpecification {
#[serde(rename = "public.filename-extension")]
public_filename_extension: Vec<String>,
#[serde(rename = "public.mime-type")]
public_mime_type: String,
}
@@ -0,0 +1,17 @@
mod common;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod mac;
#[cfg(target_os = "windows")]
mod win;
fn main() {
#[cfg(target_os = "linux")]
linux::main().unwrap();
#[cfg(target_os = "macos")]
mac::main().unwrap();
#[cfg(target_os = "windows")]
win::main().unwrap();
}
@@ -0,0 +1,61 @@
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
use crate::common::*;
const EXECUTABLE: &str = "Graphite.exe";
pub fn main() -> Result<(), Box<dyn Error>> {
let app_bin = build_bin("graphite-desktop-platform-win", None, None)?;
let executable = bundle(&profile_path(), &app_bin);
// TODO: Consider adding more useful cli
let args: Vec<String> = std::env::args().collect();
if let Some(pos) = args.iter().position(|a| a == "open") {
let extra_args: Vec<&str> = args[pos + 1..].iter().map(|s| s.as_str()).collect();
run_command(&executable.to_string_lossy(), &extra_args).expect("failed to open app")
}
Ok(())
}
fn bundle(out_dir: &Path, app_bin: &Path) -> PathBuf {
let app_dir = out_dir.join(APP_NAME);
clean_dir(&app_dir);
copy_dir(&cef_path(), &app_dir);
if let Err(e) = remove_unnecessary_cef_files(&app_dir) {
eprintln!("Failed to remove unnecessary CEF files: {}", e);
}
let bin_path = app_dir.join(EXECUTABLE);
fs::copy(app_bin, &bin_path).unwrap();
bin_path
}
fn remove_unnecessary_cef_files(app_dir: &Path) -> Result<(), Box<dyn Error>> {
fs::remove_dir_all(app_dir.join("cmake"))?;
fs::remove_dir_all(app_dir.join("include"))?;
fs::remove_dir_all(app_dir.join("libcef_dll"))?;
for entry in fs::read_dir(app_dir.join("locales"))? {
let path = entry?.path();
if path.is_file() && path.file_name() != Some("en-US.pak".as_ref()) {
fs::remove_file(path)?;
}
}
fs::remove_file(app_dir.join("archive.json"))?;
fs::remove_file(app_dir.join("CMakeLists.txt"))?;
fs::remove_file(app_dir.join("bootstrapc.exe"))?;
fs::remove_file(app_dir.join("bootstrap.exe"))?;
fs::remove_file(app_dir.join("libcef.lib"))?;
fs::remove_file(app_dir.join("CREDITS.html"))?;
Ok(())
}
@@ -0,0 +1,15 @@
[package]
name = "graphite-desktop-embedded-resources"
version = "0.1.0"
description = "Graphite Desktop Embedded Resources"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[dependencies]
include_dir = { workspace = true }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(embedded_resources)'] }
@@ -0,0 +1,32 @@
const EMBEDDED_RESOURCES_ENV: &str = "EMBEDDED_RESOURCES";
const DEFAULT_RESOURCES_DIR: &str = "../../frontend/dist";
fn main() {
let mut embedded_resources: Option<String> = None;
println!("cargo:rerun-if-env-changed={EMBEDDED_RESOURCES_ENV}");
if let Ok(embedded_resources_env) = std::env::var(EMBEDDED_RESOURCES_ENV)
&& std::path::PathBuf::from(&embedded_resources_env).exists()
{
embedded_resources = Some(embedded_resources_env);
}
if embedded_resources.is_none() {
// Check if the directory `DEFAULT_RESOURCES_DIR` exists and sets the embedded_resources cfg accordingly
// Absolute path of `DEFAULT_RESOURCES_DIR` available via the `EMBEDDED_RESOURCES` environment variable
let crate_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
println!("cargo:rerun-if-changed={DEFAULT_RESOURCES_DIR}");
if let Ok(resources) = crate_dir.join(DEFAULT_RESOURCES_DIR).canonicalize()
&& resources.exists()
{
embedded_resources = Some(resources.to_string_lossy().to_string());
}
}
if let Some(embedded_resources) = embedded_resources {
println!("cargo:rustc-cfg=embedded_resources");
println!("cargo:rustc-env={EMBEDDED_RESOURCES_ENV}={embedded_resources}");
} else {
println!("cargo:warning=Resource directory does not exist. Resources will not be embedded. Did you forget to build the frontend?");
}
}
@@ -0,0 +1,10 @@
//! This crate provides `EMBEDDED_RESOURCES` that can be included in the desktop application binary.
//! It is intended to be used by the `embedded_resources` feature of the `graphite-desktop` crate.
//! The build script checks if the specified resources directory exists and sets the `embedded_resources` cfg flag accordingly.
//! If the resources directory does not exist, resources will not be embedded and a warning will be reported during compilation.
#[cfg(embedded_resources)]
pub static EMBEDDED_RESOURCES: Option<include_dir::Dir> = Some(include_dir::include_dir!("$EMBEDDED_RESOURCES"));
#[cfg(not(embedded_resources))]
pub static EMBEDDED_RESOURCES: Option<include_dir::Dir> = None;
@@ -0,0 +1,16 @@
[package]
name = "graphite-desktop-platform-linux"
version = "0.0.0"
description = "Graphite Desktop Platform Linux"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[[bin]]
name = "graphite"
path = "src/main.rs"
[dependencies]
graphite-desktop = { path = "../.." }
@@ -0,0 +1,3 @@
fn main() -> std::process::ExitCode {
graphite_desktop::start()
}
@@ -0,0 +1,27 @@
[package]
name = "graphite-desktop-platform-mac"
version = "0.0.0"
description = "Graphite Desktop Platform Mac"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[features]
main = ["dep:graphite-desktop"]
helper = ["dep:graphite-desktop-ui"]
[[bin]]
name = "graphite"
path = "src/main.rs"
required-features = ["main"]
[[bin]]
name = "helper"
path = "src/helper.rs"
required-features = ["helper"]
[dependencies]
graphite-desktop = { path = "../..", optional = true }
graphite-desktop-ui = { path = "../../ui", optional = true }
@@ -0,0 +1,3 @@
fn main() -> std::process::ExitCode {
graphite_desktop_ui::run_helper()
}
@@ -0,0 +1,3 @@
fn main() -> std::process::ExitCode {
graphite_desktop::start()
}
@@ -0,0 +1,19 @@
[package]
name = "graphite-desktop-platform-win"
version = "0.0.0"
description = "Graphite Desktop Platform Windows"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[[bin]]
name = "graphite"
path = "src/main.rs"
[dependencies]
graphite-desktop = { path = "../.." }
[target.'cfg(target_os = "windows")'.build-dependencies]
winres = "0.1"
@@ -0,0 +1,32 @@
fn main() {
#[cfg(target_os = "windows")]
{
let mut res = winres::WindowsResource::new();
res.set_icon("../../../branding/app-icons/graphite.ico");
res.set_language(0x0409); // English (US)
// TODO: Replace with actual version
res.set_version_info(winres::VersionInfo::FILEVERSION, {
const MAJOR: u64 = 0;
const MINOR: u64 = 0;
const PATCH: u64 = 0;
const RELEASE: u64 = 0;
(MAJOR << 48) | (MINOR << 32) | (PATCH << 16) | RELEASE
});
res.set("FileVersion", "0.0.0.0");
res.set("ProductVersion", "0.0.0.0");
res.set("OriginalFilename", "Graphite.exe");
res.set("FileDescription", "Graphite");
res.set("ProductName", "Graphite");
// TODO: Pull this year from the Git commit date
res.set("LegalCopyright", "Copyright © 2026 Graphite Labs, LLC");
res.set("CompanyName", "Graphite Labs, LLC");
res.compile().expect("Failed to compile Windows resources");
}
}
@@ -0,0 +1,4 @@
#![windows_subsystem = "windows"]
fn main() -> std::process::ExitCode {
graphite_desktop::start()
}
+696
View File
@@ -0,0 +1,696 @@
use rand::Rng;
use rfd::AsyncFileDialog;
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, SyncSender};
use std::thread;
use std::time::{Duration, Instant};
use winit::application::ApplicationHandler;
use winit::dpi::{PhysicalPosition, PhysicalSize};
use winit::event::{ButtonSource, ElementState, MouseButton, StartCause, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::window::WindowId;
use crate::dirs;
use crate::event::{AppEvent, AppEventScheduler};
use crate::persist;
use crate::preferences;
use crate::render::{RenderError, RenderState};
use crate::ui::{UiCommand, UiInstance};
use crate::window::Window;
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences};
use crate::wrapper::{DesktopWrapper, MmapResourceStorage, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
pub(crate) struct App {
render_state: Option<RenderState>,
wgpu_context: WgpuContext,
window: Option<Window>,
window_scale: f64,
window_size: PhysicalSize<u32>,
window_maximized: bool,
window_fullscreen: bool,
window_pending_drag: bool,
pointer_position: PhysicalPosition<f64>,
pointer_lock_position: Option<PhysicalPosition<f64>>,
ui_scale: f64,
app_event_receiver: Receiver<AppEvent>,
app_event_scheduler: AppEventScheduler,
desktop_wrapper: DesktopWrapper,
ui: UiInstance,
ui_frame_received: bool,
start_render_sender: SyncSender<()>,
web_communication_initialized: bool,
web_communication_startup_buffer: Vec<Vec<u8>>,
preferences: Preferences,
launch_documents: Option<Vec<PathBuf>>,
startup_time: Option<Instant>,
exiting: Arc<AtomicBool>,
exit_reason: ExitReason,
}
impl App {
pub(crate) fn init() {
Window::init();
}
pub(crate) fn new(
ui: UiInstance,
wgpu_context: WgpuContext,
app_event_receiver: Receiver<AppEvent>,
app_event_scheduler: AppEventScheduler,
preferences: Preferences,
launch_documents: Vec<PathBuf>,
) -> Self {
let ctrlc_app_event_scheduler = app_event_scheduler.clone();
ctrlc::set_handler(move || {
tracing::info!("Termination signal received, exiting...");
ctrlc_app_event_scheduler.schedule(AppEvent::Exit);
})
.expect("Error setting Ctrl-C handler");
let exiting = Arc::new(AtomicBool::new(false));
let rendering_app_event_scheduler = app_event_scheduler.clone();
let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1);
let exiting_clone = exiting.clone();
std::thread::spawn(move || {
let runtime = tokio::runtime::Runtime::new().unwrap();
loop {
let result = runtime.block_on(DesktopWrapper::execute_node_graph());
rendering_app_event_scheduler.schedule(AppEvent::NodeGraphExecutionResult(result));
let _ = start_render_receiver.recv_timeout(Duration::from_millis(10));
if exiting_clone.load(Ordering::Relaxed) {
break;
}
}
});
let resource_storage = MmapResourceStorage::new(dirs::app_resources_dir()).expect("Failed to initialize on-disk resource storage");
// Wake the winit event loop when an editor future completes.
let wake_scheduler = app_event_scheduler.clone();
let wake = Arc::new(move || {
wake_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::Wake));
});
let desktop_wrapper = DesktopWrapper::new(rand::rng().random(), Arc::new(resource_storage), dirs::app_autosave_documents_dir(), wgpu_context.clone(), wake);
let completion_render_sender = start_render_sender.clone();
DesktopWrapper::set_completion_notifier(move || {
let _ = completion_render_sender.try_send(());
});
Self {
render_state: None,
wgpu_context,
window: None,
window_scale: 1.,
window_size: PhysicalSize { width: 0, height: 0 },
window_maximized: false,
window_fullscreen: false,
window_pending_drag: false,
pointer_position: Default::default(),
pointer_lock_position: Default::default(),
ui_scale: 1.,
app_event_receiver,
app_event_scheduler,
desktop_wrapper,
ui,
ui_frame_received: false,
start_render_sender,
web_communication_initialized: false,
web_communication_startup_buffer: Vec::new(),
preferences,
launch_documents: Some(launch_documents),
startup_time: None,
exiting,
exit_reason: ExitReason::Shutdown,
}
}
pub(crate) fn run(mut self, event_loop: EventLoop) -> ExitReason {
event_loop.run_app(&mut self).unwrap();
self.exit_reason
}
fn exit(&mut self, reason: Option<ExitReason>) {
if self.exiting.swap(true, Ordering::Relaxed) {
return;
}
let _ = self.start_render_sender.send(());
if let Some(reason) = reason {
self.exit_reason = reason;
}
self.app_event_scheduler.schedule(AppEvent::Exit);
}
fn resize(&mut self) {
let Some(window) = &self.window else {
tracing::error!("Resize failed due to missing window");
return;
};
let maximized = window.is_maximized();
if maximized != self.window_maximized {
self.window_maximized = maximized;
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::UpdateMaximized { maximized }));
}
let fullscreen = window.is_fullscreen();
if fullscreen != self.window_fullscreen {
self.window_fullscreen = fullscreen;
self.app_event_scheduler
.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::UpdateFullscreen { fullscreen }));
}
let size = window.surface_size();
let scale = window.scale_factor() * self.ui_scale;
let is_new_size = size != self.window_size;
let is_new_scale = scale != self.window_scale;
if !is_new_size && !is_new_scale {
return;
}
if is_new_size {
self.ui.send(UiCommand::Resized {
width: size.width,
height: size.height,
});
}
if is_new_scale {
self.ui.send(UiCommand::ScaleChanged(scale));
}
self.ui.send(UiCommand::Refresh);
if let Some(render_state) = &mut self.render_state {
render_state.resize(size.width, size.height);
}
window.request_redraw();
self.window_size = size;
self.window_scale = scale;
}
fn handle_desktop_frontend_message(&mut self, message: DesktopFrontendMessage, responses: &mut Vec<DesktopWrapperMessage>) {
match message {
DesktopFrontendMessage::ToWeb(messages) => {
let Some(bytes) = serialize_frontend_messages(messages) else {
tracing::error!("Failed to serialize frontend messages");
return;
};
self.send_or_queue_web_message(bytes);
}
DesktopFrontendMessage::OpenFileDialog { title, filters, multiple, context } => {
let app_event_scheduler = self.app_event_scheduler.clone();
let _ = thread::spawn(move || {
let mut dialog = AsyncFileDialog::new().set_title(title);
for filter in filters {
dialog = dialog.add_filter(filter.name, &filter.extensions);
}
let handles = if multiple {
futures::executor::block_on(dialog.pick_files()).unwrap_or_default()
} else {
futures::executor::block_on(dialog.pick_file()).into_iter().collect()
};
for handle in handles {
let path = handle.path().to_path_buf();
match fs::read(&path) {
Ok(content) => {
let message = DesktopWrapperMessage::FileDialogResult { path, content, context };
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
}
Err(e) => tracing::error!("Failed to read file {}: {}", path.display(), e),
}
}
});
}
DesktopFrontendMessage::SaveFileDialog {
title,
default_filename,
default_folder,
filters,
context,
} => {
let app_event_scheduler = self.app_event_scheduler.clone();
let _ = thread::spawn(move || {
let mut dialog = AsyncFileDialog::new().set_title(title).set_file_name(default_filename);
if let Some(folder) = default_folder {
dialog = dialog.set_directory(folder);
}
for filter in filters {
dialog = dialog.add_filter(filter.name, &filter.extensions);
}
let show_dialog = async move { dialog.save_file().await.map(|f| f.path().to_path_buf()) };
if let Some(path) = futures::executor::block_on(show_dialog) {
let message = DesktopWrapperMessage::SaveFileDialogResult { path, context };
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
}
});
}
DesktopFrontendMessage::WriteFile { path, content } => {
if let Err(e) = fs::write(&path, content) {
tracing::error!("Failed to write file {}: {}", path.display(), e);
}
}
DesktopFrontendMessage::OpenUrl(url) => {
let _ = thread::spawn(move || {
if let Err(e) = open::that(&url) {
tracing::error!("Failed to open URL: {}: {}", url, e);
}
});
}
DesktopFrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height } => {
if let Some(render_state) = &mut self.render_state
&& let Some(window) = &self.window
{
let window_size = window.surface_size();
let viewport_offset_x = x / window_size.width as f64;
let viewport_offset_y = y / window_size.height as f64;
render_state.set_viewport_offset([viewport_offset_x as f32, viewport_offset_y as f32]);
let viewport_scale_x = if width != 0. { window_size.width as f64 / width } else { 1. };
let viewport_scale_y = if height != 0. { window_size.height as f64 / height } else { 1. };
render_state.set_viewport_scale([viewport_scale_x as f32, viewport_scale_y as f32]);
}
}
DesktopFrontendMessage::UpdateUIScale { scale } => {
self.ui_scale = scale;
self.resize();
}
DesktopFrontendMessage::UpdateOverlays(scene) => {
if let Some(render_state) = &mut self.render_state {
render_state.set_overlays_scene(scene);
}
if let Some(window) = &self.window {
window.request_redraw();
}
}
DesktopFrontendMessage::PersistenceWriteState { state } => {
persist::write_state(state);
}
DesktopFrontendMessage::PersistenceReadState => {
responses.push(DesktopWrapperMessage::LoadPersistedState { state: persist::read_state() });
}
DesktopFrontendMessage::PersistenceReadDocument { id } => {
if let Some(document) = persist::read_document_content(&id) {
responses.push(DesktopWrapperMessage::LoadDocumentContent { id, document });
} else {
tracing::error!("Failed to read document content for {id:?}");
}
}
DesktopFrontendMessage::PersistenceWriteDocument { id, document_serialized_content } => {
persist::write_document_content(id, document_serialized_content);
}
DesktopFrontendMessage::PersistenceDeleteDocument { id } => {
persist::delete_document(&id);
}
DesktopFrontendMessage::PersistenceWritePreferences { preferences } => {
preferences::write(preferences);
}
DesktopFrontendMessage::PersistenceLoadPreferences => {
let preferences = preferences::read();
let message = DesktopWrapperMessage::LoadPreferences { preferences };
responses.push(message);
}
DesktopFrontendMessage::OpenLaunchDocuments => {
let Some(launch_documents) = std::mem::take(&mut self.launch_documents) else {
tracing::error!("OpenLaunchDocuments should only be sent once");
return;
};
self.app_event_scheduler.schedule(AppEvent::OpenFiles(launch_documents));
}
DesktopFrontendMessage::UpdateMenu { entries } => {
if let Some(window) = &self.window {
window.update_menu(entries);
}
}
DesktopFrontendMessage::ClipboardRead => {
if let Some(window) = &self.window {
let content = window.clipboard_read();
let message = DesktopWrapperMessage::ClipboardReadResult { content };
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
}
}
DesktopFrontendMessage::ClipboardWrite { content } => {
if let Some(window) = &mut self.window {
window.clipboard_write(content);
}
}
DesktopFrontendMessage::PointerLock => {
self.pointer_lock_position = Some(self.pointer_position);
if let Some(window) = &self.window {
window.start_pointer_lock();
}
}
DesktopFrontendMessage::WindowClose => {
self.app_event_scheduler.schedule(AppEvent::Exit);
}
DesktopFrontendMessage::WindowMinimize => {
if let Some(window) = &self.window {
window.minimize();
}
}
DesktopFrontendMessage::WindowMaximize => {
if let Some(window) = &self.window {
window.toggle_maximize();
}
}
DesktopFrontendMessage::WindowFullscreen => {
if let Some(window) = &mut self.window {
window.toggle_fullscreen();
}
}
DesktopFrontendMessage::WindowDrag => {
self.window_pending_drag = true;
}
DesktopFrontendMessage::WindowFocus => {
if let Some(window) = &self.window {
window.focus();
}
}
DesktopFrontendMessage::WindowHide => {
if let Some(window) = &self.window {
window.hide();
}
}
DesktopFrontendMessage::WindowHideOthers => {
if let Some(window) = &self.window {
window.hide_others();
}
}
DesktopFrontendMessage::WindowShowAll => {
if let Some(window) = &self.window {
window.show_all();
}
}
DesktopFrontendMessage::Restart => {
self.exit(Some(ExitReason::Restart));
}
DesktopFrontendMessage::LoadThirdPartyLicenses => {
let compressed = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/third-party-licenses.txt.xz"));
let mut reader = lzma_rust2::XzReader::new(compressed.as_slice(), false);
let mut text = String::new();
if let Err(e) = reader.read_to_string(&mut text) {
tracing::error!("Failed to decompress third-party licenses: {e}");
return;
}
let message = DesktopWrapperMessage::LoadThirdPartyLicenses { text };
responses.push(message);
}
}
}
fn handle_desktop_frontend_messages(&mut self, messages: Vec<DesktopFrontendMessage>) {
let mut responses = Vec::new();
for message in messages {
self.handle_desktop_frontend_message(message, &mut responses);
}
for message in responses {
self.dispatch_desktop_wrapper_message(message);
}
}
fn dispatch_desktop_wrapper_message(&mut self, message: DesktopWrapperMessage) {
let responses = self.desktop_wrapper.dispatch(message);
self.handle_desktop_frontend_messages(responses);
}
fn send_or_queue_web_message(&mut self, message: Vec<u8>) {
if self.web_communication_initialized {
self.ui.send(UiCommand::Message(message));
} else {
self.web_communication_startup_buffer.push(message);
}
}
fn user_event(&mut self, event_loop: &dyn ActiveEventLoop, event: AppEvent) {
match event {
AppEvent::WebCommunicationInitialized => {
self.web_communication_initialized = true;
for message in self.web_communication_startup_buffer.drain(..) {
self.ui.send(UiCommand::Message(message));
}
}
AppEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message),
AppEvent::NodeGraphExecutionResult(result) => match result {
NodeGraphExecutionResult::HasRun(texture) => {
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::PollNodeGraphEvaluation);
if let Some(texture) = texture
&& let Some(render_state) = self.render_state.as_mut()
&& let Some(window) = self.window.as_ref()
{
render_state.bind_viewport_texture(texture);
window.request_redraw();
}
}
NodeGraphExecutionResult::NotRun => {}
},
AppEvent::UiUpdate(texture) => {
if let Some(render_state) = self.render_state.as_mut() {
render_state.bind_ui_texture(texture);
}
if let Some(window) = &self.window {
window.request_redraw();
}
if !self.ui_frame_received {
self.ui_frame_received = true;
}
}
AppEvent::CursorChange(cursor) => {
if let Some(window) = &mut self.window {
window.set_cursor(event_loop, cursor);
}
}
AppEvent::Exit => {
tracing::info!("Exiting main event loop");
event_loop.exit();
}
AppEvent::UiCrashed => {
tracing::error!("UI process crashed, exiting.");
self.exit(Some(ExitReason::Shutdown));
}
AppEvent::OpenFiles(paths) => {
// Accumulate launch documents until OpenLaunchDocuments message is received
if let Some(launch_documents) = &mut self.launch_documents {
launch_documents.extend(paths);
return;
}
if paths.is_empty() {
return;
}
let app_event_scheduler = self.app_event_scheduler.clone();
let _ = thread::spawn(move || {
for path in paths {
tracing::info!("Opening file: {}", path.display());
if let Ok(content) = fs::read(&path) {
let message = DesktopWrapperMessage::OpenFile { path, content };
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
} else {
tracing::error!("Failed to read file: {}", path.display());
}
}
});
}
#[cfg(target_os = "macos")]
AppEvent::MenuEvent { id } => {
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id });
}
}
}
}
impl ApplicationHandler for App {
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
let window = Window::new(event_loop, self.app_event_scheduler.clone());
self.window = Some(window);
#[cfg(not(target_os = "macos"))]
let present_mode = None;
#[cfg(target_os = "macos")]
let present_mode = if !self.preferences.vsync { Some(wgpu::PresentMode::Immediate) } else { None };
let render_state = RenderState::new(self.window.as_ref().unwrap(), self.wgpu_context.clone(), present_mode);
self.render_state = Some(render_state);
if let Some(window) = &self.window.as_ref() {
window.show();
}
self.resize();
self.startup_time = Some(Instant::now());
}
fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) {
while let Ok(event) = self.app_event_receiver.try_recv() {
self.user_event(event_loop, event);
}
}
fn window_event(&mut self, _event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
// Handle pointer lock release
if let Some(pointer_lock_position) = self.pointer_lock_position
&& let WindowEvent::PointerButton {
state: ElementState::Released,
button: ButtonSource::Mouse(MouseButton::Left),
..
} = event
{
self.pointer_lock_position = None;
if let Some(window) = &self.window {
window.end_pointer_lock();
}
self.ui.send(UiCommand::Input(WindowEvent::PointerMoved {
device_id: None,
position: pointer_lock_position,
primary: true,
source: winit::event::PointerSource::Mouse,
}));
}
self.ui.send(UiCommand::Input(event.clone()));
match event {
WindowEvent::CloseRequested => {
self.app_event_scheduler.schedule(AppEvent::Exit);
}
WindowEvent::SurfaceResized(_) | WindowEvent::ScaleFactorChanged { .. } => {
self.resize();
}
WindowEvent::RedrawRequested => {
#[cfg(target_os = "macos")]
self.resize();
let Some(render_state) = &mut self.render_state else { return };
if let Some(window) = &self.window {
if !window.can_render() {
return;
}
match render_state.render(window) {
Ok(_) => {}
Err(RenderError::OutdatedUITextureError) => {
self.ui.send(UiCommand::Refresh);
}
Err(RenderError::SurfaceLost) => {
tracing::warn!("lost surface");
}
Err(other) => tracing::error!("Render error: {:?}", other),
}
let _ = self.start_render_sender.try_send(());
}
if !self.ui_frame_received
&& !self.preferences.disable_ui_acceleration
&& self.web_communication_initialized
&& let Some(startup_time) = self.startup_time
&& startup_time.elapsed() > Duration::from_secs(3)
{
tracing::error!("UI acceleration not working, exiting.");
self.exit(Some(ExitReason::UiAccelerationFailure));
}
}
WindowEvent::DragDropped { paths, .. } => {
for path in paths {
match fs::read(&path) {
Ok(content) => {
let message = DesktopWrapperMessage::ImportFile { path, content };
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
}
Err(e) => {
tracing::error!("Failed to read dropped file {}: {}", path.display(), e);
return;
}
};
}
}
// Forward and Back buttons are not supported by CEF and thus need to be directly forwarded the editor
WindowEvent::PointerButton {
button: ButtonSource::Mouse(button),
state: ElementState::Pressed,
..
} => {
let mouse_keys = match button {
MouseButton::Back => Some(MouseKeys::BACK),
MouseButton::Forward => Some(MouseKeys::FORWARD),
_ => None,
};
if let Some(mouse_keys) = mouse_keys {
let message = DesktopWrapperMessage::Input(InputMessage::PointerDown {
editor_mouse_state: MouseState { mouse_keys, ..Default::default() },
modifier_keys: Default::default(),
});
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
let message = DesktopWrapperMessage::Input(InputMessage::PointerUp {
editor_mouse_state: Default::default(),
modifier_keys: Default::default(),
});
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
}
}
WindowEvent::PointerMoved { position, .. } | WindowEvent::PointerLeft { position: Some(position), .. } | WindowEvent::PointerEntered { position, .. }
if self.pointer_lock_position.is_none() =>
{
self.pointer_position = position;
if self.window_pending_drag {
self.window_pending_drag = false;
if let Some(window) = &self.window {
window.start_drag();
}
}
}
WindowEvent::PointerButton {
button: ButtonSource::Mouse(MouseButton::Left),
state: ElementState::Released,
..
} => {
self.window_pending_drag = false;
}
_ => {}
}
}
fn device_event(&mut self, _event_loop: &dyn ActiveEventLoop, _device_id: Option<winit::event::DeviceId>, event: winit::event::DeviceEvent) {
if self.pointer_lock_position.is_some()
&& let winit::event::DeviceEvent::PointerMotion { delta: (x, y) } = event
{
let message = DesktopWrapperMessage::PointerLockMove { x, y };
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
}
}
fn new_events(&mut self, _event_loop: &dyn ActiveEventLoop, cause: winit::event::StartCause) {
if let StartCause::ResumeTimeReached { .. } = cause
&& let Some(window) = &self.window
{
window.request_redraw();
}
}
fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) {
event_loop.set_control_flow(ControlFlow::WaitUntil(Instant::now() + Duration::from_millis(10)));
}
}
pub(crate) enum ExitReason {
Shutdown,
Restart,
UiAccelerationFailure,
}
+9
View File
@@ -0,0 +1,9 @@
#[derive(clap::Parser)]
#[clap(name = "graphite", version)]
pub struct Cli {
#[arg(help = "Files to open on startup")]
pub files: Vec<std::path::PathBuf>,
#[arg(long, action = clap::ArgAction::SetTrue, help = "Disable hardware accelerated UI rendering")]
pub disable_ui_acceleration: bool,
}
+14
View File
@@ -0,0 +1,14 @@
pub(crate) const APP_NAME: &str = "Graphite";
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub(crate) const APP_ID: &str = "art.graphite.Graphite";
#[cfg(target_os = "linux")]
pub(crate) const APP_DIRECTORY_NAME: &str = "graphite";
#[cfg(not(target_os = "linux"))]
pub(crate) const APP_DIRECTORY_NAME: &str = "Graphite";
pub(crate) const APP_LOCK_FILE_NAME: &str = "instance.lock";
pub(crate) const APP_SOCKET_FILE_NAME: &str = "instance.sock";
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron";
pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents";
pub(crate) const APP_RESOURCES_DIRECTORY_NAME: &str = "resources";
+55
View File
@@ -0,0 +1,55 @@
use std::fs;
use std::path::PathBuf;
use crate::consts::{APP_DIRECTORY_NAME, APP_DOCUMENTS_DIRECTORY_NAME, APP_RESOURCES_DIRECTORY_NAME};
pub(crate) fn ensure_dir_exists(path: &PathBuf) {
if !path.exists() {
fs::create_dir_all(path).unwrap_or_else(|_| panic!("Failed to create directory at {path:?}"));
}
}
pub(crate) fn clear_dir(path: &PathBuf) {
let Ok(entries) = fs::read_dir(path) else {
tracing::error!("Failed to read directory at {path:?}");
return;
};
for entry in entries.flatten() {
let entry_path = entry.path();
if entry_path.is_dir() {
if let Err(e) = fs::remove_dir_all(&entry_path) {
tracing::error!("Failed to remove directory at {:?}: {}", entry_path, e);
}
} else if entry_path.is_file() {
if let Err(e) = fs::remove_file(&entry_path) {
tracing::error!("Failed to remove file at {:?}: {}", entry_path, e);
}
}
}
}
pub(crate) fn app_data_dir() -> PathBuf {
let path = dirs::data_dir().expect("Failed to get data directory").join(APP_DIRECTORY_NAME);
ensure_dir_exists(&path);
path
}
pub(crate) fn app_autosave_documents_dir() -> PathBuf {
let path = app_data_dir().join(APP_DOCUMENTS_DIRECTORY_NAME);
ensure_dir_exists(&path);
path
}
pub(crate) fn app_resources_dir() -> PathBuf {
let path = app_data_dir().join(APP_RESOURCES_DIRECTORY_NAME);
ensure_dir_exists(&path);
path
}
// TODO: Eventually remove this cleanup code for the old "browser" CEF directory
pub(crate) fn delete_old_cef_browser_directory() {
let old_browser_dir = crate::dirs::app_data_dir().join("browser");
if old_browser_dir.is_dir() {
let _ = std::fs::remove_dir_all(&old_browser_dir);
}
}
+41
View File
@@ -0,0 +1,41 @@
use crate::ui::Cursor;
use crate::wrapper::NodeGraphExecutionResult;
use crate::wrapper::messages::DesktopWrapperMessage;
pub(crate) enum AppEvent {
UiUpdate(wgpu::Texture),
CursorChange(Cursor),
WebCommunicationInitialized,
DesktopWrapperMessage(DesktopWrapperMessage),
NodeGraphExecutionResult(NodeGraphExecutionResult),
Exit,
UiCrashed,
OpenFiles(Vec<std::path::PathBuf>),
#[cfg(target_os = "macos")]
MenuEvent {
id: String,
},
}
#[derive(Clone)]
pub(crate) struct AppEventScheduler {
pub(crate) proxy: winit::event_loop::EventLoopProxy,
pub(crate) sender: std::sync::mpsc::Sender<AppEvent>,
}
impl AppEventScheduler {
pub(crate) fn schedule(&self, event: AppEvent) {
let _ = self.sender.send(event);
self.proxy.wake_up();
}
}
pub(crate) trait CreateAppEventSchedulerEventLoopExt {
fn create_app_event_scheduler(&self, sender: std::sync::mpsc::Sender<AppEvent>) -> AppEventScheduler;
}
impl CreateAppEventSchedulerEventLoopExt for winit::event_loop::EventLoop {
fn create_app_event_scheduler(&self, sender: std::sync::mpsc::Sender<AppEvent>) -> AppEventScheduler {
AppEventScheduler { proxy: self.create_proxy(), sender }
}
}
@@ -0,0 +1,21 @@
use crate::wrapper::{WgpuContext, WgpuContextBuilder, WgpuFeatures};
pub(super) async fn create_wgpu_context() -> WgpuContext {
let mut wgpu_context_builder = WgpuContextBuilder::new().with_features(WgpuFeatures::IMMEDIATES);
// TODO: make this configurable via cli flags instead
if let Some(index) = std::env::var("GRAPHITE_WGPU_ADAPTER").ok().and_then(|s| s.parse().ok()) {
tracing::info!("Overriding WGPU adapter selection with adapter index {index}");
wgpu_context_builder = wgpu_context_builder.with_selection(index);
}
// TODO: add a cli flag to list adapters and exit instead of always printing
println!("\nAvailable WGPU adapters:\n{}", wgpu_context_builder.available_adapters_fmt().await);
let wgpu_context = wgpu_context_builder.build().await.expect("Failed to create WGPU context");
// TODO: add a cli flag to list adapters and exit instead of always printing
println!("Using WGPU adapter: {:?}", wgpu_context.adapter.get_info());
wgpu_context
}
+175
View File
@@ -0,0 +1,175 @@
use crate::app::App;
use crate::cli::Cli;
use crate::consts::APP_LOCK_FILE_NAME;
use crate::event::{AppEvent, CreateAppEventSchedulerEventLoopExt};
use clap::Parser;
use std::io::Write;
use std::process::ExitCode;
use tracing_subscriber::EnvFilter;
use ui::{Acceleration, UiConfig, UiContext, UiEvent, UiSetupResult};
use winit::event_loop::EventLoop;
pub(crate) use graphite_desktop_ui as ui;
pub(crate) use graphite_desktop_wrapper as wrapper;
mod app;
mod cli;
mod dirs;
mod event;
mod gpu_context;
mod persist;
mod preferences;
mod render;
mod socket;
mod window;
pub(crate) mod consts;
pub fn start() -> ExitCode {
tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init();
let ui_context = match UiContext::setup() {
UiSetupResult::Ready(context) => context,
UiSetupResult::Helper(code) => return code,
UiSetupResult::Failed => {
tracing::error!("Failed to set up the UI runtime");
return ExitCode::FAILURE;
}
};
let cli = Cli::parse();
let Ok(lock_file) = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(dirs::app_data_dir().join(APP_LOCK_FILE_NAME))
else {
tracing::error!("Failed to open lock file.");
return ExitCode::FAILURE;
};
let mut lock = fd_lock::RwLock::new(lock_file);
let lock = match lock.try_write() {
Ok(mut guard) => {
tracing::info!("Acquired application lock");
let _ = guard.set_len(0);
let _ = write!(guard, "{}", std::process::id());
let _ = guard.sync_all();
guard
}
Err(_) => {
tracing::error!("Another instance is already running, Exiting.");
if !cli.files.is_empty()
&& let Err(error) = socket::send(socket::Message::OpenFiles(cli.files))
{
tracing::error!("Failed to send socket message to running instance: {}", error);
return ExitCode::FAILURE;
}
return ExitCode::SUCCESS;
}
};
dirs::clear_dir(&ui::temp_dir_root());
// TODO: Eventually remove this cleanup code for the old "browser" CEF directory
dirs::delete_old_cef_browser_directory();
let mut prefs = preferences::read();
// Must be called before event loop initialization or native window integrations will break
App::init();
let wgpu_context = futures::executor::block_on(gpu_context::create_wgpu_context());
let event_loop = EventLoop::new().unwrap();
let (app_event_sender, app_event_receiver) = std::sync::mpsc::channel();
let app_event_scheduler = event_loop.create_app_event_scheduler(app_event_sender);
let _socket_handle = socket::start(app_event_scheduler.clone());
if cli.disable_ui_acceleration {
prefs.disable_ui_acceleration = true;
}
if prefs.disable_ui_acceleration {
println!("UI acceleration is disabled");
}
let acceleration = if prefs.disable_ui_acceleration { Acceleration::Disabled } else { Acceleration::Auto };
let ui_context = match ui_context.start(UiConfig { acceleration }) {
Ok(context) => context,
Err(error) => {
tracing::error!("Failed to start the UI runtime: {error}");
return ExitCode::FAILURE;
}
};
let ui = match ui_context.instance(&wgpu_context.device, &wgpu_context.queue) {
Ok(ui) => ui,
Err(error) => {
tracing::error!("Failed to start the UI: {error}");
return ExitCode::FAILURE;
}
};
tracing::info!("UI runtime started successfully");
{
let ui = ui.clone();
let scheduler = app_event_scheduler.clone();
let spawned = std::thread::Builder::new().name("ui-events".to_string()).spawn(move || {
while let Some(event) = ui.recv() {
match event {
UiEvent::Ready => scheduler.schedule(AppEvent::WebCommunicationInitialized),
UiEvent::Frame(texture) => scheduler.schedule(AppEvent::UiUpdate(texture)),
UiEvent::Cursor(cursor) => scheduler.schedule(AppEvent::CursorChange(cursor)),
UiEvent::Message(message) => match wrapper::deserialize_editor_message(&message) {
Some(message) => scheduler.schedule(AppEvent::DesktopWrapperMessage(message)),
None => tracing::error!("Failed to deserialize web message"),
},
UiEvent::Failure(error) => {
tracing::error!("UI failure: {error}");
scheduler.schedule(AppEvent::UiCrashed);
}
UiEvent::Crashed => scheduler.schedule(AppEvent::UiCrashed),
}
}
});
if let Err(error) = spawned {
tracing::error!("Failed to spawn the UI event bridge thread: {error}");
return ExitCode::FAILURE;
}
}
let app = App::new(ui.clone(), wgpu_context, app_event_receiver, app_event_scheduler, prefs, cli.files);
let exit_reason = app.run(event_loop);
// ui needs to be shutdown before restarting
ui.shutdown();
// If exiting due to a UI acceleration failure, update preferences to disable it for next launch
if matches!(exit_reason, app::ExitReason::UiAccelerationFailure) {
tracing::error!("Disabling UI acceleration");
preferences::modify(|prefs| {
prefs.disable_ui_acceleration = true;
});
}
// Explicitly drop the instance lock
drop(lock);
match exit_reason {
app::ExitReason::Restart | app::ExitReason::UiAccelerationFailure => {
tracing::info!("Restarting application");
let mut command = std::process::Command::new(std::env::current_exe().unwrap());
#[cfg(target_family = "unix")]
let _ = std::os::unix::process::CommandExt::exec(&mut command);
#[cfg(target_family = "unix")]
tracing::error!("Failed to restart application");
#[cfg(not(target_family = "unix"))]
let _ = command.spawn();
}
_ => {}
}
ExitCode::SUCCESS
}
+3
View File
@@ -0,0 +1,3 @@
fn main() -> std::process::ExitCode {
graphite_desktop::start()
}
+92
View File
@@ -0,0 +1,92 @@
use crate::wrapper::messages::{DocumentId, PersistedState};
pub(crate) fn read_state() -> PersistedState {
let path = state_file_path();
let data = match std::fs::read_to_string(&path) {
Ok(d) => d,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
tracing::info!("No persistent data file found at {path:?}, starting fresh");
return PersistedState::default();
}
Err(e) => {
tracing::error!("Failed to read persistent data from disk: {e}");
return PersistedState::default();
}
};
let loaded = match ron::from_str(&data) {
Ok(d) => d,
Err(e) => {
tracing::error!("Failed to deserialize persistent data: {e}");
return PersistedState::default();
}
};
garbage_collect_document_files(&loaded);
loaded
}
pub(crate) fn write_state(state: PersistedState) {
let state: &PersistedState = &state;
let data = match ron::ser::to_string_pretty(state, Default::default()) {
Ok(d) => d,
Err(e) => {
tracing::error!("Failed to serialize persistent data: {e}");
return;
}
};
if let Err(e) = std::fs::write(state_file_path(), data) {
tracing::error!("Failed to write persistent data to disk: {e}");
}
garbage_collect_document_files(state);
}
pub(crate) fn write_document_content(id: DocumentId, document_content: String) {
if let Err(e) = std::fs::write(document_content_path(&id), document_content) {
tracing::error!("Failed to write document {id:?} to disk: {e}");
}
}
pub(crate) fn read_document_content(id: &DocumentId) -> Option<String> {
std::fs::read_to_string(document_content_path(id)).ok()
}
pub(crate) fn delete_document(id: &DocumentId) {
if let Err(e) = std::fs::remove_file(document_content_path(id)) {
tracing::error!("Failed to delete document {id:?} from disk: {e}");
}
}
fn garbage_collect_document_files(state: &PersistedState) {
let valid_paths: std::collections::HashSet<_> = state.documents.iter().map(|doc| document_content_path(&doc.id)).collect();
let directory = crate::dirs::app_autosave_documents_dir();
let entries = match std::fs::read_dir(&directory) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
Err(e) => {
tracing::error!("Failed to read autosave documents directory: {e}");
return;
}
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && !valid_paths.contains(&path) {
if let Err(e) = std::fs::remove_file(&path) {
tracing::error!("Failed to remove orphaned document file {path:?}: {e}");
}
}
}
}
fn state_file_path() -> std::path::PathBuf {
let mut path = crate::dirs::app_data_dir();
path.push(crate::consts::APP_STATE_FILE_NAME);
path
}
fn document_content_path(id: &DocumentId) -> std::path::PathBuf {
let mut path = crate::dirs::app_autosave_documents_dir();
path.push(format!("{:x}.{}", id.0, graphite_desktop_wrapper::FILE_EXTENSION));
path
}
@@ -0,0 +1,33 @@
use graphite_desktop_wrapper::messages::Preferences;
pub(crate) fn write(preferences: Preferences) {
let Ok(preferences) = ron::ser::to_string_pretty(&preferences, Default::default()) else {
tracing::error!("Failed to serialize preferences");
return;
};
std::fs::write(file_path(), &preferences).unwrap_or_else(|e| {
tracing::error!("Failed to write preferences to disk: {e}");
});
}
pub(crate) fn read() -> Preferences {
let Ok(data) = std::fs::read_to_string(file_path()) else {
return Preferences::default();
};
let Ok(preferences) = ron::from_str(&data) else {
return Preferences::default();
};
preferences
}
pub(crate) fn modify(f: impl FnOnce(&mut Preferences)) {
let mut preferences = read();
f(&mut preferences);
write(preferences);
}
fn file_path() -> std::path::PathBuf {
let mut path = crate::dirs::app_data_dir();
path.push(crate::consts::APP_PREFERENCES_FILE_NAME);
path
}
+2
View File
@@ -0,0 +1,2 @@
mod state;
pub(crate) use state::{RenderError, RenderState};
@@ -0,0 +1,121 @@
// =============
// 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 {
viewport_scale: vec2<f32>,
viewport_offset: vec2<f32>,
ui_scale: vec2<f32>,
background_color: vec4<f32>,
};
var<immediate> immediates: Immediates;
@group(0) @binding(0)
var t_viewport: texture_2d<f32>;
@group(0) @binding(1)
var t_overlays: texture_2d<f32>;
@group(0) @binding(2)
var t_ui: texture_2d<f32>;
@group(0) @binding(3)
var s_diffuse: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let ui_coordinate = in.tex_coords * immediates.ui_scale;
if (ui_coordinate.x < 0.0 || ui_coordinate.x > 1.0 ||
ui_coordinate.y < 0.0 || ui_coordinate.y > 1.0) {
return srgb_to_linear(immediates.background_color);
}
let ui_linear = srgb_to_linear(textureSample(t_ui, s_diffuse, ui_coordinate));
if (ui_linear.a >= 0.999) {
return ui_linear;
}
// UI texture is premultiplied, we need to unpremultiply before blending
let ui_srgb = linear_to_srgb(unpremultiply(ui_linear));
let viewport_coordinate = (in.tex_coords - immediates.viewport_offset) * immediates.viewport_scale;
if (viewport_coordinate.x < 0.0 || viewport_coordinate.x > 1.0 ||
viewport_coordinate.y < 0.0 || viewport_coordinate.y > 1.0) {
return srgb_to_linear(immediates.background_color);
}
let overlay_srgb = textureSample(t_overlays, s_diffuse, viewport_coordinate);
var viewport_srgb = textureSample(t_viewport, s_diffuse, viewport_coordinate);
if (viewport_srgb.a < 0.001) {
viewport_srgb = immediates.background_color;
} else if (viewport_srgb.a < 0.999) {
viewport_srgb = blend(viewport_srgb, immediates.background_color);
}
if (overlay_srgb.a < 0.001) {
if (ui_srgb.a < 0.001) {
return srgb_to_linear(viewport_srgb);
} else {
return srgb_to_linear(blend(ui_srgb, viewport_srgb));
}
}
let composite_linear = blend(srgb_to_linear(overlay_srgb), srgb_to_linear(viewport_srgb));
if (ui_srgb.a < 0.001) {
return composite_linear;
}
return srgb_to_linear(blend(ui_srgb, linear_to_srgb(composite_linear)));
}
fn blend(fg: vec4<f32>, bg: vec4<f32>) -> vec4<f32> {
let a = fg.a + bg.a * (1.0 - fg.a);
let rgb = fg.rgb * fg.a + bg.rgb * bg.a * (1.0 - fg.a);
return vec4<f32>(rgb, a);
}
fn linear_to_srgb(in: vec4<f32>) -> vec4<f32> {
let cutoff = vec3<f32>(0.0031308);
let lo = in.rgb * 12.92;
let hi = 1.055 * pow(max(in.rgb, vec3<f32>(0.0)), vec3<f32>(1.0/2.4)) - 0.055;
return vec4<f32>(select(lo, hi, in.rgb > cutoff), in.a);
}
fn srgb_to_linear(in: vec4<f32>) -> vec4<f32> {
let cutoff = vec3<f32>(0.04045);
let lo = in.rgb / 12.92;
let hi = pow((in.rgb + 0.055) / 1.055, vec3<f32>(2.4));
return vec4<f32>(select(lo, hi, in.rgb > cutoff), in.a);
}
fn unpremultiply(in: vec4<f32>) -> vec4<f32> {
if (in.a > 0.0) {
return vec4<f32>((in.rgb / in.a), in.a);
} else {
return vec4<f32>(0.0);
}
}
@@ -0,0 +1,385 @@
use wgpu::PresentMode;
use crate::window::Window;
use crate::wrapper::{WgpuContext, WgpuCurrentSurfaceTexture, WgpuExecutor, WgpuSurface};
#[derive(derivative::Derivative)]
#[derivative(Debug)]
pub(crate) struct RenderState {
surface: WgpuSurface,
executor: WgpuExecutor,
config: wgpu::SurfaceConfiguration,
render_pipeline: wgpu::RenderPipeline,
transparent_texture: std::sync::Arc<wgpu::Texture>,
sampler: wgpu::Sampler,
desired_width: u32,
desired_height: u32,
viewport_scale: [f32; 2],
viewport_offset: [f32; 2],
viewport_texture: Option<std::sync::Arc<wgpu::Texture>>,
overlays_texture: Option<std::sync::Arc<wgpu::Texture>>,
ui_texture: Option<wgpu::Texture>,
bind_group: Option<wgpu::BindGroup>,
#[derivative(Debug = "ignore")]
overlays_scene: Option<vello::Scene>,
surface_outdated: bool,
}
impl RenderState {
pub(crate) fn new(window: &Window, context: WgpuContext, present_mode: Option<PresentMode>) -> Self {
let size = window.surface_size();
let surface = window.create_surface(&context.instance);
let surface_caps = surface.get_capabilities(&context.adapter);
let surface_format = surface_caps.formats.iter().find(|f| f.is_srgb()).copied().unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: present_mode.unwrap_or(surface_caps.present_modes[0]),
alpha_mode: surface_caps.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 1,
};
surface.configure(&context.device, &config);
let transparent_texture = std::sync::Arc::new(context.device.create_texture(&wgpu::TextureDescriptor {
label: Some("Transparent Texture"),
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Bgra8UnormSrgb,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
}));
// Create shader module
let shader = context.device.create_shader_module(wgpu::include_wgsl!("composite_shader.wgsl"));
// Create sampler
let sampler = context.device.create_sampler(&wgpu::SamplerDescriptor {
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::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
let texture_bind_group_layout = context.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
label: Some("texture_bind_group_layout"),
});
let render_pipeline_layout = context.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"),
bind_group_layouts: &[Some(&texture_bind_group_layout)],
immediate_size: size_of::<Immediates>() as u32,
});
let render_pipeline = context.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Render Pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: config.format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
});
let executor = WgpuExecutor::with_context(context).expect("Failed to create WgpuExecutor");
Self {
surface,
executor,
config,
render_pipeline,
transparent_texture,
sampler,
desired_width: size.width,
desired_height: size.height,
viewport_scale: [1., 1.],
viewport_offset: [0., 0.],
viewport_texture: None,
overlays_texture: None,
ui_texture: None,
bind_group: None,
overlays_scene: None,
surface_outdated: true,
}
}
pub(crate) fn resize(&mut self, width: u32, height: u32) {
if width == self.desired_width && height == self.desired_height {
return;
}
self.desired_width = width;
self.desired_height = height;
self.surface_outdated = true;
}
pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: std::sync::Arc<wgpu::Texture>) {
self.viewport_texture = Some(viewport_texture);
self.update_bindgroup();
}
pub(crate) fn bind_ui_texture(&mut self, ui_texture: wgpu::Texture) {
if self.ui_texture.as_ref() == Some(&ui_texture) {
self.surface_outdated = true;
return;
}
self.ui_texture = Some(ui_texture);
self.update_bindgroup();
}
pub(crate) fn set_viewport_scale(&mut self, scale: [f32; 2]) {
self.surface_outdated = true;
self.viewport_scale = scale;
}
pub(crate) fn set_viewport_offset(&mut self, offset: [f32; 2]) {
self.surface_outdated = true;
self.viewport_offset = offset;
}
pub(crate) fn set_overlays_scene(&mut self, scene: vello::Scene) {
self.surface_outdated = true;
self.overlays_scene = Some(scene);
}
fn render_overlays(&mut self, scene: vello::Scene) {
let Some(viewport_texture) = self.viewport_texture.as_ref() else {
tracing::warn!("No viewport texture bound, cannot render overlays");
return;
};
let size = glam::UVec2::new(viewport_texture.width(), viewport_texture.height());
let result = self.executor.render_vello_scene(&scene, size, &Default::default(), None);
match result {
Ok(texture) => {
self.overlays_texture = Some(texture.into());
}
Err(e) => {
self.overlays_texture = None;
tracing::error!("Error rendering overlays: {:?}", e);
}
}
self.update_bindgroup();
}
pub(crate) fn render(&mut self, window: &Window) -> Result<(), RenderError> {
if !self.surface_outdated {
return Ok(());
}
// Apply resize once per presented frame.
if self.desired_width > 0 && self.desired_height > 0 && (self.config.width != self.desired_width || self.config.height != self.desired_height) {
self.config.width = self.desired_width;
self.config.height = self.desired_height;
self.surface.configure(&self.executor.context().device, &self.config);
}
let ui_scale = if let Some(ui_texture) = &self.ui_texture
&& (self.desired_width != ui_texture.width() || self.desired_height != ui_texture.height())
{
Some([self.desired_width as f32 / ui_texture.width() as f32, self.desired_height as f32 / ui_texture.height() as f32])
} else {
None
};
if let Some(scene) = self.overlays_scene.take() {
self.render_overlays(scene);
}
let (surface_texture, suboptimal) = match self.surface.get_current_texture(&self.executor.context().queue) {
WgpuCurrentSurfaceTexture::Success(t) => (t, false),
WgpuCurrentSurfaceTexture::Suboptimal(t) => (t, true),
WgpuCurrentSurfaceTexture::Occluded => return Ok(()),
WgpuCurrentSurfaceTexture::Lost => return Err(RenderError::SurfaceLost),
WgpuCurrentSurfaceTexture::Outdated => return Err(RenderError::SurfaceOutdated),
WgpuCurrentSurfaceTexture::Timeout => return Err(RenderError::SurfaceTimeout),
WgpuCurrentSurfaceTexture::Validation => return Err(RenderError::SurfaceValidation),
};
let view = surface_texture.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self.executor.context().device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder") });
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Graphite Composition Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.01, g: 0.01, b: 0.01, a: 1. }),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
multiview_mask: None,
});
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_immediates(
0,
bytemuck::bytes_of(&Immediates {
viewport_scale: self.viewport_scale,
viewport_offset: self.viewport_offset,
ui_scale: ui_scale.unwrap_or([1., 1.]),
_pad: [0., 0.],
background_color: [0x22 as f32 / 0xff as f32, 0x22 as f32 / 0xff as f32, 0x22 as f32 / 0xff as f32, 1.], // #222222
}),
);
if let Some(bind_group) = &self.bind_group {
render_pass.set_bind_group(0, bind_group, &[]);
render_pass.draw(0..3, 0..1); // Draw 3 vertices for fullscreen triangle
} else {
tracing::warn!("No bind group available - showing clear color only");
}
}
surface_texture.queue.submit(std::iter::once(encoder.finish()));
window.pre_present_notify();
surface_texture.present();
if suboptimal {
self.surface.configure(&self.executor.context().device, &self.config);
}
if ui_scale.is_some() {
return Err(RenderError::OutdatedUITextureError);
}
self.surface_outdated = false;
Ok(())
}
fn update_bindgroup(&mut self) {
self.surface_outdated = true;
let viewport_texture_view = self.viewport_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default());
let overlays_texture_view = self.overlays_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default());
let ui_texture_view = self.ui_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = self.executor.context().device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &self.render_pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&viewport_texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&overlays_texture_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(&ui_texture_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
label: Some("texture_bind_group"),
});
self.bind_group = Some(bind_group);
}
}
#[derive(Debug)]
pub(crate) enum RenderError {
OutdatedUITextureError,
SurfaceLost,
SurfaceOutdated,
SurfaceTimeout,
SurfaceValidation,
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Immediates {
viewport_scale: [f32; 2],
viewport_offset: [f32; 2],
ui_scale: [f32; 2],
_pad: [f32; 2],
background_color: [f32; 4],
}
+126
View File
@@ -0,0 +1,126 @@
use interprocess::local_socket::{GenericFilePath, GenericNamespaced, ListenerNonblockingMode, ListenerOptions, Name, prelude::*};
use std::io::{ErrorKind, Read, Write};
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use crate::consts::APP_SOCKET_FILE_NAME;
use crate::event::{AppEvent, AppEventScheduler};
// TODO: Needs to be integrated/replaced with the action system.
// TODO: At that point this should just wrap the action, meaning all actions bindable by the user can also be accessed via the socket.
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) enum Message {
OpenFiles(Vec<std::path::PathBuf>),
}
fn handle_message(message: Message, app_event_scheduler: &AppEventScheduler) {
match message {
Message::OpenFiles(paths) => {
app_event_scheduler.schedule(AppEvent::OpenFiles(paths));
}
}
}
pub(crate) fn send(message: Message) -> std::io::Result<()> {
let data = ron::ser::to_string(&message).map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
let mut connection = interprocess::local_socket::Stream::connect(socket_name())?;
connection.write_all(data.as_bytes())
}
pub(crate) struct SocketHandle {
thread: Option<thread::JoinHandle<()>>,
shutdown_sender: mpsc::Sender<()>,
}
impl Drop for SocketHandle {
fn drop(&mut self) {
let _ = self.shutdown_sender.send(());
let _ = self.thread.take().expect("SocketHandle can only be dropped once").join();
}
}
pub(crate) fn start(app_event_scheduler: AppEventScheduler) -> SocketHandle {
let (shutdown_sender, shutdown_receiver) = mpsc::channel();
let thread = thread::Builder::new()
.name("socket".to_string())
.spawn(move || run(app_event_scheduler, shutdown_receiver))
.expect("Failed to spawn socket thread");
SocketHandle {
shutdown_sender,
thread: Some(thread),
}
}
fn run(app_event_scheduler: AppEventScheduler, shutdown_receiver: mpsc::Receiver<()>) {
let listener = match ListenerOptions::new()
.name(socket_name())
.nonblocking(ListenerNonblockingMode::Accept)
.try_overwrite(true)
.max_spin_time(Duration::from_millis(100))
.create_sync()
{
Ok(listener) => listener,
Err(error) => {
tracing::error!("Failed to bind socket: {}", error);
return;
}
};
let max_backoff = Duration::from_millis(100);
let mut backoff = Duration::ZERO;
loop {
if backoff.is_zero() {
match shutdown_receiver.try_recv() {
Ok(()) | Err(mpsc::TryRecvError::Disconnected) => break,
Err(mpsc::TryRecvError::Empty) => {}
}
backoff = Duration::from_nanos(1);
} else {
match shutdown_receiver.recv_timeout(backoff) {
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => break,
Err(mpsc::RecvTimeoutError::Timeout) => {}
}
backoff = (backoff * 2).min(max_backoff);
}
match listener.accept() {
Ok(mut connection) => {
backoff = Duration::ZERO;
let app_event_scheduler = app_event_scheduler.clone();
let spawn_result = thread::Builder::new().name("socket-connection".to_string()).spawn(move || {
let mut data = String::new();
if let Err(error) = connection.read_to_string(&mut data) {
tracing::error!("Failed to read socket message: {}", error);
return;
}
match ron::de::from_str(&data) {
Ok(message) => handle_message(message, &app_event_scheduler),
Err(error) => tracing::error!("Failed to deserialize socket message: {}", error),
}
});
if let Err(error) = spawn_result {
tracing::error!("Failed to spawn socket connection thread: {}", error);
}
}
Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {}
Err(error) => {
tracing::error!("Failed to accept socket connection: {}", error);
}
}
}
}
fn socket_name() -> Name<'static> {
if cfg!(target_os = "windows") {
let user = std::env::var("USERNAME").unwrap_or_default();
let name = format!("{user}-{app}-{APP_SOCKET_FILE_NAME}", app = crate::consts::APP_NAME);
name.to_ns_name::<GenericNamespaced>().expect("valid named pipe name")
} else {
crate::dirs::app_data_dir().join(APP_SOCKET_FILE_NAME).to_fs_name::<GenericFilePath>().expect("valid socket path")
}
}
+235
View File
@@ -0,0 +1,235 @@
use crate::consts::APP_NAME;
use crate::event::AppEventScheduler;
use crate::ui::Cursor;
use crate::wrapper::messages::MenuItem;
use crate::wrapper::{WgpuInstance, WgpuSurface};
use std::collections::HashMap;
use std::sync::Arc;
use winit::cursor::{CustomCursor, CustomCursorSource};
use winit::event_loop::ActiveEventLoop;
use winit::monitor::Fullscreen;
use winit::window::{Window as WinitWindow, WindowAttributes};
pub(crate) trait NativeWindow {
fn init() {}
fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes;
fn new(window: &dyn WinitWindow, app_event_scheduler: AppEventScheduler) -> Self;
fn can_render(&self) -> bool {
true
}
fn update_menu(&self, _entries: Vec<MenuItem>) {}
fn hide(&self) {}
fn hide_others(&self) {}
fn show_all(&self) {}
}
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
use linux as native;
#[cfg(target_os = "macos")]
mod mac;
#[cfg(target_os = "macos")]
use mac as native;
#[cfg(target_os = "windows")]
mod win;
#[cfg(target_os = "windows")]
use win as native;
pub(crate) struct Window {
winit_window: Arc<dyn winit::window::Window>,
#[allow(dead_code)]
native_handle: native::NativeWindowImpl,
custom_cursors: HashMap<CustomCursorSource, CustomCursor>,
clipboard: Option<window_clipboard::Clipboard>,
}
impl Drop for Window {
fn drop(&mut self) {
// Clipboard must be dropped before `winit_window`
drop(self.clipboard.take());
}
}
impl Window {
pub(crate) fn init() {
native::NativeWindowImpl::init();
}
pub(crate) fn new(event_loop: &dyn ActiveEventLoop, app_event_scheduler: AppEventScheduler) -> Self {
let mut attributes = WindowAttributes::default()
.with_title(APP_NAME)
.with_min_surface_size(winit::dpi::LogicalSize::new(400, 300))
.with_surface_size(winit::dpi::LogicalSize::new(1200, 800))
.with_resizable(true)
.with_visible(false)
.with_theme(Some(winit::window::Theme::Dark));
attributes = native::NativeWindowImpl::configure(attributes, event_loop);
let winit_window = event_loop.create_window(attributes).unwrap();
let native_handle = native::NativeWindowImpl::new(winit_window.as_ref(), app_event_scheduler);
let clipboard = unsafe { window_clipboard::Clipboard::connect(&winit_window) }.ok();
Self {
winit_window: winit_window.into(),
native_handle,
custom_cursors: HashMap::new(),
clipboard,
}
}
pub(crate) fn show(&self) {
self.winit_window.set_visible(true);
self.winit_window.focus_window();
}
pub(crate) fn request_redraw(&self) {
self.winit_window.request_redraw();
}
pub(crate) fn create_surface(&self, instance: &WgpuInstance) -> WgpuSurface {
instance.create_surface(self.winit_window.clone()).expect("Failed to create surface")
}
pub(crate) fn pre_present_notify(&self) {
self.winit_window.pre_present_notify();
}
pub(crate) fn can_render(&self) -> bool {
self.native_handle.can_render()
}
pub(crate) fn surface_size(&self) -> winit::dpi::PhysicalSize<u32> {
self.winit_window.surface_size()
}
pub(crate) fn scale_factor(&self) -> f64 {
self.winit_window.scale_factor()
}
pub(crate) fn minimize(&self) {
self.winit_window.set_minimized(true);
}
pub(crate) fn toggle_maximize(&self) {
if self.is_fullscreen() {
return;
}
self.winit_window.set_maximized(!self.winit_window.is_maximized());
}
pub(crate) fn is_maximized(&self) -> bool {
self.winit_window.is_maximized()
}
pub(crate) fn toggle_fullscreen(&mut self) {
if self.is_fullscreen() {
self.winit_window.set_fullscreen(None);
} else {
self.winit_window.set_fullscreen(Some(Fullscreen::Borderless(None)));
}
}
pub(crate) fn is_fullscreen(&self) -> bool {
self.winit_window.fullscreen().is_some()
}
pub(crate) fn start_drag(&self) {
if self.is_fullscreen() {
return;
}
let _ = self.winit_window.drag_window();
}
pub(crate) fn focus(&self) {
self.winit_window.set_minimized(false);
self.winit_window.focus_window();
}
pub(crate) fn hide(&self) {
self.native_handle.hide();
}
pub(crate) fn hide_others(&self) {
self.native_handle.hide_others();
}
pub(crate) fn show_all(&self) {
self.native_handle.show_all();
}
pub(crate) fn set_cursor(&mut self, event_loop: &dyn ActiveEventLoop, cursor: Cursor) {
let cursor = match cursor {
Cursor::Icon(cursor_icon) => cursor_icon.into(),
Cursor::Custom {
rgba,
width,
height,
hotspot_x,
hotspot_y,
} => {
let Ok(custom_cursor_source) = CustomCursorSource::from_rgba(rgba, width, height, hotspot_x, hotspot_y) else {
tracing::error!("Invalid custom cursor image");
return;
};
let custom_cursor = match self.custom_cursors.get(&custom_cursor_source).cloned() {
Some(cursor) => cursor,
None => {
let Ok(custom_cursor) = event_loop.create_custom_cursor(custom_cursor_source.clone()) else {
tracing::error!("Failed to create custom cursor");
return;
};
self.custom_cursors.insert(custom_cursor_source, custom_cursor.clone());
custom_cursor
}
};
custom_cursor.into()
}
Cursor::None => {
self.winit_window.set_cursor_visible(false);
return;
}
};
self.winit_window.set_cursor_visible(true);
self.winit_window.set_cursor(cursor);
}
pub(crate) fn start_pointer_lock(&self) {
let _ = self.winit_window.set_cursor_grab(winit::window::CursorGrabMode::Locked);
self.winit_window.set_cursor_visible(false);
}
pub(crate) fn end_pointer_lock(&self) {
let _ = self.winit_window.set_cursor_grab(winit::window::CursorGrabMode::None);
self.winit_window.set_cursor_visible(true);
}
pub(crate) fn update_menu(&self, entries: Vec<MenuItem>) {
self.native_handle.update_menu(entries);
}
pub(crate) fn clipboard_read(&self) -> Option<String> {
let Some(clipboard) = &self.clipboard else {
tracing::error!("Clipboard not available");
return None;
};
match clipboard.read() {
Ok(data) => Some(data),
Err(e) => {
tracing::error!("Failed to read from clipboard: {e}");
None
}
}
}
pub(crate) fn clipboard_write(&mut self, data: String) {
let Some(clipboard) = &mut self.clipboard else {
tracing::error!("Clipboard not available");
return;
};
if let Err(e) = clipboard.write(data) {
tracing::error!("Failed to write to clipboard: {e}")
}
}
}
@@ -0,0 +1,26 @@
use winit::event_loop::ActiveEventLoop;
use winit::platform::wayland::ActiveEventLoopExtWayland;
use winit::platform::wayland::WindowAttributesWayland;
use winit::platform::x11::WindowAttributesX11;
use winit::window::{Window, WindowAttributes};
use crate::consts::{APP_ID, APP_NAME};
use crate::event::AppEventScheduler;
pub(super) struct NativeWindowImpl {}
impl super::NativeWindow for NativeWindowImpl {
fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes {
if event_loop.is_wayland() {
let wayland_attributes = WindowAttributesWayland::default().with_name(APP_ID, "").with_prefer_csd(true);
attributes.with_platform_attributes(Box::new(wayland_attributes))
} else {
let x11_attributes = WindowAttributesX11::default().with_name(APP_ID, APP_NAME);
attributes.with_platform_attributes(Box::new(x11_attributes))
}
}
fn new(_window: &dyn Window, _app_event_scheduler: AppEventScheduler) -> Self {
NativeWindowImpl {}
}
}
@@ -0,0 +1,49 @@
use winit::event_loop::ActiveEventLoop;
use winit::platform::macos::WindowAttributesMacOS;
use winit::window::{Window, WindowAttributes};
use crate::event::AppEventScheduler;
use crate::wrapper::messages::MenuItem;
mod app;
mod menu;
pub(super) struct NativeWindowImpl {
menu: menu::Menu,
}
impl super::NativeWindow for NativeWindowImpl {
fn init() {
app::init();
}
fn configure(attributes: WindowAttributes, _event_loop: &dyn ActiveEventLoop) -> WindowAttributes {
let mac_window = WindowAttributesMacOS::default()
.with_titlebar_transparent(true)
.with_fullsize_content_view(true)
.with_title_hidden(true);
attributes.with_platform_attributes(Box::new(mac_window))
}
fn new(_window: &dyn Window, app_event_scheduler: AppEventScheduler) -> Self {
app::setup(app_event_scheduler.clone());
let menu = menu::Menu::new(app_event_scheduler);
NativeWindowImpl { menu }
}
fn update_menu(&self, entries: Vec<MenuItem>) {
self.menu.update(entries);
}
fn hide(&self) {
app::hide();
}
fn hide_others(&self) {
app::hide_others();
}
fn show_all(&self) {
app::show_all();
}
}
@@ -0,0 +1,119 @@
use std::ffi::CStr;
use std::ffi::OsStr;
use std::ops::Deref;
use std::ops::DerefMut;
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
use std::sync::{Mutex, Once};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2::{ClassType, MainThreadMarker, MainThreadOnly, define_class, msg_send};
use objc2_app_kit::{NSApplication, NSApplicationDelegate, NSEvent, NSEventType, NSResponder};
use objc2_foundation::{NSArray, NSObject, NSObjectProtocol, NSURL};
use crate::event::{AppEvent, AppEventScheduler};
static APP_EVENT_SCHEDULER: Mutex<Option<AppEventScheduler>> = Mutex::new(None);
static PENDING_EVENTS: Mutex<Option<Vec<AppEvent>>> = Mutex::new(Some(Vec::new()));
fn dispatch_event(event: AppEvent) {
let app_event_scheduler_guard = APP_EVENT_SCHEDULER.lock().unwrap();
if let Some(app_event_scheduler) = app_event_scheduler_guard.deref() {
app_event_scheduler.schedule(event);
} else if let Some(pending_events) = PENDING_EVENTS.lock().unwrap().deref_mut() {
pending_events.push(event);
} else {
tracing::error!("Failed to dispatch event");
}
}
fn instance() -> objc2::rc::Retained<NSApplication> {
unsafe { msg_send![GraphiteApplication::class(), sharedApplication] }
}
static INSTALL_DELEGATE: Once = Once::new();
pub(super) fn init() {
let _ = instance();
INSTALL_DELEGATE.call_once(|| {
let mtm = MainThreadMarker::new().expect("should only ever be called from main thread");
let delegate: Retained<GraphiteApplicationDelegate> = unsafe { msg_send![super(GraphiteApplicationDelegate::alloc(mtm).set_ivars(())), init] };
instance().setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
std::mem::forget(delegate);
});
}
pub(super) fn setup(app_event_scheduler: AppEventScheduler) {
let mut app_event_scheduler_guard = APP_EVENT_SCHEDULER.lock().unwrap();
if let Some(mut pending_events) = PENDING_EVENTS.lock().unwrap().take() {
pending_events.drain(..).for_each(|event| {
app_event_scheduler.schedule(event);
});
} else {
tracing::error!("Failed to take PENDING_EVENTS and schedule them. This a bug.");
}
*app_event_scheduler_guard = Some(app_event_scheduler);
}
pub(super) fn hide() {
instance().hide(None);
}
pub(super) fn hide_others() {
instance().hideOtherApplications(None);
}
pub(super) fn show_all() {
instance().unhideAllApplications(None);
}
define_class!(
#[unsafe(super(NSApplication, NSResponder, NSObject))]
#[name = "GraphiteApplication"]
pub(super) struct GraphiteApplication;
impl GraphiteApplication {
#[unsafe(method(sendEvent:))]
fn send_event(&self, event: &NSEvent) {
// Route keyDown events straight to the key window to skip native menu shortcut handling.
if event.r#type() == NSEventType::KeyDown && let Some(key_window) = self.keyWindow() {
unsafe { msg_send![&key_window, sendEvent: event] }
} else {
unsafe { msg_send![super(self), sendEvent: event] }
}
}
}
);
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "GraphiteApplicationDelegate"]
struct GraphiteApplicationDelegate;
unsafe impl NSObjectProtocol for GraphiteApplicationDelegate {}
unsafe impl NSApplicationDelegate for GraphiteApplicationDelegate {
#[unsafe(method(application:openURLs:))]
fn application_open_urls(&self, _application: &NSApplication, urls: &NSArray<NSURL>) {
let paths = (0..urls.count())
.filter_map(|index| {
let url = urls.objectAtIndex(index);
if !url.isFileURL() {
tracing::error!("Ignoring open URL event for non-file URL: {:?}", url);
return None;
}
let cstr = unsafe { CStr::from_ptr(url.fileSystemRepresentation().as_ptr()) };
let path = PathBuf::from(OsStr::from_bytes(cstr.to_bytes()));
Some(path)
})
.collect::<Vec<_>>();
dispatch_event(AppEvent::OpenFiles(paths));
}
}
);
@@ -0,0 +1,146 @@
use muda::Menu as MudaMenu;
use muda::accelerator::Accelerator;
use muda::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, MenuItemKind, PredefinedMenuItem, Result, Submenu};
use crate::event::{AppEvent, AppEventScheduler};
use crate::wrapper::messages::MenuItem as WrapperMenuItem;
pub(super) struct Menu {
inner: MudaMenu,
}
impl Menu {
pub(super) fn new(event_scheduler: AppEventScheduler) -> Self {
// TODO: Remove as much app submenu special handling as possible
let app_submenu = Submenu::with_items("", true, &[]).unwrap();
let menu = MudaMenu::new();
menu.prepend(&app_submenu).unwrap();
menu.init_for_nsapp();
MenuEvent::set_event_handler(Some(move |event: MenuEvent| {
let mtm = objc2::MainThreadMarker::new().expect("only ever called from main thread");
let is_shortcut_triggered = objc2_app_kit::NSApplication::sharedApplication(mtm)
.mainMenu()
.map(|m| m.highlightedItem().is_some())
.unwrap_or_default();
if is_shortcut_triggered {
tracing::error!("A keyboard input triggered a menu event. This is most likely a bug. Please report!");
return;
}
let id = event.id().0.clone();
event_scheduler.schedule(AppEvent::MenuEvent { id });
}));
Menu { inner: menu }
}
pub(super) fn update(&self, entries: Vec<WrapperMenuItem>) {
let new_entries = menu_items_from_wrapper(entries);
let existing_entries = self.inner.items();
let mut new_entries_iter = new_entries.iter();
let mut existing_entries_iter = existing_entries.iter();
let incremental_update_ok = std::iter::from_fn(move || match (existing_entries_iter.next(), new_entries_iter.next()) {
(Some(MenuItemKind::Submenu(old)), Some(MenuItemKind::Submenu(new))) if old.text() == new.text() => {
replace_children(old, new.items());
Some(true)
}
(None, None) => None,
_ => Some(false),
})
.all(|b| b);
if !incremental_update_ok {
// Fallback to full replace
replace_children(&self.inner, new_entries);
}
}
}
fn menu_items_from_wrapper(entries: Vec<WrapperMenuItem>) -> Vec<MenuItemKind> {
let mut menu_items: Vec<MenuItemKind> = Vec::new();
for entry in entries {
match entry {
WrapperMenuItem::Action { id, text, enabled, shortcut } => {
let accelerator = shortcut.map(|s| Accelerator::new(Some(s.modifiers), s.key));
let item = MenuItem::with_id(id, text, enabled, accelerator);
menu_items.push(MenuItemKind::MenuItem(item));
}
WrapperMenuItem::Checkbox { id, text, enabled, shortcut, checked } => {
let accelerator = shortcut.map(|s| Accelerator::new(Some(s.modifiers), s.key));
let check = CheckMenuItem::with_id(id, text, enabled, checked, accelerator);
menu_items.push(MenuItemKind::Check(check));
}
WrapperMenuItem::SubMenu { text: name, items, .. } => {
let items = menu_items_from_wrapper(items);
let items = items.iter().map(menu_item_kind_to_dyn).collect::<Vec<&dyn IsMenuItem>>();
let submenu = Submenu::with_items(name, true, &items).unwrap();
menu_items.push(MenuItemKind::Submenu(submenu));
}
WrapperMenuItem::Separator => {
let separator = PredefinedMenuItem::separator();
menu_items.push(MenuItemKind::Predefined(separator));
}
}
}
menu_items
}
fn menu_item_kind_to_dyn(item: &MenuItemKind) -> &dyn IsMenuItem {
match item {
MenuItemKind::MenuItem(i) => i,
MenuItemKind::Submenu(i) => i,
MenuItemKind::Predefined(i) => i,
MenuItemKind::Check(i) => i,
MenuItemKind::Icon(i) => i,
}
}
fn replace_children<'a, T: Into<MenuContainer<'a>>>(menu: T, new_items: Vec<MenuItemKind>) {
let menu: MenuContainer = menu.into();
let items = menu.items();
for item in items.iter() {
menu.remove(menu_item_kind_to_dyn(item)).unwrap();
}
let items = new_items.iter().map(menu_item_kind_to_dyn).collect::<Vec<&dyn IsMenuItem>>();
menu.append_items(items.as_ref()).unwrap();
}
enum MenuContainer<'a> {
Menu(&'a MudaMenu),
Submenu(&'a Submenu),
}
impl<'a> MenuContainer<'a> {
fn items(&self) -> Vec<MenuItemKind> {
match self {
MenuContainer::Menu(menu) => menu.items(),
MenuContainer::Submenu(submenu) => submenu.items(),
}
}
fn remove(&self, item: &dyn IsMenuItem) -> Result<()> {
match self {
MenuContainer::Menu(menu) => menu.remove(item),
MenuContainer::Submenu(submenu) => submenu.remove(item),
}
}
fn append_items(&self, items: &[&dyn IsMenuItem]) -> Result<()> {
match self {
MenuContainer::Menu(menu) => menu.append_items(items),
MenuContainer::Submenu(submenu) => submenu.append_items(items),
}
}
}
impl<'a> From<&'a MudaMenu> for MenuContainer<'a> {
fn from(menu: &'a MudaMenu) -> Self {
MenuContainer::Menu(menu)
}
}
impl<'a> From<&'a Submenu> for MenuContainer<'a> {
fn from(submenu: &'a Submenu) -> Self {
MenuContainer::Submenu(submenu)
}
}
@@ -0,0 +1,50 @@
use windows::Win32::System::Com::{COINIT_APARTMENTTHREADED, CoInitializeEx};
use windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole};
use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
use windows::core::HSTRING;
use winit::event_loop::ActiveEventLoop;
use winit::window::{Window, WindowAttributes};
use crate::consts::APP_ID;
use crate::event::AppEventScheduler;
pub(super) struct NativeWindowImpl {
native_handle: native_handle::NativeWindowHandle,
}
impl super::NativeWindow for NativeWindowImpl {
fn init() {
// Attach to parent console if launched from a terminal (no-op otherwise)
unsafe {
let _ = AttachConsole(ATTACH_PARENT_PROCESS);
}
// Set stable app ID
let app_id = HSTRING::from(APP_ID);
unsafe {
let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok();
SetCurrentProcessExplicitAppUserModelID(&app_id).ok();
}
}
fn configure(attributes: WindowAttributes, _event_loop: &dyn ActiveEventLoop) -> WindowAttributes {
attributes
}
fn new(window: &dyn Window, _app_event_scheduler: AppEventScheduler) -> Self {
let native_handle = native_handle::NativeWindowHandle::new(window);
NativeWindowImpl { native_handle }
}
fn can_render(&self) -> bool {
self.native_handle.can_render()
}
}
impl Drop for NativeWindowImpl {
fn drop(&mut self) {
self.native_handle.destroy();
}
}
mod native_handle;
@@ -0,0 +1,410 @@
//! Implements a Windows-specific custom window frame (no titlebar, but native boarder, shadows and resize).
//! Look and feel should be similar to a standard window.
//!
//! Implementation notes:
//! - Windows that don't use standard decorations don't get native resize handles or shadows by default.
//! - We implement resize handles (outside the main window) by creating an invisible "helper" window that
//! is a little larger than the main window and positioned on top of it. The helper window does hit-testing
//! and triggers native resize operations on the main window when the user clicks and drags a resize area.
//! - The helper window is a invisible window that never activates, so it doesn't steal focus from the main window.
//! - The main window needs to update the helper window's position and size whenever it moves or resizes.
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Instant;
use wgpu::rwh::{HasWindowHandle, RawWindowHandle};
use windows::Win32::Foundation::*;
use windows::Win32::Graphics::Dwm::*;
use windows::Win32::Graphics::Gdi::*;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::Controls::MARGINS;
use windows::Win32::UI::HiDpi::*;
use windows::Win32::UI::WindowsAndMessaging::*;
use windows::core::PCWSTR;
use winit::window::Window;
#[derive(Default)]
struct NativeWindowState {
can_render: bool,
can_render_since: Option<Instant>,
}
#[derive(Clone)]
pub(super) struct NativeWindowHandle {
main: HWND,
helper: HWND,
prev_window_message_handler: isize,
state: Arc<Mutex<NativeWindowState>>,
}
impl NativeWindowHandle {
pub(super) fn new(window: &dyn Window) -> NativeWindowHandle {
// Extract Win32 HWND from winit.
let main = match window.window_handle().expect("No window handle").as_raw() {
RawWindowHandle::Win32(h) => HWND(h.hwnd.get() as *mut std::ffi::c_void),
_ => panic!("Not a Win32 window"),
};
// Register the invisible helper (resize ring) window class.
unsafe { ensure_helper_class() };
// Create the helper as a popup tool window that never activates.
// WS_EX_NOACTIVATE keeps focus on the main window; WS_EX_TOOLWINDOW hides it from Alt+Tab.
// https://learn.microsoft.com/windows/win32/winmsg/extended-window-styles
let ex = WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW;
let style = WS_POPUP;
let helper = unsafe {
CreateWindowExW(
ex,
PCWSTR(HELPER_CLASS_NAME.encode_utf16().collect::<Vec<_>>().as_ptr()),
PCWSTR::null(),
style,
0,
0,
0,
0,
Some(main),
None,
None,
// Pass the main window's HWND to WM_NCCREATE so the helper can store it.
Some(&main as *const _ as _),
)
}
.expect("CreateWindowExW failed");
// Subclass the main window.
// https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-setwindowlongptra
let prev_window_message_handler = unsafe { SetWindowLongPtrW(main, GWLP_WNDPROC, main_window_handle_message as *const () as isize) };
if prev_window_message_handler == 0 {
let _ = unsafe { DestroyWindow(helper) };
panic!("SetWindowLongPtrW failed");
}
let native_handle = NativeWindowHandle {
main,
helper,
prev_window_message_handler,
state: Arc::new(Mutex::new(NativeWindowState::default())),
};
registry::insert(&native_handle);
// Place the helper over the main window and show it without activation.
unsafe { position_helper(main, helper) };
let _ = unsafe { ShowWindow(helper, SW_SHOWNOACTIVATE) };
// DwmExtendFrameIntoClientArea is needed to keep native window frame (but no titlebar).
// https://learn.microsoft.com/windows/win32/api/dwmapi/nf-dwmapi-dwmextendframeintoclientarea
// https://learn.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
let mut boarder_size: u32 = 1;
let _ = unsafe { DwmGetWindowAttribute(main, DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, &mut boarder_size as *mut _ as *mut _, size_of::<u32>() as u32) };
let margins = MARGINS {
cxLeftWidth: 0,
cxRightWidth: 0,
cyBottomHeight: 0,
cyTopHeight: boarder_size as i32,
};
let _ = unsafe { DwmExtendFrameIntoClientArea(main, &margins) };
let hinst: HINSTANCE = unsafe { GetModuleHandleW(None) }.unwrap().into();
// Set taskbar icon
if let Ok(big) = unsafe {
LoadImageW(
Some(hinst),
PCWSTR(1usize as *const u16),
IMAGE_ICON,
GetSystemMetrics(SM_CXICON),
GetSystemMetrics(SM_CYICON),
LR_SHARED,
)
} {
unsafe { SetClassLongPtrW(main, GCLP_HICON, big.0 as isize) };
unsafe { SendMessageW(main, WM_SETICON, Some(WPARAM(ICON_BIG as usize)), Some(LPARAM(big.0 as isize))) };
}
// Set window icon
if let Ok(small) = unsafe {
LoadImageW(
Some(hinst),
PCWSTR(1usize as *const u16),
IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON),
LR_SHARED,
)
} {
unsafe { SetClassLongPtrW(main, GCLP_HICONSM, small.0 as isize) };
unsafe { SendMessageW(main, WM_SETICON, Some(WPARAM(ICON_SMALL as usize)), Some(LPARAM(small.0 as isize))) };
}
// Force window update
let _ = unsafe { SetWindowPos(main, None, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE) };
native_handle
}
pub(super) fn destroy(&self) {
registry::remove_by_main(self.main);
// Undo subclassing and destroy the helper window.
let _ = unsafe { SetWindowLongPtrW(self.main, GWLP_WNDPROC, self.prev_window_message_handler) };
if !self.helper.is_invalid() {
let _ = unsafe { DestroyWindow(self.helper) };
}
}
// Rendering should be disabled when window is minimized
// Rendering also needs to be disabled during minimize and restore animations
// Reenabling rendering is done after a small delay to account for restore animation
// TODO: Find a cleaner solution that doesn't depend on a timeout
pub(super) fn can_render(&self) -> bool {
let can_render = !unsafe { IsIconic(self.main).into() } && unsafe { IsWindowVisible(self.main).into() };
let Ok(mut state) = self.state.lock() else {
tracing::error!("Failed to lock NativeWindowState");
return true;
};
match (can_render, state.can_render, state.can_render_since) {
(true, false, None) => {
state.can_render_since = Some(Instant::now());
}
(true, false, Some(can_render_since)) if can_render_since.elapsed().as_millis() > 50 => {
state.can_render = true;
state.can_render_since = None;
}
(false, true, _) => {
state.can_render = false;
}
_ => {}
}
state.can_render
}
}
mod registry {
use std::cell::RefCell;
use windows::Win32::Foundation::HWND;
use super::NativeWindowHandle;
thread_local! {
static STORE: RefCell<Vec<NativeWindowHandle>> = RefCell::new(Vec::new());
}
pub(super) fn find_by_main(main: HWND) -> Option<NativeWindowHandle> {
STORE.with_borrow(|vec| vec.iter().find(|h| h.main == main).cloned())
}
pub(super) fn remove_by_main(main: HWND) {
STORE.with_borrow_mut(|vec| {
vec.retain(|h| h.main != main);
});
}
pub(super) fn insert(handle: &NativeWindowHandle) {
STORE.with_borrow_mut(|vec| {
vec.push(handle.clone());
});
}
}
const HELPER_CLASS_NAME: &str = "Helper\0";
static HELPER_CLASS_LOCK: OnceLock<u16> = OnceLock::new();
unsafe fn ensure_helper_class() {
// Register a window class for the invisible resize helper.
let _ = *HELPER_CLASS_LOCK.get_or_init(|| {
let class_name: Vec<u16> = HELPER_CLASS_NAME.encode_utf16().collect();
let wc = WNDCLASSW {
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(helper_window_handle_message),
hInstance: unsafe { GetModuleHandleW(None).unwrap().into() },
hIcon: HICON::default(),
hCursor: unsafe { LoadCursorW(None, IDC_ARROW).unwrap() },
// No painting; the ring is invisible.
hbrBackground: HBRUSH::default(),
lpszClassName: PCWSTR(class_name.as_ptr()),
..Default::default()
};
unsafe { RegisterClassW(&wc) }
});
}
// Main window message handler, called on the UI thread for every message the main window receives.
unsafe extern "system" fn main_window_handle_message(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
if msg == WM_NCCALCSIZE && wparam.0 != 0 {
let params = unsafe { &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS) };
// When maximized, shrink to visible frame so content doesn't extend beyond it.
if unsafe { IsZoomed(hwnd).as_bool() } && !is_effectively_fullscreen(params.rgrc[0]) {
let dpi = unsafe { GetDpiForWindow(hwnd) };
let size = unsafe { GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) };
let pad = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) };
let inset = (size + pad) as i32;
params.rgrc[0].left += inset;
params.rgrc[0].top += inset;
params.rgrc[0].right -= inset;
params.rgrc[0].bottom -= inset;
}
// Return 0 to to tell Windows to skip the default non-client area calculation and drawing.
return LRESULT(0);
}
let Some(handle) = registry::find_by_main(hwnd) else {
return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
};
match msg {
// Keep the invisible resize helper in sync with moves/resizes/visibility.
WM_MOVE | WM_MOVING | WM_SIZE | WM_SIZING | WM_WINDOWPOSCHANGED | WM_SHOWWINDOW => {
if msg == WM_SHOWWINDOW {
if wparam.0 == 0 {
let _ = unsafe { ShowWindow(handle.helper, SW_HIDE) };
} else {
let _ = unsafe { ShowWindow(handle.helper, SW_SHOWNOACTIVATE) };
}
}
unsafe { position_helper(hwnd, handle.helper) };
}
// If the main window is destroyed, destroy the helper too.
// Should only be needed if windows forcefully destroys the main window.
WM_DESTROY => {
let _ = unsafe { DestroyWindow(handle.helper) };
}
_ => {}
}
// Ensure the previous window message handler is not null.
assert_ne!(handle.prev_window_message_handler, 0);
// Call the previous window message handler, this is a standard subclassing pattern.
let prev_window_message_handler_fn_ptr: *const () = std::ptr::without_provenance(handle.prev_window_message_handler as usize);
let prev_window_message_handler_fn = unsafe { std::mem::transmute::<_, _>(prev_window_message_handler_fn_ptr) };
unsafe { CallWindowProcW(Some(prev_window_message_handler_fn), hwnd, msg, wparam, lparam) }
}
// Helper window message handler, called on the UI thread for every message the helper window receives.
unsafe extern "system" fn helper_window_handle_message(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
match msg {
// Helper window creation, should be the first message that the helper window receives.
WM_NCCREATE => {
// Main window HWND is provided when creating the helper window with `CreateWindowExW`
// Save main window HWND in GWLP_USERDATA so we can extract it later
let crate_struct = lparam.0 as *const CREATESTRUCTW;
let create_param = unsafe { (*crate_struct).lpCreateParams as *const HWND };
unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, (*create_param).0 as isize) };
return LRESULT(1);
}
// Invisible; no background erase.
WM_ERASEBKGND => return LRESULT(1),
// Tell windows what resize areas we are hitting, this is used to decide what cursor to show.
WM_NCHITTEST => {
let ht = unsafe { calculate_hit(hwnd, lparam) };
return LRESULT(ht as isize);
}
// This starts the system's resize loop for the main window if a resize area is hit.
// Helper window button down translates to SC_SIZE | WMSZ_* on the main window.
WM_NCLBUTTONDOWN | WM_NCRBUTTONDOWN | WM_NCMBUTTONDOWN => {
// Extract the main window's HWND from GWLP_USERDATA that we saved earlier.
let main_ptr = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) } as *mut std::ffi::c_void;
let main = HWND(main_ptr);
if unsafe { IsWindow(Some(main)).as_bool() } {
let Some(wmsz) = (unsafe { calculate_resize_direction(hwnd, lparam) }) else {
return LRESULT(0);
};
// Ensure that the main window can receive WM_SYSCOMMAND.
let _ = unsafe { SetForegroundWindow(main) };
// Start sizing on the main window in the calculated direction. (SC_SIZE + WMSZ_*)
let _ = unsafe { PostMessageW(Some(main), WM_SYSCOMMAND, WPARAM((SC_SIZE + wmsz) as usize), lparam) };
}
return LRESULT(0);
}
// Never activate the helper window, allows all inputs that don't hit the resize areas to pass through.
WM_MOUSEACTIVATE => return LRESULT(MA_NOACTIVATE as isize),
_ => {}
}
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}
const RESIZE_BAND_THICKNESS: i32 = 8;
// Position the helper window to match the main window's location and size (plus the resize band size).
unsafe fn position_helper(main: HWND, helper: HWND) {
let mut r = RECT::default();
let _ = unsafe { GetWindowRect(main, &mut r) };
let x = r.left - RESIZE_BAND_THICKNESS;
let y = r.top - RESIZE_BAND_THICKNESS;
let w = (r.right - r.left) + RESIZE_BAND_THICKNESS * 2;
let h = (r.bottom - r.top) + RESIZE_BAND_THICKNESS * 2;
let _ = unsafe { SetWindowPos(helper, Some(main), x, y, w, h, SWP_NOACTIVATE | SWP_NOSENDCHANGING) };
}
unsafe fn calculate_hit(helper: HWND, lparam: LPARAM) -> u32 {
let x = (lparam.0 & 0xFFFF) as i16 as i32;
let y = ((lparam.0 >> 16) & 0xFFFF) as i16 as i32;
let mut r = RECT::default();
let _ = unsafe { GetWindowRect(helper, &mut r) };
let on_top = y < (r.top + RESIZE_BAND_THICKNESS) as i32;
let on_right = x >= (r.right - RESIZE_BAND_THICKNESS) as i32;
let on_bottom = y >= (r.bottom - RESIZE_BAND_THICKNESS) as i32;
let on_left = x < (r.left + RESIZE_BAND_THICKNESS) as i32;
match (on_top, on_right, on_bottom, on_left) {
(true, _, _, true) => HTTOPLEFT,
(true, true, _, _) => HTTOPRIGHT,
(_, true, true, _) => HTBOTTOMRIGHT,
(_, _, true, true) => HTBOTTOMLEFT,
(true, _, _, _) => HTTOP,
(_, true, _, _) => HTRIGHT,
(_, _, true, _) => HTBOTTOM,
(_, _, _, true) => HTLEFT,
_ => HTTRANSPARENT as u32,
}
}
unsafe fn calculate_resize_direction(helper: HWND, lparam: LPARAM) -> Option<u32> {
match unsafe { calculate_hit(helper, lparam) } {
HTLEFT => Some(WMSZ_LEFT),
HTRIGHT => Some(WMSZ_RIGHT),
HTTOP => Some(WMSZ_TOP),
HTBOTTOM => Some(WMSZ_BOTTOM),
HTTOPLEFT => Some(WMSZ_TOPLEFT),
HTTOPRIGHT => Some(WMSZ_TOPRIGHT),
HTBOTTOMLEFT => Some(WMSZ_BOTTOMLEFT),
HTBOTTOMRIGHT => Some(WMSZ_BOTTOMRIGHT),
_ => None,
}
}
// Check if the rect is effectively fullscreen, meaning it would cover the entire monitor.
// We need to use this heuristic because Windows doesn't provide a way to check for fullscreen state.
fn is_effectively_fullscreen(rect: RECT) -> bool {
let hmon = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST) };
if hmon.is_invalid() {
return false;
}
let mut monitor_info = MONITORINFO {
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
..Default::default()
};
if !unsafe { GetMonitorInfoW(hmon, &mut monitor_info) }.as_bool() {
return false;
}
// Allow a tiny tolerance for DPI / rounding issues
const EPS: i32 = 1;
(rect.left - monitor_info.rcMonitor.left).abs() <= EPS
&& (rect.top - monitor_info.rcMonitor.top).abs() <= EPS
&& (rect.right - monitor_info.rcMonitor.right).abs() <= EPS
&& (rect.bottom - monitor_info.rcMonitor.bottom).abs() <= EPS
}
+53
View 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 }
@@ -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;
@@ -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);
}
});
});
}
@@ -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
View 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
}
}
@@ -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)
}
}
@@ -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;
@@ -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: &[],
})
}
@@ -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()))
}
}
}
@@ -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))
}
@@ -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) })
}
}
@@ -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
}
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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}");
}
}
}
@@ -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);
}
}
@@ -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],
}
@@ -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));
}
@@ -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);
}
}
}
@@ -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;
}
}
}
@@ -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}"),
}
}
}
}
@@ -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
View 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,
}
}
}
@@ -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, ' '),
)
}
}
@@ -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;
}
}
@@ -0,0 +1,27 @@
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;
#[cfg(target_os = "macos")]
mod keyboard_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;
@@ -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;
}
}
@@ -0,0 +1,128 @@
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;
#[cfg(target_os = "macos")]
use super::keyboard_handler::KeyboardHandlerImpl;
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()))
}
#[cfg(target_os = "macos")]
fn keyboard_handler(&self) -> Option<cef::KeyboardHandler> {
Some(cef::KeyboardHandler::new(KeyboardHandlerImpl::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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -0,0 +1,110 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_keyboard_handler_t, cef_base_ref_counted_t, cef_event_flags_t, cef_key_event_type_t};
use cef::{Browser, Frame, ImplBrowser, ImplBrowserHost, ImplFrame, ImplKeyboardHandler, KeyEvent, WrapKeyboardHandler};
const SHIFT: u32 = cef_event_flags_t::EVENTFLAG_SHIFT_DOWN.0;
const CONTROL: u32 = cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0;
const ALT: u32 = cef_event_flags_t::EVENTFLAG_ALT_DOWN.0;
const COMMAND: u32 = cef_event_flags_t::EVENTFLAG_COMMAND_DOWN.0;
const MODIFIER_MASK: u32 = SHIFT | CONTROL | ALT | COMMAND;
const COMMAND_SHIFT: u32 = COMMAND | SHIFT;
const SHIFT_ALT: u32 = SHIFT | ALT;
const VK_END: i32 = 0x23;
const VK_HOME: i32 = 0x24;
const VK_UP: i32 = 0x26;
const VK_DOWN: i32 = 0x28;
const VK_A: i32 = 0x41;
const VK_C: i32 = 0x43;
const VK_V: i32 = 0x56;
const VK_X: i32 = 0x58;
const VK_Z: i32 = 0x5A;
const KVK_HOME: i32 = 0x73;
const KVK_END: i32 = 0x77;
const NS_HOME_FUNCTION_KEY: u16 = 0xF729;
const NS_END_FUNCTION_KEY: u16 = 0xF72B;
pub(crate) struct KeyboardHandlerImpl {
object: *mut RcImpl<_cef_keyboard_handler_t, Self>,
}
impl KeyboardHandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
}
impl ImplKeyboardHandler for KeyboardHandlerImpl {
fn on_key_event(&self, browser: Option<&mut Browser>, event: Option<&KeyEvent>, _os_event: *mut u8) -> std::ffi::c_int {
let (Some(browser), Some(event)) = (browser, event) else { return 0 };
if event.type_ != cef_key_event_type_t::KEYEVENT_RAWKEYDOWN.into() {
return 0;
}
let shortcut = (event.modifiers & MODIFIER_MASK, event.windows_key_code);
let edit_operation: Option<fn(&Frame)> = match shortcut {
(COMMAND, VK_A) => Some(Frame::select_all),
(COMMAND, VK_C) => Some(Frame::copy),
(COMMAND, VK_V) => Some(Frame::paste),
(COMMAND_SHIFT, VK_V) => Some(Frame::paste_and_match_style),
(COMMAND, VK_X) => Some(Frame::cut),
(COMMAND, VK_Z) => Some(Frame::undo),
(COMMAND_SHIFT, VK_Z) => Some(Frame::redo),
_ => None,
};
if let Some(edit_operation) = edit_operation {
let Some(frame) = browser.focused_frame() else { return 0 };
edit_operation(&frame);
return 1;
}
let remap = match shortcut {
(SHIFT_ALT, VK_UP) => Some((VK_HOME, KVK_HOME, NS_HOME_FUNCTION_KEY)),
(SHIFT_ALT, VK_DOWN) => Some((VK_END, KVK_END, NS_END_FUNCTION_KEY)),
_ => None,
};
if let Some((windows_key_code, native_key_code, character)) = remap {
let Some(host) = browser.host() else { return 0 };
host.send_key_event(Some(&KeyEvent {
type_: cef_key_event_type_t::KEYEVENT_RAWKEYDOWN.into(),
modifiers: SHIFT,
windows_key_code,
native_key_code,
character,
unmodified_character: character,
..Default::default()
}));
return 1;
}
0
}
fn get_raw(&self) -> *mut _cef_keyboard_handler_t {
self.object.cast()
}
}
impl Clone for KeyboardHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl Rc for KeyboardHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapKeyboardHandler for KeyboardHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_keyboard_handler_t, Self>) {
self.object = object;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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
View 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
View 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)
}
}
@@ -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
}
}
@@ -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(())
});
}
}
@@ -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}");
}
}
@@ -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);
}
}
}
@@ -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
}
}
}
}
@@ -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;
}
}
}
}
@@ -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,
}
@@ -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(());
}
}
}
@@ -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
}
}
}
@@ -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
View 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
}
}
@@ -0,0 +1,31 @@
[package]
name = "graphite-desktop-wrapper"
version = "0.1.0"
description = "Graphite Desktop Wrapper"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[features]
gpu = ["graphite-editor/gpu", "graphene-std/shader-nodes"]
[dependencies]
# Local dependencies
graphite-editor = { workspace = true }
graphite-wasm-wrapper = { path = "../../frontend/wrapper", default-features = false, features = ["editor"] }
graphene-std = { workspace = true }
graph-craft = { workspace = true }
wgpu-executor = { workspace = true }
wgpu = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
dirs = { workspace = true }
vello = { workspace = true }
image = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
keyboard-types = { workspace = true }
base64 = { workspace = true }
@@ -0,0 +1,94 @@
use graphite_editor::messages::clipboard::utility_types::ClipboardContentRaw;
use graphite_editor::messages::prelude::*;
use super::DesktopWrapperMessageDispatcher;
use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, EditorMessage, OpenFileDialogContext, SaveFileDialogContext};
pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: DesktopWrapperMessage) {
match message {
DesktopWrapperMessage::FromWeb(message) => {
dispatcher.queue_editor_message(*message);
}
DesktopWrapperMessage::Wake => {
dispatcher.queue_editor_message(EditorMessage::Future(FutureMessage::Wake));
}
DesktopWrapperMessage::Input(message) => {
dispatcher.queue_editor_message(EditorMessage::InputPreprocessor(message));
}
DesktopWrapperMessage::FileDialogResult { path, content, context } => match context {
OpenFileDialogContext::Open => {
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenFile { path, content });
}
OpenFileDialogContext::Import => {
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportFile { path, content });
}
},
DesktopWrapperMessage::SaveFileDialogResult { path, context } => match context {
SaveFileDialogContext::Document { document_id, content } => {
dispatcher.respond(DesktopFrontendMessage::WriteFile { path: path.clone(), content });
dispatcher.queue_editor_message(EditorMessage::Portfolio(PortfolioMessage::DocumentPassMessage {
document_id,
message: DocumentMessage::SavedDocument { path: Some(path) },
}));
}
SaveFileDialogContext::File { content } => {
dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content });
}
},
DesktopWrapperMessage::OpenFile { path, content } => {
let message = PortfolioMessage::OpenFile { path, content };
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::ImportFile { path, content } => {
let message = PortfolioMessage::ImportFile { path, content };
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::PollNodeGraphEvaluation => dispatcher.poll_node_graph_evaluation(),
DesktopWrapperMessage::UpdateMaximized { maximized } => {
let message = FrontendMessage::UpdateMaximized { maximized };
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::UpdateFullscreen { fullscreen } => {
let message = FrontendMessage::UpdateFullscreen { fullscreen };
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::LoadDocumentContent { id, document } => {
let message = PersistentStateMessage::LoadDocument { document_id: id, document };
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::LoadPersistedState { state } => {
let message = PersistentStateMessage::LoadState { state };
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::LoadPreferences { preferences } => {
let message = PreferencesMessage::Load { preferences };
dispatcher.queue_editor_message(message);
}
#[cfg(target_os = "macos")]
DesktopWrapperMessage::MenuEvent { id } => {
if let Some(message) = crate::utils::menu::parse_item_path(id) {
dispatcher.queue_editor_message(message);
} else {
tracing::error!("Received a malformed MenuEvent id");
}
}
#[cfg(not(target_os = "macos"))]
DesktopWrapperMessage::MenuEvent { id: _ } => {}
DesktopWrapperMessage::ClipboardReadResult { content } => {
if let Some(content) = content {
let message = ClipboardMessage::ReadClipboard {
content: ClipboardContentRaw::Text(content),
};
dispatcher.queue_editor_message(message);
}
}
DesktopWrapperMessage::PointerLockMove { x, y } => {
let message = AppWindowMessage::PointerLockMove { x, y };
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::LoadThirdPartyLicenses { text } => {
let message = DialogMessage::RequestLicensesThirdPartyDialogWithLicenseText { license_text: text };
dispatcher.queue_editor_message(message);
}
}
}
@@ -0,0 +1,7 @@
use super::DesktopWrapperMessageDispatcher;
use super::messages::EditorMessage;
pub(super) fn intercept_editor_message(_dispatcher: &mut DesktopWrapperMessageDispatcher, message: EditorMessage) -> Option<EditorMessage> {
// TODO: remove if it turns out to be unnecessary
Some(message)
}

Some files were not shown because too many files have changed in this diff Show More