fix merge conflicts

This commit is contained in:
0SlowPoke0 2025-09-10 11:20:05 +05:30
commit 9d34664989
582 changed files with 38567 additions and 37145 deletions

View file

@ -23,7 +23,6 @@
"streetsidesoftware.code-spell-checker",
// Helpful
"mhutchie.git-graph",
"waderyan.gitblame",
"qezhu.gitlink",
"wmaurer.change-case"
]

View file

@ -47,12 +47,15 @@ jobs:
echo "Latest updated version:"
rustc --version
- name: 🦀 Fetch Rust dependencies
run: |
echo "If it fails here, the committed Cargo.lock may be out of date"
cargo fetch --locked
- name: ✂ Replace template in <head> of index.html
if: github.ref != 'refs/heads/master'
env:
INDEX_HTML_HEAD_REPLACEMENT: ""
run: |
# Remove the INDEX_HTML_HEAD_REPLACEMENT environment variable for build links (not master deploys)
git rev-parse --abbrev-ref HEAD | grep master > /dev/null || export INDEX_HTML_HEAD_REPLACEMENT=""
sed -i "s|<!-- INDEX_HTML_HEAD_REPLACEMENT -->|$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html
- name: 🌐 Build Graphite web code
@ -105,7 +108,7 @@ jobs:
- name: 🧪 Run Rust tests
run: |
mold -run cargo test --all-features --workspace
mold -run cargo test --all-features
- name: 📃 Generate code documentation info for website
if: github.ref == 'refs/heads/master'

View file

@ -1,7 +1,7 @@
name: Profiling Changes
on:
pull_request:
pull_request: {}
env:
CARGO_TERM_COLOR: always
@ -9,58 +9,83 @@ env:
jobs:
profile:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
uses: dtolnay/rust-toolchain@stable
- name: Install Valgrind
run: |
sudo apt update
sudo apt install -y valgrind
- name: Cache dependencies
uses: actions/cache@v3
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# Cache on Cargo.lock file
cache-on-failure: true
- name: Cache iai-callgrind binary
id: cache-iai
uses: actions/cache@v4
with:
path: ~/.cargo/bin/iai-callgrind-runner
key: ${{ runner.os }}-iai-callgrind-runner-0.16.1
- name: Install iai-callgrind
if: steps.cache-iai.outputs.cache-hit != 'true'
run: |
cargo install iai-callgrind-runner@0.12.3
cargo install iai-callgrind-runner@0.16.1
- name: Checkout master branch
run: |
git fetch origin master:master
git checkout master
- name: Get master commit SHA
id: master-sha
run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Cache benchmark baselines
id: cache-benchmark-baselines
uses: actions/cache@v4
with:
path: target/iai
key: ${{ runner.os }}-benchmark-baselines-master-${{ steps.master-sha.outputs.sha }}
restore-keys: |
${{ runner.os }}-benchmark-baselines-master-
- name: Run baseline benchmarks
if: steps.cache-benchmark-baselines.outputs.cache-hit != 'true'
run: |
# Compile benchmarks
cargo bench --bench compile_demo_art_iai -- --save-baseline=master
# Runtime benchmarks
cargo bench --bench update_executor_iai -- --save-baseline=master
cargo bench --bench run_once_iai -- --save-baseline=master
cargo bench --bench run_cached_iai -- --save-baseline=master
- name: Checkout PR branch
run: |
git checkout ${{ github.event.pull_request.head.sha }}
- name: Run PR benchmarks
id: benchmark
run: |
BENCH_OUTPUT=$(cargo bench --bench compile_demo_art_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g')
echo "BENCHMARK_OUTPUT<<EOF" >> $GITHUB_OUTPUT
echo "$BENCH_OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Compile benchmarks
cargo bench --bench compile_demo_art_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/compile_output.json
# Runtime benchmarks
cargo bench --bench update_executor_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/update_output.json
cargo bench --bench run_once_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/run_once_output.json
cargo bench --bench run_cached_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/run_cached_output.json
- name: Make old comments collapsed by default
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
@ -85,11 +110,17 @@ jobs:
}
- name: Comment PR
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const benchmarkOutput = JSON.parse(`${{ steps.benchmark.outputs.BENCHMARK_OUTPUT }}`);
const fs = require('fs');
const compileOutput = JSON.parse(fs.readFileSync('/tmp/compile_output.json', 'utf8'));
const updateOutput = JSON.parse(fs.readFileSync('/tmp/update_output.json', 'utf8'));
const runOnceOutput = JSON.parse(fs.readFileSync('/tmp/run_once_output.json', 'utf8'));
const runCachedOutput = JSON.parse(fs.readFileSync('/tmp/run_cached_output.json', 'utf8'));
let significantChanges = false;
let commentBody = "";
@ -110,58 +141,108 @@ jobs:
return str.padStart(len);
}
for (const benchmark of benchmarkOutput) {
if (benchmark.callgrind_summary && benchmark.callgrind_summary.summaries) {
const summary = benchmark.callgrind_summary.summaries[0];
const irDiff = summary.events.Ir;
if (irDiff.diff_pct !== null) {
const changePercentage = formatPercentage(irDiff.diff_pct);
const color = irDiff.diff_pct > 0 ? "red" : "lime";
commentBody += "---\n\n";
commentBody += `${benchmark.module_path} ${benchmark.id}:${benchmark.details}\n`;
commentBody += `Instructions: \`${formatNumber(irDiff.old)}\` (master) -> \`${formatNumber(irDiff.new)}\` (HEAD) : `;
commentBody += `$$\\color{${color}}${changePercentage.replace("%", "\\\\%")}$$\n\n`;
commentBody += "<details>\n<summary>Detailed metrics</summary>\n\n```\n";
commentBody += `Baselines: master| HEAD\n`;
for (const [eventKind, costsDiff] of Object.entries(summary.events)) {
if (costsDiff.diff_pct !== null) {
const changePercentage = formatPercentage(costsDiff.diff_pct);
const line = `${padRight(eventKind, 20)} ${padLeft(formatNumber(costsDiff.old), 11)}|${padLeft(formatNumber(costsDiff.new), 11)} ${padLeft(changePercentage, 15)}`;
commentBody += `${line}\n`;
function processBenchmarkOutput(benchmarkOutput, sectionTitle, isLast = false) {
let sectionBody = "";
let hasResults = false;
let hasSignificantChanges = false;
for (const benchmark of benchmarkOutput) {
if (benchmark.profiles && benchmark.profiles.length > 0) {
const profile = benchmark.profiles[0];
if (profile.summaries && profile.summaries.parts && profile.summaries.parts.length > 0) {
const part = profile.summaries.parts[0];
if (part.metrics_summary && part.metrics_summary.Callgrind && part.metrics_summary.Callgrind.Ir) {
const irData = part.metrics_summary.Callgrind.Ir;
if (irData.diffs && irData.diffs.diff_pct !== null) {
const irDiff = {
diff_pct: parseFloat(irData.diffs.diff_pct),
old: irData.metrics.Both[1].Int,
new: irData.metrics.Both[0].Int
};
hasResults = true;
const changePercentage = formatPercentage(irDiff.diff_pct);
const color = irDiff.diff_pct > 0 ? "red" : "lime";
sectionBody += `**${benchmark.module_path} ${benchmark.id}:${benchmark.details}**\n`;
sectionBody += `Instructions: \`${formatNumber(irDiff.old)}\` (master) → \`${formatNumber(irDiff.new)}\` (HEAD) : `;
sectionBody += `$$\\color{${color}}${changePercentage.replace("%", "\\\\%")}$$\n\n`;
sectionBody += "<details>\n<summary>Detailed metrics</summary>\n\n```\n";
sectionBody += `Baselines: master| HEAD\n`;
for (const [metricName, metricData] of Object.entries(part.metrics_summary.Callgrind)) {
if (metricData.diffs && metricData.diffs.diff_pct !== null) {
const changePercentage = formatPercentage(parseFloat(metricData.diffs.diff_pct));
const oldValue = metricData.metrics.Both[1].Int || metricData.metrics.Both[1].Float;
const newValue = metricData.metrics.Both[0].Int || metricData.metrics.Both[0].Float;
const line = `${padRight(metricName, 20)} ${padLeft(formatNumber(Math.round(oldValue)), 11)}|${padLeft(formatNumber(Math.round(newValue)), 11)} ${padLeft(changePercentage, 15)}`;
sectionBody += `${line}\n`;
}
}
sectionBody += "```\n</details>\n\n";
if (Math.abs(irDiff.diff_pct) > 5) {
significantChanges = true;
hasSignificantChanges = true;
}
}
}
}
commentBody += "```\n</details>\n\n";
if (Math.abs(irDiff.diff_pct) > 5) {
significantChanges = true;
}
}
}
if (hasResults) {
// Wrap section in collapsible details, open only if there are significant changes
const openAttribute = hasSignificantChanges ? " open" : "";
const ruler = isLast ? "" : "\n\n---";
return `<details${openAttribute}>\n<summary><h2>${sectionTitle}</h2></summary>\n\n${sectionBody}${ruler}\n</details>`;
}
return "";
}
const output = `
<details open>
// Process each benchmark category
const sections = [
{ output: compileOutput, title: "🔧 Graph Compilation" },
{ output: updateOutput, title: "🔄 Executor Update" },
{ output: runOnceOutput, title: "🚀 Render: Cold Execution" },
{ output: runCachedOutput, title: "⚡ Render: Cached Execution" }
];
<summary>Performance Benchmark Results</summary>
// Generate sections and determine which ones have results
const generatedSections = sections.map(({ output, title }) =>
processBenchmarkOutput(output, title, true) // temporarily mark all as last
).filter(section => section.length > 0);
${commentBody}
// Re-generate with correct isLast flags
let sectionIndex = 0;
const finalSections = sections.map(({ output, title }) => {
const section = processBenchmarkOutput(output, title, true); // check if it has results
if (section.length > 0) {
const isLast = sectionIndex === generatedSections.length - 1;
sectionIndex++;
return processBenchmarkOutput(output, title, isLast);
}
return "";
}).filter(section => section.length > 0);
</details>
`;
// Combine all sections
commentBody = finalSections.join("\n\n");
if (significantChanges) {
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
});
if (commentBody.length > 0) {
const output = `<details open>\n<summary>Performance Benchmark Results</summary>\n\n${commentBody}\n</details>`;
if (significantChanges) {
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
});
} else {
console.log("No significant performance changes detected. Skipping comment.");
console.log(output);
}
} else {
console.log("No significant performance changes detected. Skipping comment.");
console.log(output);
console.log("No benchmark results to display.");
}

View file

@ -44,46 +44,6 @@ jobs:
# Remove the INDEX_HTML_HEAD_INCLUSION environment variable for build links (not master deploys)
git rev-parse --abbrev-ref HEAD | grep master > /dev/null || export INDEX_HTML_HEAD_INCLUSION=""
- name: 🌐 Build Graphite website with Zola
env:
MODE: prod
run: |
cd website
npm run install-fonts
zola --config config.toml build --minify
- name: 💿 Restore cache of `website/other/dist` directory, if available and `website/other` didn't change
if: steps.changes.outputs.website-other != 'true'
id: cache-website-other-dist
uses: actions/cache/restore@v3
with:
path: website/other/dist
key: website-other-dist-${{ runner.os }}
- name: 🟢 Set up Node only if we are going to build in the next step
if: steps.cache-website-other-dist.outputs.cache-hit != 'true'
uses: actions/setup-node@v4
with:
node-version: "latest"
- name: 📁 Build `website/other` directory only if changed or not cached
if: steps.cache-website-other-dist.outputs.cache-hit != 'true'
id: build-website-other
run: |
sh website/other/build.sh
- name: 💾 Save cache of `website/other/dist` directory if it was built above
if: steps.cache-website-other-dist.outputs.cache-hit != 'true'
uses: actions/cache/save@v3
with:
path: website/other/dist
key: ${{ steps.cache-website-other-dist.outputs.cache-primary-key }}
- name: 🚚 Move `website/other/dist` contents to `website/public`
run: |
mkdir -p website/public
mv website/other/dist/* website/public
- name: 💿 Obtain cache of auto-generated code docs artifacts
id: cache-website-code-docs
uses: actions/cache/restore@v3
@ -103,9 +63,22 @@ jobs:
mkdir artifacts
mv hierarchical_message_system_tree.txt artifacts/hierarchical_message_system_tree.txt
- name: 🚚 Move `artifacts` contents to `website/public`
- name: 🚚 Move `artifacts` contents to the project root
run: |
mv artifacts/* website/public
mv artifacts/* .
- name: 🔧 Build auto-generated code docs artifacts into HTML
run: |
cd website
npm run generate-editor-structure
- name: 🌐 Build Graphite website with Zola
env:
MODE: prod
run: |
cd website
npm run install-fonts
zola --config config.toml build --minify
- name: 📤 Publish to Cloudflare Pages
id: cloudflare

2
.gitignore vendored
View file

@ -3,7 +3,9 @@ target/
*.exrc
perf.data*
profile.json
profile.json.gz
flamegraph.svg
.idea/
.direnv
hierarchical_message_system_tree.txt
hierarchical_message_system_tree.html

44
.nix/flake.lock generated
View file

@ -1,5 +1,19 @@
{
"nodes": {
"flake-compat": {
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"revCount": 69,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
@ -20,27 +34,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1748190013,
"narHash": "sha256-R5HJFflOfsP5FBtk+zE8FpL8uqE7n62jqOsADvVshhE=",
"lastModified": 1754214453,
"narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "62b852f6c6742134ade1abdd2a21685fd617a291",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1748190013,
"narHash": "sha256-R5HJFflOfsP5FBtk+zE8FpL8uqE7n62jqOsADvVshhE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "62b852f6c6742134ade1abdd2a21685fd617a291",
"rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376",
"type": "github"
},
"original": {
@ -52,9 +50,9 @@
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"nixpkgs-unstable": "nixpkgs-unstable",
"rust-overlay": "rust-overlay"
}
},
@ -65,11 +63,11 @@
]
},
"locked": {
"lastModified": 1748399823,
"narHash": "sha256-kahD8D5hOXOsGbNdoLLnqCL887cjHkx98Izc37nDjlA=",
"lastModified": 1753238793,
"narHash": "sha256-jmQeEpgX+++MEgrcikcwoSiI7vDZWLP0gci7XiWb9uQ=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "d68a69dc71bc19beb3479800392112c2f6218159",
"rev": "0ad7ab4ca8e83febf147197e65c006dff60623ab",
"type": "github"
},
"original": {

View file

@ -1,84 +1,134 @@
# This is a helper file for people using NixOS as their operating system.
# If you don't know what this file does, you can safely ignore it.
# This file defines both the development environment for the project.
# This file defines the reproducible development environment for the project.
#
# Development Environment:
# - Provides all necessary tools for Rust/WASM development
# - Includes Tauri dependencies for desktop app development
# - Provides all necessary tools for Rust/Wasm development
# - Includes dependencies for desktop app development
# - Sets up profiling and debugging tools
# - Configures mold as the default linker for faster builds
#
#
# Usage:
# - Development shell: `nix develop`
# - Development shell: `nix develop .nix` from the project root
# - Run in dev shell with direnv: add `use flake` to .envrc
{
description = "Development environment and build configuration";
inputs = {
# This url should be changed to match your system packages if you work on tauri because you need to use the same graphics library versions as the ones used by your system
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
# This is used to provide a identical development shell at `shell.nix` for users that do not use flakes
flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz";
};
outputs = { nixpkgs, nixpkgs-unstable, rust-overlay, flake-utils, ... }:
outputs = { nixpkgs, rust-overlay, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
inherit system overlays;
};
pkgs-unstable = import nixpkgs-unstable {
inherit system overlays;
};
rustc-wasm = pkgs.rust-bin.stable.latest.default.override {
rustExtensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ];
rust = pkgs.rust-bin.stable.latest.default.override {
targets = [ "wasm32-unknown-unknown" ];
extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ];
extensions = rustExtensions;
};
rustGPUToolchainPkg = pkgs.rust-bin.nightly."2025-06-23".default.override {
extensions = rustExtensions ++ [ "rustc-dev" "llvm-tools" ];
};
rustGPUToolchainRustPlatform = pkgs.makeRustPlatform {
cargo = rustGPUToolchainPkg;
rustc = rustGPUToolchainPkg;
};
rustc_codegen_spirv = rustGPUToolchainRustPlatform.buildRustPackage (finalAttrs: {
pname = "rustc_codegen_spirv";
version = "0-unstable-2025-08-04";
src = pkgs.fetchFromGitHub {
owner = "Rust-GPU";
repo = "rust-gpu";
rev = "c12f216121820580731440ee79ebc7403d6ea04f";
hash = "sha256-rG1cZvOV0vYb1dETOzzbJ0asYdE039UZImobXZfKIno=";
};
cargoHash = "sha256-AEigcEc5wiBd3zLqWN/2HSbkfOVFneAqNvg9HsouZf4=";
cargoBuildFlags = [ "-p" "rustc_codegen_spirv" "--features=use-compiled-tools" "--no-default-features" ];
doCheck = false;
});
rustGpuCargo = pkgs.writeShellScriptBin "cargo" ''
#!${pkgs.lib.getExe pkgs.bash}
filtered_args=()
for arg in "$@"; do
case "$arg" in
+nightly|+nightly-*) ;;
*) filtered_args+=("$arg") ;;
esac
done
exec ${rustGPUToolchainPkg}/bin/cargo ${"\${filtered_args[@]}"}
'';
rustGpuPathOverride = "${rustGpuCargo}/bin:${rustGPUToolchainPkg}/bin";
libcef = pkgs.libcef.overrideAttrs (finalAttrs: previousAttrs: {
version = "139.0.17";
gitRevision = "6c347eb";
chromiumVersion = "139.0.7258.31";
srcHash = "sha256-kRMO8DP4El1qytDsAZBdHvR9AAHXce90nPdyfJailBg=";
__intentionallyOverridingVersion = true;
postInstall = ''
strip $out/lib/*
'';
});
libcefPath = pkgs.runCommand "libcef-path" {} ''
mkdir -p $out
ln -s ${libcef}/include $out/include
find ${libcef}/lib -type f -name "*" -exec ln -s {} $out/ \;
find ${libcef}/libexec -type f -name "*" -exec ln -s {} $out/ \;
cp -r ${libcef}/share/cef/* $out/
echo '${builtins.toJSON {
type = "minimal";
name = builtins.baseNameOf libcef.src.url;
sha1 = "";
}}' > $out/archive.json
'';
# Shared build inputs - system libraries that need to be in LD_LIBRARY_PATH
buildInputs = with pkgs; [
# System libraries
wayland
openssl
vulkan-loader
mesa
libraw
libGL
# Tauri dependencies: keep in sync with https://v2.tauri.app/start/prerequisites/#system-dependencies (under the NixOS tab)
at-spi2-atk
atkmm
cairo
gdk-pixbuf
glib
gtk3
harfbuzz
librsvg
libsoup_3
pango
webkitgtk_4_1
openssl
# X11 libraries, not needed on wayland! Remove when x11 is finally dead
libxkbcommon
xorg.libXcursor
xorg.libxcb
xorg.libX11
];
# Development tools that don't need to be in LD_LIBRARY_PATH
buildTools = [
rustc-wasm
rust
pkgs.nodejs
pkgs.nodePackages.npm
pkgs.binaryen
pkgs.wasm-bindgen-cli
pkgs-unstable.wasm-pack
pkgs.wasm-pack
pkgs.pkg-config
pkgs.git
pkgs.gobject-introspection
pkgs-unstable.cargo-tauri
pkgs-unstable.cargo-about
pkgs.cargo-about
# Linker
pkgs.mold
@ -88,12 +138,11 @@
cargo-watch
cargo-nextest
cargo-expand
# Profiling tools
gnuplot
samply
cargo-flamegraph
];
in
{
@ -101,10 +150,12 @@
devShells.default = pkgs.mkShell {
packages = buildInputs ++ buildTools ++ devTools;
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
GIO_MODULE_DIR="${pkgs.glib-networking}/lib/gio/modules/";
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath buildInputs}:${libcefPath}";
CEF_PATH = libcefPath;
XDG_DATA_DIRS="${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS";
RUST_GPU_PATH_OVERRIDE = rustGpuPathOverride;
RUSTC_CODEGEN_SPIRV_PATH = "${rustc_codegen_spirv}/lib/librustc_codegen_spirv.so";
shellHook = ''
alias cargo='mold --run cargo'

31
.nix/shell.nix Normal file
View file

@ -0,0 +1,31 @@
# This is a helper file for people using NixOS as their operating system.
# If you don't know what this file does, you can safely ignore it.
# If you are using Nix as your package manager, you can run 'nix-shell .nix'
# in the root directory of the project and Nix will open a bash shell
# with all the packages needed to build and run Graphite installed.
# A shell.nix file is used in the Nix ecosystem to define a development
# environment with specific dependencies. When you enter a Nix shell using
# this file, it ensures that all the specified tools and libraries are
# available regardless of the host system's configuration. This provides
# a reproducible development environment across different machines and developers.
# You can enter the Nix shell and run Graphite like normal with:
# > npm start
# Or you can run it like this without needing to first enter the Nix shell:
# > nix-shell .nix --command "npm start"
# Uses flake compat to provide a development shell that is identical to the one defined in the flake
(import
(
let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
nodeName = lock.nodes.root.inputs.flake-compat;
in
fetchTarball {
url = lock.nodes.${nodeName}.locked.url;
sha256 = lock.nodes.${nodeName}.locked.narHash;
}
)
{ src = ./.; }
).shellNix

View file

@ -13,7 +13,6 @@
"streetsidesoftware.code-spell-checker",
// Helpful
"mhutchie.git-graph",
"waderyan.gitblame",
"qezhu.gitlink",
"wmaurer.change-case"
]

17
.vscode/settings.json vendored
View file

@ -33,18 +33,21 @@
},
// Rust Analyzer config
"rust-analyzer.cargo.allTargets": false,
"rust-analyzer.procMacro.ignored": {
"serde_derive": ["Serialize", "Deserialize"],
"specta_macros": ["Type"] // Disabled because of: https://github.com/specta-rs/specta/issues/387
},
// ESLint config
"eslint.format.enable": true,
"eslint.workingDirectories": ["./frontend", "./website/other/bezier-rs-demos", "./website"],
"eslint.workingDirectories": ["./frontend", "./website"],
"eslint.validate": ["javascript", "typescript", "svelte"],
// Svelte config
"svelte.plugin.svelte.compilerWarnings": {
// NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"css-unused-selector": "ignore",
"vite-plugin-svelte-css-no-scopable-elements": "ignore",
"a11y-no-static-element-interactions": "ignore",
"a11y-no-noninteractive-element-interactions": "ignore",
"a11y-click-events-have-key-events": "ignore"
"css-unused-selector": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"vite-plugin-svelte-css-no-scopable-elements": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y-no-static-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y-no-noninteractive-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y-click-events-have-key-events": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
},
// VS Code config
"html.format.wrapLineLength": 200,

4817
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,55 +1,76 @@
[workspace]
members = [
"desktop",
"desktop/wrapper",
"desktop/embedded-resources",
"editor",
"proc-macros",
"frontend/wasm",
"frontend/src-tauri",
"libraries/dyn-any",
"libraries/path-bool",
"libraries/math-parser",
"node-graph/gapplication-io",
"node-graph/gbrush",
"node-graph/gcore",
"node-graph/gstd",
"node-graph/gcore-shaders",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"node-graph/graph-craft",
"node-graph/graphene-cli",
"node-graph/graster-nodes",
"node-graph/graster-nodes/shaders",
"node-graph/gstd",
"node-graph/gsvg-renderer",
"node-graph/interpreted-executor",
"node-graph/node-macro",
"node-graph/preprocessor",
"libraries/dyn-any",
"libraries/path-bool",
"libraries/bezier-rs",
"libraries/math-parser",
"website/other/bezier-rs-demos/wasm",
"node-graph/wgpu-executor",
"proc-macros",
]
default-members = [
"desktop",
"desktop/wrapper",
"editor",
"frontend/wasm",
"libraries/dyn-any",
"libraries/path-bool",
"libraries/math-parser",
"node-graph/gapplication-io",
"node-graph/gbrush",
"node-graph/gcore",
"node-graph/gstd",
"node-graph/gcore-shaders",
"node-graph/graster-nodes/shaders",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"node-graph/graph-craft",
"node-graph/graphene-cli",
"node-graph/graster-nodes",
"node-graph/gstd",
"node-graph/gsvg-renderer",
"node-graph/interpreted-executor",
"node-graph/node-macro",
"node-graph/preprocessor",
"node-graph/wgpu-executor",
# blocked by https://github.com/rust-lang/cargo/issues/15890
# "proc-macros",
]
resolver = "2"
[workspace.dependencies]
# Local dependencies
bezier-rs = { path = "libraries/bezier-rs", features = ["dyn-any", "serde"] }
dyn-any = { path = "libraries/dyn-any", features = ["derive", "glam", "reqwest", "log-bad-types", "rc"] }
preprocessor = { path = "node-graph/preprocessor"}
dyn-any = { path = "libraries/dyn-any", features = [
"derive",
"glam",
"reqwest",
"log-bad-types",
"rc",
] }
preprocessor = { path = "node-graph/preprocessor" }
math-parser = { path = "libraries/math-parser" }
path-bool = { path = "libraries/path-bool" }
graphene-application-io = { path = "node-graph/gapplication-io" }
graphene-brush = { path = "node-graph/gbrush" }
graphene-core = { path = "node-graph/gcore" }
graphene-core-shaders = { path = "node-graph/gcore-shaders" }
graphene-math-nodes = { path = "node-graph/gmath-nodes" }
graphene-path-bool = { path = "node-graph/gpath-bool" }
graph-craft = { path = "node-graph/graph-craft" }
@ -63,7 +84,7 @@ graphite-proc-macros = { path = "proc-macros" }
# Workspace dependencies
rustc-hash = "2.0"
bytemuck = { version = "1.13", features = ["derive"] }
bytemuck = { version = "1.13", features = ["derive", "min_const_generics"] }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
serde-wasm-bindgen = "0.6"
@ -73,17 +94,16 @@ env_logger = "0.11"
log = "0.4"
bitflags = { version = "2.4", features = ["serde"] }
ctor = "0.2"
convert_case = "0.7"
convert_case = "0.8"
derivative = "2.2"
thiserror = "2"
anyhow = "1.0"
proc-macro2 = { version = "1", features = [ "span-locations" ] }
proc-macro2 = { version = "1", features = ["span-locations"] }
quote = "1.0"
axum = "0.8"
chrono = "0.4"
ron = "0.8"
ron = "0.11"
fastnoise-lite = "1.1"
wgpu = { version = "23", features = [
wgpu = { version = "25.0", features = [
# We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm
"fragile-send-sync-non-atomic-wasm",
"spirv",
@ -111,24 +131,32 @@ web-sys = { version = "=0.3.77", features = [
"HtmlImageElement",
"ImageBitmapRenderingContext",
] }
winit = "0.29"
winit = { version = "0.30", features = ["wayland", "rwh_06"] }
url = "2.5"
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt"] }
vello = { git = "https://github.com/linebender/vello.git", rev = "3275ec8" } # TODO switch back to stable when a release is made
resvg = "0.44"
usvg = "0.44"
vello = { git = "https://github.com/linebender/vello.git", rev = "87cc5bee6d3a34d15017dbbb58634ddc7f33ff9b" } # TODO switch back to stable when a release is made
resvg = "0.45"
usvg = "0.45"
rand = { version = "0.9", default-features = false, features = ["std_rng"] }
rand_chacha = "0.9"
glam = { version = "0.29", default-features = false, features = ["serde", "scalar-math", "debug-glam-assert"] }
glam = { version = "0.29", default-features = false, features = [
"nostd-libm",
"scalar-math",
"bytemuck",
] }
base64 = "0.22"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp"] }
parley = "0.5.0"
skrifa = "0.32.0"
pretty_assertions = "1.4.1"
image = { version = "0.25", default-features = false, features = [
"png",
"jpeg",
"bmp",
] }
parley = "0.5"
skrifa = "0.36"
pretty_assertions = "1.4"
fern = { version = "0.7", features = ["colored"] }
num_enum = "0.7"
num_enum = { version = "0.7", default-features = false }
num-derive = "0.4"
num-traits = { version = "0.2", default-features = false, features = ["i128"] }
num-traits = { version = "0.2", default-features = false, features = ["libm"] }
specta = { version = "2.0.0-rc.22", features = [
"glam",
"derive",
@ -145,21 +173,38 @@ syn = { version = "2.0", default-features = false, features = [
"extra-traits",
"proc-macro",
] }
kurbo = { version = "0.11.0", features = ["serde"] }
petgraph = { version = "0.7.1", default-features = false, features = [
"graphmap",
] }
half = { version = "2.4.1", default-features = false, features = ["bytemuck", "serde"] }
kurbo = { version = "0.11", features = ["serde"] }
lyon_geom = "1.0"
petgraph = { version = "0.7", default-features = false, features = ["graphmap"] }
half = { version = "2.4", default-features = false, features = ["bytemuck"] }
tinyvec = { version = "1", features = ["std"] }
criterion = { version = "0.5", features = ["html_reports"] }
iai-callgrind = { version = "0.12.3" }
ndarray = "0.16.1"
criterion = { version = "0.7", features = ["html_reports"] }
iai-callgrind = { version = "0.16" }
ndarray = "0.16"
strum = { version = "0.27", features = ["derive"] }
dirs = "6.0"
cef = "=139.0.1"
cef-dll-sys = "=139.0.1"
include_dir = "0.7"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing = "0.1"
rfd = "0.15"
open = "5.3"
poly-cool = "0.3"
spin = "0.10"
clap = "4.5"
spirv-std = { git = "https://github.com/rust-gpu/rust-gpu", rev = "c12f216121820580731440ee79ebc7403d6ea04f", features = ["bytemuck"] }
cargo-gpu = { git = "https://github.com/rust-gpu/cargo-gpu", rev = "f969528e87baa17a7d48eecf4a6fcfdcaaf30566" }
[workspace.lints.rust]
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(target_arch, values("spirv"))'] }
[profile.dev]
opt-level = 1
[profile.dev.package]
graphite-editor = { opt-level = 1 }
graphene-core-shaders = { opt-level = 1 }
graphene-core = { opt-level = 1 }
graphene-std = { opt-level = 1 }
interpreted-executor = { opt-level = 1 } # This is a mitigation for https://github.com/rustwasm/wasm-pack/issues/981 which is needed because the node_registry function is too large

View file

@ -1,21 +1,22 @@
# Keep this list in sync with those in `/deny.toml` and `/frontend/vite.config.ts`.
accepted = [
"Apache-2.0 WITH LLVM-exception",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0",
"CC0-1.0",
"CDLA-Permissive-2.0",
"ISC",
"MIT-0",
"MIT",
"MPL-2.0",
"OpenSSL",
"Unicode-3.0",
"Unicode-DFS-2016",
"Zlib",
"NCSA",
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/deny.toml`
"Apache-2.0", # Keep this list in sync with those in `/deny.toml`
"BSD-2-Clause", # Keep this list in sync with those in `/deny.toml`
"BSD-3-Clause", # Keep this list in sync with those in `/deny.toml`
"BSL-1.0", # Keep this list in sync with those in `/deny.toml`
"CC0-1.0", # Keep this list in sync with those in `/deny.toml`
"CDLA-Permissive-2.0", # Keep this list in sync with those in `/deny.toml`
"ISC", # Keep this list in sync with those in `/deny.toml`
"MIT-0", # Keep this list in sync with those in `/deny.toml`
"MIT", # Keep this list in sync with those in `/deny.toml`
"MPL-2.0", # Keep this list in sync with those in `/deny.toml`
"OpenSSL", # Keep this list in sync with those in `/deny.toml`
"Unicode-3.0", # Keep this list in sync with those in `/deny.toml`
"Unicode-DFS-2016", # Keep this list in sync with those in `/deny.toml`
"Zlib", # Keep this list in sync with those in `/deny.toml`
"NCSA", # Keep this list in sync with those in `/deny.toml`
"bzip2-1.0.6", # Keep this list in sync with those in `/deny.toml`
"OFL-1.1", # Keep this list in sync with those in `/deny.toml`
]
workarounds = ["ring"]
ignore-build-dependencies = true

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -28,10 +28,6 @@ targets = [
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
]
# Tauri produces too many nonsense warnings.
exclude = ["tauri", "tauri-build"]
# This section is considered when running `cargo deny check advisories`
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
@ -45,9 +41,7 @@ db-urls = ["https://github.com/rustsec/advisory-db"]
ignore = [
"RUSTSEC-2024-0370", # Unmaintained but still fully functional crate `proc-macro-error`
"RUSTSEC-2024-0388", # Unmaintained but still fully functional crate `derivative`
"RUSTSEC-2025-0007", # Unmaintained but still fully functional crate `ring`
"RUSTSEC-2024-0436", # Unmaintained but still fully functional crate `paste`
"RUSTSEC-2025-0014", # Unmaintained but still fully functional crate `humantime`
]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories
@ -67,24 +61,25 @@ ignore = [
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
#
# Keep this list in sync with those in `/about.toml` and `/frontend/vite.config.ts`.
allow = [
"Apache-2.0 WITH LLVM-exception",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0",
"CC0-1.0",
"CDLA-Permissive-2.0",
"ISC",
"MIT-0",
"MIT",
"MPL-2.0",
"OpenSSL",
"Unicode-3.0",
"Unicode-DFS-2016",
"Zlib",
"NCSA",
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/about.toml`
"Apache-2.0", # Keep this list in sync with those in `/about.toml`
"BSD-2-Clause", # Keep this list in sync with those in `/about.toml`
"BSD-3-Clause", # Keep this list in sync with those in `/about.toml`
"BSL-1.0", # Keep this list in sync with those in `/about.toml`
"CC0-1.0", # Keep this list in sync with those in `/about.toml`
"CDLA-Permissive-2.0", # Keep this list in sync with those in `/about.toml`
"ISC", # Keep this list in sync with those in `/about.toml`
"MIT-0", # Keep this list in sync with those in `/about.toml`
"MIT", # Keep this list in sync with those in `/about.toml`
"MPL-2.0", # Keep this list in sync with those in `/about.toml`
"OpenSSL", # Keep this list in sync with those in `/about.toml`
"Unicode-3.0", # Keep this list in sync with those in `/about.toml`
"Unicode-DFS-2016", # Keep this list in sync with those in `/about.toml`
"Zlib", # Keep this list in sync with those in `/about.toml`
"NCSA", # Keep this list in sync with those in `/about.toml`
"bzip2-1.0.6", # Keep this list in sync with those in `/about.toml`
"OFL-1.1", # Keep this list in sync with those in `/about.toml`
]
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the

67
desktop/Cargo.toml Normal file
View file

@ -0,0 +1,67 @@
[package]
name = "graphite-desktop"
version = "0.1.0"
description = "Graphite Desktop"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[features]
default = ["recommended", "embedded_resources"]
recommended = ["gpu", "accelerated_paint"]
embedded_resources = ["dep:graphite-desktop-embedded-resources"]
gpu = ["graphite-desktop-wrapper/gpu"]
# Hardware acceleration features
accelerated_paint = ["accelerated_paint_dmabuf", "accelerated_paint_d3d11", "accelerated_paint_iosurface"]
accelerated_paint_dmabuf = ["libc", "ash"]
accelerated_paint_d3d11 = ["windows", "ash"]
accelerated_paint_iosurface = ["objc2-io-surface", "objc2-metal", "core-foundation"]
[dependencies]
# Local dependencies
graphite-desktop-wrapper = { path = "wrapper" }
graphite-desktop-embedded-resources = { path = "embedded-resources", optional = true }
wgpu = { workspace = true }
winit = { workspace = true, features = ["serde"] }
thiserror = { workspace = true }
futures = { workspace = true }
cef = { workspace = true }
cef-dll-sys = { 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 }
serde = { workspace = true }
# Hardware acceleration dependencies
ash = { version = "0.38", optional = true }
# Windows-specific dependencies
[target.'cfg(windows)'.dependencies]
windows = { version = "0.58", features = [
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Direct3D12",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_Foundation"
], optional = true }
# macOS-specific dependencies
[target.'cfg(target_os = "macos")'.dependencies]
objc2-io-surface = { version = "0.3", optional = true }
objc2-metal = { version = "0.3", optional = true }
core-foundation = { version = "0.9", optional = true }
# Linux-specific dependencies
[target.'cfg(target_os = "linux")'.dependencies]
libc = { version = "0.2", optional = true }

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024">
<path fill="#ffffff" d="M.0007 659.5456c.0027 11.3936.0161 22.786.0834 34.1805.069 11.996.207 23.99.533 35.983.708 26.133 2.246 52.494 6.892 78.337 4.712 26.218 12.404 50.618 24.528 74.438 11.918 23.413 27.489 44.837 46.066 63.413 18.576 18.577 40.001 34.149 63.413 46.066 23.82 12.125 48.22 19.816 74.438 24.529 25.843 4.645 52.204 6.183 78.337 6.892 11.993.325 23.987.463 35.983.532 14.243.084 28.483.084 42.726.084h278c14.243 0 28.483 0 42.726-.084 11.996-.069 23.99-.207 35.983-.532 26.134-.709 52.494-2.247 78.338-6.892 26.217-4.713 50.617-12.404 74.437-24.529 23.413-11.917 44.837-27.489 63.414-46.066 18.576-18.576 34.148-40 46.065-63.413 12.125-23.82 19.816-48.22 24.529-74.438 4.645-25.843 6.183-52.204 6.892-78.337.325-11.993.463-23.987.532-35.983.084-14.243.084-28.483.084-42.726V373c0-14.243 0-28.483-.084-42.726-.069-11.996-.207-23.99-.532-35.983-.709-26.133-2.247-52.494-6.892-78.337-4.713-26.218-12.404-50.618-24.529-74.438-11.917-23.412-27.489-44.837-46.065-63.413-18.577-18.577-40.001-34.149-63.414-46.066-23.82-12.124-48.22-19.816-74.437-24.528-25.844-4.646-52.204-6.184-78.338-6.893-11.993-.325-23.987-.463-35.983-.532C679.483 0 665.243 0 651 0c0 0-275.1519-.002-286.5455.0007-11.3936.0027-22.7861.0161-34.1805.0834-11.996.069-23.99.207-35.983.532-26.133.709-52.494 2.247-78.337 6.893-26.218 4.712-50.618 12.404-74.438 24.528-23.412 11.917-44.837 27.489-63.413 46.066-18.577 18.576-34.148 40.001-46.066 63.413-12.124 23.82-19.816 48.22-24.528 74.438-4.646 25.843-6.184 52.204-6.892 78.337-.326 11.993-.464 23.987-.533 35.983C0 344.517 0 358.757 0 373"/>
<path fill="#f1decd" d="m789.9503 428.9507-135.005-233.833c-5.642-8.618-15.035-14.043-25.327-14.632h-270.01c-10.292.589-19.685 6.014-25.327 14.632l-135.036 233.833c-4.619 9.207-4.619 20.026 0 29.233l135.036 233.864c5.642 8.618 15.035 14.043 25.327 14.601h270.01c10.292-.558 19.685-5.983 25.327-14.601l135.036-233.864c4.588-9.207 4.588-20.026-.031-29.233Z"/>
<path fill="#3ea8ff" d="m693.8813 243.5087-42.346-73.315h-235.879l200.818 346.301 176.979-100.502-99.572-172.484Z"/>
<path fill="#2180ce" d="m552.5523 325.0697-89.373-154.876h-121.148l-37.355 51.832 106.609 184.605 95.604 165.664 142.383-79.732-96.72-167.493Z"/>
<path fill="#deba92" d="m800.4283 428.0827-166.532 81.282 205.53 312.542-38.998-393.824Z"/>
<path fill="#d49b64" d="m653.4263 499.7547-141.298 81.592 290.098 176.111-148.8-257.703Z"/>
<path fill="#473a3a" d="m870.2712 818.8377-.217-2.139c-2.666-29.109-34.565-376.278-34.658-377.766-.713-8.804-3.317-17.36-7.595-25.079-.093-.279-.217-.527-.341-.775l-.186-.465-.093.093-.062-.031.124-.062-36.58-63.364-102.889-178.25c-11.098-19.189-31.558-31-53.723-31h-278.969c-22.134 0-42.594 11.811-53.692 31l-139.5 241.583c-11.067 19.189-11.067 42.811 0 62l139.5 241.583c11.067 19.189 31.527 31 53.692 31h278.969c11.966-.093 23.653-3.689 33.635-10.292l119.629 85.808c-21.607-6.758-43.958-10.757-66.557-11.873-56.141-2.697-220.72-.868-407.402 14.353-76.601 6.231-112.809 24.428-108.593 27.993 10.819 9.238 23.622 12.896 87.42 12.648 56.792-.186 222.115 5.921 272.056 8.99 35.371 2.201 72.571 8.928 99.324 9.207 28.52-.372 56.947-2.542 85.188-6.51 42.594-5.859 99.076-17.67 112.065-33.387 6.851-6.51 10.354-15.841 9.455-25.265Zm-522.939-603.446c5.177-7.905 13.795-12.896 23.25-13.423h247.969c9.424.527 18.011 5.487 23.219 13.361l103.106 178.591c-15.469 16.12-28.892 34.1-39.959 53.506l-76.7869 8.525-45.787 62.248c-22.351-.124-44.64 2.542-66.34 7.874l-174.003-301.413 5.332-9.269Zm23.25 469.774c-9.455-.527-18.073-5.518-23.25-13.423l-124-214.737c-4.247-8.432-4.247-18.414 0-26.846l82.863-143.468 184.76 320.044.124-.062c2.139 3.844 5.084 7.161 8.649 9.765l95.883 68.758-225.029-.031Zm361.956 21.917-179.49-128.743c19.809-4.309 40.021-6.479 60.295-6.479l45.7871-62.217 76.787-8.525c10.106-17.546 22.103-33.945 35.712-48.949l21.793 219.79c-25.792-2.759-50.406 11.439-60.884 35.123Zm-325.841-87.482c4.619 7.223 2.48 16.802-4.712 21.421-7.223 4.619-16.802 2.48-21.421-4.712-.248-.372-.465-.775-.682-1.178l-108.5-187.891c-4.619-7.223-2.48-16.802 4.712-21.421 7.223-4.619 16.802-2.48 21.421 4.712.248.372.465.775.682 1.178l108.5 187.891Z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -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-editor
Terminal=false
Type=Application
Icon=graphite-icon-color
Categories=Graphics;VectorGraphics;RasterGraphics;
Keywords=graphite;editor;vector;raster;procedural;design;
StartupWMClass=rs.graphite.GraphiteEditor

View file

@ -0,0 +1,15 @@
[package]
name = "graphite-desktop-embedded-resources"
version = "0.1.0"
description = "Graphite Desktop Embedded Resources"
authors = ["Graphite Authors <contact@graphite.rs>"]
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)'] }

View file

@ -0,0 +1,17 @@
const RESOURCES: &str = "../../frontend/dist";
// Check if the directory `RESOURCES` exists and sets the embedded_resources cfg accordingly
// Absolute path of `RESOURCES` available via the `EMBEDDED_RESOURCES` environment variable
fn main() {
let crate_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
println!("cargo:rerun-if-changed={RESOURCES}");
if let Ok(resources) = crate_dir.join(RESOURCES).canonicalize()
&& resources.exists()
{
println!("cargo:rustc-cfg=embedded_resources");
println!("cargo:rustc-env=EMBEDDED_RESOURCES={}", resources.to_string_lossy());
} else {
println!("cargo:warning=Resource directory does not exist. Resources will not be embedded. Did you forget to build the frontend?");
}
}

View file

@ -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;

406
desktop/src/app.rs Normal file
View file

@ -0,0 +1,406 @@
use crate::CustomEvent;
use crate::cef::WindowSize;
use crate::consts::{APP_NAME, CEF_MESSAGE_LOOP_MAX_ITERATIONS};
use crate::persist::PersistentData;
use crate::render::GraphicsState;
use graphite_desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, Platform};
use graphite_desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
use rfd::AsyncFileDialog;
use std::sync::Arc;
use std::sync::mpsc::Sender;
use std::sync::mpsc::SyncSender;
use std::thread;
use std::time::Duration;
use std::time::Instant;
use winit::application::ApplicationHandler;
use winit::dpi::PhysicalSize;
use winit::event::WindowEvent;
use winit::event_loop::ActiveEventLoop;
use winit::event_loop::ControlFlow;
use winit::event_loop::EventLoopProxy;
use winit::window::Window;
use winit::window::WindowId;
use crate::cef;
pub(crate) struct WinitApp {
cef_context: Box<dyn cef::CefContext>,
window: Option<Arc<Window>>,
cef_schedule: Option<Instant>,
window_size_sender: Sender<WindowSize>,
graphics_state: Option<GraphicsState>,
wgpu_context: WgpuContext,
event_loop_proxy: EventLoopProxy<CustomEvent>,
desktop_wrapper: DesktopWrapper,
last_ui_update: Instant,
avg_frame_time: f32,
start_render_sender: SyncSender<()>,
web_communication_initialized: bool,
web_communication_startup_buffer: Vec<Vec<u8>>,
persistent_data: PersistentData,
}
impl WinitApp {
pub(crate) fn new(cef_context: Box<dyn cef::CefContext>, window_size_sender: Sender<WindowSize>, wgpu_context: WgpuContext, event_loop_proxy: EventLoopProxy<CustomEvent>) -> Self {
let rendering_loop_proxy = event_loop_proxy.clone();
let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1);
std::thread::spawn(move || {
loop {
let result = futures::executor::block_on(DesktopWrapper::execute_node_graph());
let _ = rendering_loop_proxy.send_event(CustomEvent::NodeGraphExecutionResult(result));
let _ = start_render_receiver.recv();
}
});
let mut persistent_data = PersistentData::default();
persistent_data.load_from_disk();
Self {
cef_context,
window: None,
cef_schedule: Some(Instant::now()),
graphics_state: None,
window_size_sender,
wgpu_context,
event_loop_proxy,
desktop_wrapper: DesktopWrapper::new(),
last_ui_update: Instant::now(),
avg_frame_time: 0.,
start_render_sender,
web_communication_initialized: false,
web_communication_startup_buffer: Vec::new(),
persistent_data,
}
}
fn handle_desktop_frontend_message(&mut self, message: DesktopFrontendMessage) {
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, context } => {
let event_loop_proxy = self.event_loop_proxy.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 show_dialog = async move { dialog.pick_file().await.map(|f| f.path().to_path_buf()) };
if let Some(path) = futures::executor::block_on(show_dialog)
&& let Ok(content) = std::fs::read(&path)
{
let message = DesktopWrapperMessage::OpenFileDialogResult { path, content, context };
let _ = event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message));
}
});
}
DesktopFrontendMessage::SaveFileDialog {
title,
default_filename,
default_folder,
filters,
context,
} => {
let event_loop_proxy = self.event_loop_proxy.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 };
let _ = event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message));
}
});
}
DesktopFrontendMessage::WriteFile { path, content } => {
if let Err(e) = std::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::UpdateViewportBounds { x, y, width, height } => {
if let Some(graphics_state) = &mut self.graphics_state
&& let Some(window) = &self.window
{
let window_size = window.inner_size();
let viewport_offset_x = x / window_size.width as f32;
let viewport_offset_y = y / window_size.height as f32;
graphics_state.set_viewport_offset([viewport_offset_x, viewport_offset_y]);
let viewport_scale_x = if width != 0.0 { window_size.width as f32 / width } else { 1.0 };
let viewport_scale_y = if height != 0.0 { window_size.height as f32 / height } else { 1.0 };
graphics_state.set_viewport_scale([viewport_scale_x, viewport_scale_y]);
}
}
DesktopFrontendMessage::UpdateOverlays(scene) => {
if let Some(graphics_state) = &mut self.graphics_state {
graphics_state.set_overlays_scene(scene);
}
}
DesktopFrontendMessage::UpdateWindowState { maximized, minimized } => {
if let Some(window) = &self.window {
window.set_maximized(maximized);
window.set_minimized(minimized);
}
}
DesktopFrontendMessage::CloseWindow => {
let _ = self.event_loop_proxy.send_event(CustomEvent::CloseWindow);
}
DesktopFrontendMessage::PersistenceWriteDocument { id, document } => {
self.persistent_data.write_document(id, document);
}
DesktopFrontendMessage::PersistenceDeleteDocument { id } => {
self.persistent_data.delete_document(&id);
}
DesktopFrontendMessage::PersistenceUpdateCurrentDocument { id } => {
self.persistent_data.set_current_document(id);
}
DesktopFrontendMessage::PersistenceUpdateDocumentsList { ids } => {
self.persistent_data.set_document_order(ids);
}
DesktopFrontendMessage::PersistenceLoadCurrentDocument => {
if let Some((id, document)) = self.persistent_data.current_document() {
let message = DesktopWrapperMessage::LoadDocument {
id,
document,
to_front: false,
select_after_open: true,
};
self.dispatch_desktop_wrapper_message(message);
}
}
DesktopFrontendMessage::PersistenceLoadRemainingDocuments => {
for (id, document) in self.persistent_data.documents_before_current().into_iter().rev() {
let message = DesktopWrapperMessage::LoadDocument {
id,
document,
to_front: true,
select_after_open: false,
};
self.dispatch_desktop_wrapper_message(message);
}
for (id, document) in self.persistent_data.documents_after_current() {
let message = DesktopWrapperMessage::LoadDocument {
id,
document,
to_front: false,
select_after_open: false,
};
self.dispatch_desktop_wrapper_message(message);
}
if let Some(id) = self.persistent_data.current_document_id() {
let message = DesktopWrapperMessage::SelectDocument { id };
self.dispatch_desktop_wrapper_message(message);
}
}
DesktopFrontendMessage::PersistenceWritePreferences { preferences } => {
self.persistent_data.write_preferences(preferences);
}
DesktopFrontendMessage::PersistenceLoadPreferences => {
if let Some(preferences) = self.persistent_data.load_preferences() {
let message = DesktopWrapperMessage::LoadPreferences { preferences };
self.dispatch_desktop_wrapper_message(message);
}
}
}
}
fn handle_desktop_frontend_messages(&mut self, messages: Vec<DesktopFrontendMessage>) {
for message in messages {
self.handle_desktop_frontend_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.cef_context.send_web_message(message);
} else {
self.web_communication_startup_buffer.push(message);
}
}
}
impl ApplicationHandler<CustomEvent> for WinitApp {
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
// Set a timeout in case we miss any cef schedule requests
let timeout = Instant::now() + Duration::from_millis(10);
let wait_until = timeout.min(self.cef_schedule.unwrap_or(timeout));
if let Some(schedule) = self.cef_schedule
&& schedule < Instant::now()
{
self.cef_schedule = None;
// Poll cef message loop multiple times to avoid message loop starvation
for _ in 0..CEF_MESSAGE_LOOP_MAX_ITERATIONS {
self.cef_context.work();
}
}
if let Some(window) = &self.window.as_ref() {
window.request_redraw();
}
event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until));
}
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let mut window = Window::default_attributes()
.with_title(APP_NAME)
.with_min_inner_size(winit::dpi::LogicalSize::new(400, 300))
.with_inner_size(winit::dpi::LogicalSize::new(1200, 800));
#[cfg(target_os = "linux")]
{
use crate::consts::APP_ID;
use winit::platform::wayland::ActiveEventLoopExtWayland;
window = if event_loop.is_wayland() {
winit::platform::wayland::WindowAttributesExtWayland::with_name(window, APP_ID, "")
} else {
winit::platform::x11::WindowAttributesExtX11::with_name(window, APP_ID, APP_NAME)
}
}
let window = Arc::new(event_loop.create_window(window).unwrap());
let graphics_state = GraphicsState::new(window.clone(), self.wgpu_context.clone());
self.window = Some(window);
self.graphics_state = Some(graphics_state);
tracing::info!("Winit window created and ready");
self.desktop_wrapper.init(self.wgpu_context.clone());
#[cfg(target_os = "windows")]
let platform = Platform::Windows;
#[cfg(target_os = "macos")]
let platform = Platform::Mac;
#[cfg(target_os = "linux")]
let platform = Platform::Linux;
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::UpdatePlatform(platform));
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: CustomEvent) {
match event {
CustomEvent::WebCommunicationInitialized => {
self.web_communication_initialized = true;
for message in self.web_communication_startup_buffer.drain(..) {
self.cef_context.send_web_message(message);
}
}
CustomEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message),
CustomEvent::NodeGraphExecutionResult(result) => match result {
NodeGraphExecutionResult::HasRun(texture) => {
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::PollNodeGraphEvaluation);
if let Some(texture) = texture
&& let Some(graphics_state) = self.graphics_state.as_mut()
&& let Some(window) = self.window.as_ref()
{
graphics_state.bind_viewport_texture(texture);
window.request_redraw();
}
}
NodeGraphExecutionResult::NotRun => {}
},
CustomEvent::UiUpdate(texture) => {
if let Some(graphics_state) = self.graphics_state.as_mut() {
graphics_state.resize(texture.width(), texture.height());
graphics_state.bind_ui_texture(texture);
let elapsed = self.last_ui_update.elapsed().as_secs_f32();
self.last_ui_update = Instant::now();
if elapsed < 0.5 {
self.avg_frame_time = (self.avg_frame_time * 3. + elapsed) / 4.;
}
}
if let Some(window) = &self.window {
window.request_redraw();
}
}
CustomEvent::ScheduleBrowserWork(instant) => {
if instant <= Instant::now() {
self.cef_context.work();
} else {
self.cef_schedule = Some(instant);
}
}
CustomEvent::CloseWindow => {
// TODO: Implement graceful shutdown
tracing::info!("Exiting main event loop");
event_loop.exit();
}
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
self.cef_context.handle_window_event(&event);
match event {
WindowEvent::CloseRequested => {
let _ = self.event_loop_proxy.send_event(CustomEvent::CloseWindow);
}
WindowEvent::Resized(PhysicalSize { width, height }) => {
let _ = self.window_size_sender.send(WindowSize::new(width as usize, height as usize));
self.cef_context.notify_of_resize();
}
WindowEvent::RedrawRequested => {
let Some(ref mut graphics_state) = self.graphics_state else { return };
// Only rerender once we have a new UI texture to display
if let Some(window) = &self.window {
match graphics_state.render(window.as_ref()) {
Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => {
tracing::warn!("lost surface");
}
Err(wgpu::SurfaceError::OutOfMemory) => {
event_loop.exit();
}
Err(e) => tracing::error!("{:?}", e),
}
let _ = self.start_render_sender.try_send(());
}
}
// Currently not supported on wayland see https://github.com/rust-windowing/winit/issues/1881
WindowEvent::DroppedFile(path) => {
match std::fs::read(&path) {
Ok(content) => {
let message = DesktopWrapperMessage::OpenFile { path, content };
let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message));
}
Err(e) => {
tracing::error!("Failed to read dropped file {}: {}", path.display(), e);
return;
}
};
}
_ => {}
}
// Notify cef of possible input events
self.cef_context.work();
}
}

252
desktop/src/cef.rs Normal file
View file

@ -0,0 +1,252 @@
//! CEF (Chromium Embedded Framework) integration for Graphite Desktop
//!
//! This module provides CEF browser integration with hardware-accelerated texture sharing.
//!
//! # Hardware Acceleration
//!
//! The texture import system supports platform-specific hardware acceleration:
//!
//! - **Linux**: DMA-BUF via Vulkan external memory (`accelerated_paint_dmabuf` feature)
//! - **Windows**: D3D11 shared textures via either Vulkan or D3D12 interop (`accelerated_paint_d3d11` feature)
//! - **macOS**: IOSurface via Metal/Vulkan interop (`accelerated_paint_iosurface` feature)
//!
//!
//! The system gracefully falls back to CPU textures when hardware acceleration is unavailable.
use crate::CustomEvent;
use crate::render::FrameBufferRef;
use graphite_desktop_wrapper::{WgpuContext, deserialize_editor_message};
use std::fs::File;
use std::io::{Cursor, Read};
use std::path::PathBuf;
use std::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex};
use std::time::Instant;
mod consts;
mod context;
mod dirs;
mod input;
mod internal;
mod ipc;
mod platform;
mod utility;
#[cfg(feature = "accelerated_paint")]
mod texture_import;
#[cfg(feature = "accelerated_paint")]
use texture_import::SharedTextureHandle;
pub(crate) use context::{CefContext, CefContextBuilder, InitError};
use winit::event_loop::EventLoopProxy;
pub(crate) trait CefEventHandler: Clone + Send + Sync + 'static {
fn window_size(&self) -> WindowSize;
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>);
#[cfg(feature = "accelerated_paint")]
fn draw_gpu(&self, shared_texture: SharedTextureHandle);
fn load_resource(&self, path: PathBuf) -> Option<Resource>;
/// Schedule the main event loop to run the CEF event loop after the timeout.
/// See [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation.
fn schedule_cef_message_loop_work(&self, scheduled_time: Instant);
fn initialized_web_communication(&self);
fn receive_web_message(&self, message: &[u8]);
}
#[derive(Clone, Copy)]
pub(crate) struct WindowSize {
pub(crate) width: usize,
pub(crate) height: usize,
}
impl WindowSize {
pub(crate) fn new(width: usize, height: usize) -> Self {
Self { width, height }
}
}
#[derive(Clone)]
pub(crate) struct Resource {
pub(crate) reader: ResourceReader,
pub(crate) mimetype: Option<String>,
}
#[expect(dead_code)]
#[derive(Clone)]
pub(crate) enum ResourceReader {
Embedded(Cursor<&'static [u8]>),
File(Arc<File>),
}
impl Read for ResourceReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
ResourceReader::Embedded(cursor) => cursor.read(buf),
ResourceReader::File(file) => file.as_ref().read(buf),
}
}
}
#[derive(Clone)]
pub(crate) struct CefHandler {
window_size_receiver: Arc<Mutex<WindowSizeReceiver>>,
event_loop_proxy: EventLoopProxy<CustomEvent>,
wgpu_context: WgpuContext,
}
struct WindowSizeReceiver {
receiver: Receiver<WindowSize>,
window_size: WindowSize,
}
impl WindowSizeReceiver {
fn new(window_size_receiver: Receiver<WindowSize>) -> Self {
Self {
window_size: WindowSize { width: 1, height: 1 },
receiver: window_size_receiver,
}
}
}
impl CefHandler {
pub(crate) fn new(window_size_receiver: Receiver<WindowSize>, event_loop_proxy: EventLoopProxy<CustomEvent>, wgpu_context: WgpuContext) -> Self {
Self {
window_size_receiver: Arc::new(Mutex::new(WindowSizeReceiver::new(window_size_receiver))),
event_loop_proxy,
wgpu_context,
}
}
}
impl CefEventHandler for CefHandler {
fn window_size(&self) -> WindowSize {
let Ok(mut guard) = self.window_size_receiver.lock() else {
tracing::error!("Failed to lock window_size_receiver");
return WindowSize::new(1, 1);
};
let WindowSizeReceiver { receiver, window_size } = &mut *guard;
for new_window_size in receiver.try_iter() {
*window_size = new_window_size;
}
*window_size
}
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>) {
let width = frame_buffer.width() as u32;
let height = frame_buffer.height() as u32;
let texture = self.wgpu_context.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::Bgra8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
self.wgpu_context.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
frame_buffer.buffer(),
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
}
#[cfg(feature = "accelerated_paint")]
fn draw_gpu(&self, shared_texture: SharedTextureHandle) {
match shared_texture.import_texture(&self.wgpu_context.device) {
Ok(texture) => {
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
}
Err(e) => {
tracing::error!("Failed to import shared texture: {}", e);
}
}
}
fn load_resource(&self, path: PathBuf) -> Option<Resource> {
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,
};
#[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(Cursor::new(file.contents())),
mimetype,
});
}
}
#[cfg(not(feature = "embedded_resources"))]
{
use std::path::Path;
let asset_path_env = std::env::var("GRAPHITE_RESOURCES").ok()?;
let asset_path = Path::new(&asset_path_env);
let file_path = asset_path.join(path.strip_prefix("/").unwrap_or(&path));
if file_path.exists() && file_path.is_file() {
if let Ok(file) = std::fs::File::open(file_path) {
return Some(Resource {
reader: ResourceReader::File(file.into()),
mimetype,
});
}
}
}
None
}
fn schedule_cef_message_loop_work(&self, scheduled_time: std::time::Instant) {
let _ = self.event_loop_proxy.send_event(CustomEvent::ScheduleBrowserWork(scheduled_time));
}
fn initialized_web_communication(&self) {
let _ = self.event_loop_proxy.send_event(CustomEvent::WebCommunicationInitialized);
}
fn receive_web_message(&self, message: &[u8]) {
let Some(desktop_wrapper_message) = deserialize_editor_message(message) else {
tracing::error!("Failed to deserialize web message");
return;
};
let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(desktop_wrapper_message));
}
}

View file

@ -0,0 +1,2 @@
pub(crate) const RESOURCE_SCHEME: &str = "resources";
pub(crate) const RESOURCE_DOMAIN: &str = "resources";

View file

@ -0,0 +1,15 @@
mod multithreaded;
mod singlethreaded;
mod builder;
pub(crate) use builder::{CefContextBuilder, InitError};
pub(crate) trait CefContext {
fn work(&mut self);
fn handle_window_event(&mut self, event: &winit::event::WindowEvent);
fn notify_of_resize(&self);
fn send_web_message(&self, message: Vec<u8>);
}

View file

@ -0,0 +1,176 @@
use cef::args::Args;
use cef::sys::{CEF_API_VERSION_LAST, cef_resultcode_t};
use cef::{
App, BrowserSettings, CefString, Client, DictionaryValue, ImplCommandLine, RenderHandler, RequestContext, Settings, WindowInfo, api_hash, browser_host_create_browser_sync, execute_process,
};
use super::CefContext;
use super::singlethreaded::SingleThreadedCefContext;
use crate::cef::CefEventHandler;
use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
use crate::cef::dirs::{cef_cache_dir, cef_data_dir};
use crate::cef::input::InputState;
use crate::cef::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderHandlerImpl, RenderProcessAppImpl};
pub(crate) struct CefContextBuilder<H: CefEventHandler> {
pub(crate) args: Args,
pub(crate) is_sub_process: bool,
_marker: std::marker::PhantomData<H>,
}
unsafe impl<H: CefEventHandler> Send for CefContextBuilder<H> {}
impl<H: CefEventHandler> CefContextBuilder<H> {
pub(crate) fn new() -> Self {
#[cfg(target_os = "macos")]
let _loader = {
let loader = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), false);
assert!(loader.load());
loader
};
let _ = api_hash(CEF_API_VERSION_LAST, 0);
let args = Args::new();
let cmd = args.as_cmd_line().unwrap();
let switch = CefString::from("type");
let is_sub_process = cmd.has_switch(Some(&switch)) == 1;
Self {
args,
is_sub_process,
_marker: std::marker::PhantomData,
}
}
pub(crate) fn is_sub_process(&self) -> bool {
self.is_sub_process
}
pub(crate) fn execute_sub_process(&self) -> SetupError {
let cmd = self.args.as_cmd_line().unwrap();
let switch = CefString::from("type");
let process_type = CefString::from(&cmd.switch_value(Some(&switch)));
let mut app = RenderProcessAppImpl::<H>::app();
let ret = execute_process(Some(self.args.as_main_args()), Some(&mut app), std::ptr::null_mut());
if ret >= 0 {
SetupError::SubprocessFailed(process_type.to_string())
} else {
SetupError::Subprocess
}
}
#[cfg(target_os = "macos")]
pub(crate) fn initialize(self, event_handler: H) -> Result<impl CefContext, InitError> {
let settings = Settings {
windowless_rendering_enabled: 1,
multi_threaded_message_loop: 0,
external_message_pump: 1,
root_cache_path: cef_data_dir().to_str().map(CefString::from).unwrap(),
cache_path: cef_cache_dir().to_str().map(CefString::from).unwrap(),
..Default::default()
};
self.initialize_inner(&event_handler, settings)?;
create_browser(event_handler)
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn initialize(self, event_handler: H) -> Result<impl CefContext, InitError> {
let settings = Settings {
windowless_rendering_enabled: 1,
multi_threaded_message_loop: 1,
root_cache_path: cef_data_dir().to_str().map(CefString::from).unwrap(),
cache_path: cef_cache_dir().to_str().map(CefString::from).unwrap(),
..Default::default()
};
self.initialize_inner(&event_handler, settings)?;
super::multithreaded::run_on_ui_thread(move || match create_browser(event_handler) {
Ok(context) => {
super::multithreaded::CONTEXT.with(|b| {
*b.borrow_mut() = Some(context);
});
}
Err(e) => {
tracing::error!("Failed to initialize CEF context: {:?}", e);
std::process::exit(1);
}
});
Ok(super::multithreaded::MultiThreadedCefContextProxy)
}
fn initialize_inner(self, event_handler: &H, settings: Settings) -> Result<(), InitError> {
let mut cef_app = App::new(BrowserProcessAppImpl::new(event_handler.clone()));
let result = cef::initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut());
// Attention! Wrapping this in an extra App is necessary, otherwise the program still compiles but segfaults
if result != 1 {
let cef_exit_code = cef::get_exit_code() as u32;
if cef_exit_code == cef_resultcode_t::CEF_RESULT_CODE_NORMAL_EXIT_PROCESS_NOTIFIED as u32 {
return Err(InitError::AlreadyRunning);
}
return Err(InitError::InitializationFailed(cef_exit_code));
}
Ok(())
}
}
fn create_browser<H: CefEventHandler>(event_handler: H) -> Result<SingleThreadedCefContext, InitError> {
let render_handler = RenderHandler::new(RenderHandlerImpl::new(event_handler.clone()));
let mut client = Client::new(BrowserProcessClientImpl::new(render_handler, event_handler.clone()));
let url = CefString::from(format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/").as_str());
let window_info = WindowInfo {
windowless_rendering_enabled: 1,
#[cfg(feature = "accelerated_paint")]
shared_texture_enabled: if crate::cef::platform::should_enable_hardware_acceleration() { 1 } else { 0 },
..Default::default()
};
let settings = BrowserSettings {
windowless_frame_rate: crate::consts::CEF_WINDOWLESS_FRAME_RATE,
background_color: 0x0,
..Default::default()
};
let browser = browser_host_create_browser_sync(
Some(&window_info),
Some(&mut client),
Some(&url),
Some(&settings),
Option::<&mut DictionaryValue>::None,
Option::<&mut RequestContext>::None,
);
if let Some(browser) = browser {
Ok(SingleThreadedCefContext {
browser,
input_state: InputState::default(),
})
} else {
tracing::error!("Failed to create browser");
Err(InitError::BrowserCreationFailed)
}
}
#[derive(thiserror::Error, Debug)]
pub(crate) enum SetupError {
#[error("This is the sub process should exit immediately")]
Subprocess,
#[error("Subprocess returned non zero exit code")]
SubprocessFailed(String),
}
#[derive(thiserror::Error, Debug)]
pub(crate) enum InitError {
#[error("Initialization failed")]
InitializationFailed(u32),
#[error("Browser creation failed")]
BrowserCreationFailed,
#[error("Another instance is already running")]
AlreadyRunning,
}

View file

@ -0,0 +1,67 @@
use cef::sys::cef_thread_id_t;
use cef::{Task, ThreadId, post_task};
use std::cell::RefCell;
use winit::event::WindowEvent;
use crate::cef::internal::task::ClosureTask;
use super::CefContext;
use super::singlethreaded::SingleThreadedCefContext;
thread_local! {
pub(super) static CONTEXT: RefCell<Option<SingleThreadedCefContext>> = const { RefCell::new(None) };
}
pub(super) struct MultiThreadedCefContextProxy;
impl CefContext for MultiThreadedCefContextProxy {
fn work(&mut self) {
// CEF handles its own message loop in multi-threaded mode
}
fn handle_window_event(&mut self, event: &WindowEvent) {
let event_clone = event.clone();
run_on_ui_thread(move || {
CONTEXT.with(|b| {
if let Some(context) = b.borrow_mut().as_mut() {
context.handle_window_event(&event_clone);
}
});
});
}
fn notify_of_resize(&self) {
run_on_ui_thread(move || {
CONTEXT.with(|b| {
if let Some(context) = b.borrow_mut().as_mut() {
context.notify_of_resize();
}
});
});
}
fn send_web_message(&self, message: Vec<u8>) {
run_on_ui_thread(move || {
CONTEXT.with(|b| {
if let Some(context) = b.borrow_mut().as_mut() {
context.send_web_message(message);
}
});
});
}
}
impl Drop for MultiThreadedCefContextProxy {
fn drop(&mut self) {
cef::shutdown();
}
}
pub(super) 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);
post_task(ThreadId::from(cef_thread_id_t::TID_UI), Some(&mut task));
}

View file

@ -0,0 +1,48 @@
use cef::{Browser, ImplBrowser, ImplBrowserHost};
use winit::event::WindowEvent;
use crate::cef::input;
use crate::cef::input::InputState;
use crate::cef::ipc::{MessageType, SendMessage};
use super::CefContext;
pub(super) struct SingleThreadedCefContext {
pub(super) browser: Browser,
pub(super) input_state: InputState,
}
impl CefContext for SingleThreadedCefContext {
fn work(&mut self) {
cef::do_message_loop_work();
}
fn handle_window_event(&mut self, event: &WindowEvent) {
input::handle_window_event(&self.browser, &mut self.input_state, event)
}
fn notify_of_resize(&self) {
self.browser.host().unwrap().was_resized();
}
fn send_web_message(&self, message: Vec<u8>) {
self.send_message(MessageType::SendToJS, &message);
}
}
impl Drop for SingleThreadedCefContext {
fn drop(&mut self) {
cef::shutdown();
}
}
impl SendMessage for SingleThreadedCefContext {
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);
}
}

17
desktop/src/cef/dirs.rs Normal file
View file

@ -0,0 +1,17 @@
use std::path::PathBuf;
use crate::dirs::{ensure_dir_exists, graphite_data_dir};
static CEF_DIR_NAME: &str = "browser";
pub(crate) fn cef_data_dir() -> PathBuf {
let path = graphite_data_dir().join(CEF_DIR_NAME);
ensure_dir_exists(&path);
path
}
pub(crate) fn cef_cache_dir() -> PathBuf {
let path = cef_data_dir().join("cache");
ensure_dir_exists(&path);
path
}

275
desktop/src/cef/input.rs Normal file
View file

@ -0,0 +1,275 @@
use cef::sys::{cef_event_flags_t, cef_key_event_type_t, cef_mouse_button_type_t};
use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, KeyEventType, MouseEvent};
use winit::dpi::PhysicalPosition;
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
mod keymap;
use keymap::{ToDomBits, ToVKBits};
pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputState, event: &WindowEvent) {
match event {
WindowEvent::CursorMoved { position, .. } => {
if let Some(host) = browser.host() {
host.set_focus(1);
}
input_state.update_mouse_position(position);
let mouse_event: MouseEvent = (input_state).into();
browser.host().unwrap().send_mouse_move_event(Some(&mouse_event), 0);
}
WindowEvent::MouseInput { state, button, .. } => {
if let Some(host) = browser.host() {
host.set_focus(1);
let mouse_up = match state {
ElementState::Pressed => 0,
ElementState::Released => 1,
};
let cef_button = match button {
MouseButton::Left => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_LEFT)),
MouseButton::Right => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_RIGHT)),
MouseButton::Middle => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_MIDDLE)),
MouseButton::Forward => None, //TODO: Handle Forward button
MouseButton::Back => None, //TODO: Handle Back button
_ => None,
};
let mut mouse_state = input_state.mouse_state.clone();
match button {
MouseButton::Left => {
mouse_state.left = match state {
ElementState::Pressed => true,
ElementState::Released => false,
}
}
MouseButton::Right => {
mouse_state.right = match state {
ElementState::Pressed => true,
ElementState::Released => false,
}
}
MouseButton::Middle => {
mouse_state.middle = match state {
ElementState::Pressed => true,
ElementState::Released => false,
}
}
_ => {}
};
input_state.update_mouse_state(mouse_state);
let mouse_event: MouseEvent = input_state.into();
if let Some(button) = cef_button {
host.send_mouse_click_event(
Some(&mouse_event),
button,
mouse_up,
1, // click count
);
}
}
}
WindowEvent::MouseWheel { delta, phase: _, device_id: _, .. } => {
if let Some(host) = browser.host() {
let mouse_event = input_state.into();
let line_width = 40; //feels about right, TODO: replace with correct value
let line_height = 30; //feels about right, TODO: replace with correct value
let (delta_x, delta_y) = match delta {
MouseScrollDelta::LineDelta(x, y) => (x * line_width as f32, y * line_height as f32),
MouseScrollDelta::PixelDelta(physical_position) => (physical_position.x as f32, physical_position.y as f32),
};
host.send_mouse_wheel_event(Some(&mouse_event), delta_x as i32, delta_y as i32);
}
}
WindowEvent::ModifiersChanged(modifiers) => {
input_state.update_modifiers(&modifiers.state());
}
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
if let Some(host) = browser.host() {
host.set_focus(1);
let (named_key, character) = match &event.logical_key {
winit::keyboard::Key::Named(named_key) => (
Some(named_key),
match named_key {
winit::keyboard::NamedKey::Space => Some(' '),
winit::keyboard::NamedKey::Enter => Some('\u{000d}'),
_ => None,
},
),
winit::keyboard::Key::Character(str) => {
let char = str.chars().next().unwrap_or('\0');
(None, Some(char))
}
_ => return,
};
let mut key_event = KeyEvent {
size: size_of::<KeyEvent>(),
focus_on_editable_field: 1,
modifiers: input_state.cef_modifiers(&event.location, event.repeat).raw(),
is_system_key: 0,
..Default::default()
};
if let Some(named_key) = named_key {
key_event.native_key_code = named_key.to_dom_bits();
key_event.windows_key_code = named_key.to_vk_bits();
} else if let Some(char) = character {
key_event.native_key_code = char.to_dom_bits();
key_event.windows_key_code = char.to_vk_bits();
}
match event.state {
ElementState::Pressed => {
key_event.type_ = KeyEventType::from(cef_key_event_type_t::KEYEVENT_RAWKEYDOWN);
host.send_key_event(Some(&key_event));
if let Some(char) = character {
let mut buf = [0; 2];
char.encode_utf16(&mut buf);
key_event.character = buf[0];
let mut buf = [0; 2];
char.to_lowercase().next().unwrap().encode_utf16(&mut buf);
key_event.unmodified_character = buf[0];
key_event.type_ = KeyEventType::from(cef_key_event_type_t::KEYEVENT_CHAR);
host.send_key_event(Some(&key_event));
}
}
ElementState::Released => {
key_event.type_ = KeyEventType::from(cef_key_event_type_t::KEYEVENT_KEYUP);
host.send_key_event(Some(&key_event));
}
};
}
}
_ => {}
}
}
#[derive(Default, Clone)]
pub(crate) struct MouseState {
left: bool,
right: bool,
middle: bool,
}
#[derive(Default, Clone, Debug)]
pub(crate) struct MousePosition {
x: usize,
y: usize,
}
impl From<&PhysicalPosition<f64>> for MousePosition {
fn from(position: &PhysicalPosition<f64>) -> Self {
Self {
x: position.x as usize,
y: position.y as usize,
}
}
}
#[derive(Default, Clone)]
pub(crate) struct InputState {
modifiers: winit::keyboard::ModifiersState,
mouse_position: MousePosition,
mouse_state: MouseState,
}
impl InputState {
fn update_modifiers(&mut self, modifiers: &winit::keyboard::ModifiersState) {
self.modifiers = *modifiers;
}
fn update_mouse_position(&mut self, position: &PhysicalPosition<f64>) {
self.mouse_position = position.into();
}
fn update_mouse_state(&mut self, state: MouseState) {
self.mouse_state = state;
}
fn cef_modifiers(&self, location: &winit::keyboard::KeyLocation, is_repeat: bool) -> CefModifiers {
CefModifiers::new(self, location, is_repeat)
}
fn cef_modifiers_mouse_event(&self) -> CefModifiers {
self.cef_modifiers(&winit::keyboard::KeyLocation::Standard, false)
}
}
impl From<InputState> for CefModifiers {
fn from(val: InputState) -> Self {
CefModifiers::new(&val, &winit::keyboard::KeyLocation::Standard, false)
}
}
impl From<&InputState> for MouseEvent {
fn from(val: &InputState) -> Self {
MouseEvent {
x: val.mouse_position.x as i32,
y: val.mouse_position.y as i32,
modifiers: val.cef_modifiers_mouse_event().raw(),
}
}
}
impl From<&mut InputState> for MouseEvent {
fn from(val: &mut InputState) -> Self {
MouseEvent {
x: val.mouse_position.x as i32,
y: val.mouse_position.y as i32,
modifiers: val.cef_modifiers_mouse_event().raw(),
}
}
}
struct CefModifiers(u32);
impl CefModifiers {
fn new(input_state: &InputState, location: &winit::keyboard::KeyLocation, is_repeat: bool) -> Self {
let mut inner = 0;
if input_state.modifiers.shift_key() {
inner |= cef_event_flags_t::EVENTFLAG_SHIFT_DOWN as u32;
}
if input_state.modifiers.control_key() {
inner |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN as u32;
}
if input_state.modifiers.alt_key() {
inner |= cef_event_flags_t::EVENTFLAG_ALT_DOWN as u32;
}
if input_state.modifiers.super_key() {
inner |= cef_event_flags_t::EVENTFLAG_COMMAND_DOWN as u32;
}
if input_state.mouse_state.left {
inner |= cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON as u32;
}
if input_state.mouse_state.right {
inner |= cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON as u32;
}
if input_state.mouse_state.middle {
inner |= cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON as u32;
}
if is_repeat {
inner |= cef_event_flags_t::EVENTFLAG_IS_REPEAT as u32;
}
inner |= match location {
winit::keyboard::KeyLocation::Left => cef_event_flags_t::EVENTFLAG_IS_LEFT as u32,
winit::keyboard::KeyLocation::Right => cef_event_flags_t::EVENTFLAG_IS_RIGHT as u32,
winit::keyboard::KeyLocation::Numpad => cef_event_flags_t::EVENTFLAG_IS_KEY_PAD as u32,
winit::keyboard::KeyLocation::Standard => 0,
};
Self(inner)
}
fn raw(&self) -> u32 {
self.0
}
}

View file

@ -0,0 +1,448 @@
macro_rules! map_enum {
($target:expr, $enum:ident, $( ($code:expr, $variant:ident), )+ ) => {
match $target {
$(
$enum::$variant => $code,
)+
_ => 0,
}
};
}
macro_rules! map {
($target:expr, $( ($code:expr, $variant:literal), )+ ) => {
match $target {
$(
$variant => $code,
)+
_ => 0,
}
};
}
// Windows Virtual keyboard binary representation
pub(crate) trait ToVKBits {
fn to_vk_bits(&self) -> i32;
}
impl ToVKBits for winit::keyboard::NamedKey {
fn to_vk_bits(&self) -> i32 {
use winit::keyboard::NamedKey;
map_enum!(
self,
NamedKey,
(0x12, Alt),
(0xA5, AltGraph),
(0x14, CapsLock),
(0x11, Control),
(0x90, NumLock),
(0x91, ScrollLock),
(0x10, Shift),
(0x5B, Meta),
(0x5C, Super),
(0x0D, Enter),
(0x09, Tab),
(0x20, Space),
(0x28, ArrowDown),
(0x25, ArrowLeft),
(0x27, ArrowRight),
(0x26, ArrowUp),
(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),
)
}
}
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, '?'),
)
}
}
// Chromium dom key binary representation
pub(crate) trait ToDomBits {
fn to_dom_bits(&self) -> i32;
}
impl ToDomBits for winit::keyboard::NamedKey {
fn to_dom_bits(&self) -> i32 {
use winit::keyboard::NamedKey;
map_enum!(
self,
NamedKey,
(0x00, Hyper),
(0x85, Super),
(0x25, Control),
(0x32, Shift),
(0x40, Alt),
(0x00, Fn),
(0x00, FnLock),
(0x24, Enter),
(0x09, Escape),
(0x16, Backspace),
(0x17, Tab),
(0x41, Space),
(0x42, CapsLock),
(0x43, F1),
(0x44, F2),
(0x45, F3),
(0x46, F4),
(0x47, F5),
(0x48, F6),
(0x49, F7),
(0x4a, F8),
(0x4b, F9),
(0x4c, F10),
(0x5f, F11),
(0x60, F12),
(0x6b, PrintScreen),
(0x4e, ScrollLock),
(0x7f, Pause),
(0x76, Insert),
(0x6e, Home),
(0x70, PageUp),
(0x77, Delete),
(0x73, End),
(0x75, PageDown),
(0x72, ArrowRight),
(0x71, ArrowLeft),
(0x74, ArrowDown),
(0x6f, ArrowUp),
(0x4d, NumLock),
(0x87, ContextMenu),
(0x7c, Power),
(0xbf, F13),
(0xc0, F14),
(0xc1, F15),
(0xc2, F16),
(0xc3, F17),
(0xc4, F18),
(0xc5, F19),
(0xc6, F20),
(0xc7, F21),
(0xc8, F22),
(0xc9, F23),
(0xca, F24),
(0x8e, Open),
(0x92, Help),
(0x8c, Select),
(0x89, Again),
(0x8b, Undo),
(0x91, Cut),
(0x8d, Copy),
(0x8f, Paste),
(0x90, Find),
(0x79, AudioVolumeMute),
(0x7b, AudioVolumeUp),
(0x7a, AudioVolumeDown),
(0x65, KanaMode),
(0x64, Convert),
(0x66, NonConvert),
(0x00, Props),
(0xe9, BrightnessUp),
(0xe8, BrightnessDown),
(0xd7, MediaPlay),
(0xd1, MediaPause),
(0xaf, MediaRecord),
(0xd8, MediaFastForward),
(0xb0, MediaRewind),
(0xab, MediaTrackNext),
(0xad, MediaTrackPrevious),
(0xae, MediaStop),
(0xa9, Eject),
(0xac, MediaPlayPause),
(0xa3, LaunchMail),
(0xe1, BrowserSearch),
(0xb4, BrowserHome),
(0xa6, BrowserBack),
(0xa7, BrowserForward),
(0x88, BrowserStop),
(0xb5, BrowserRefresh),
(0xa4, BrowserFavorites),
(0xf0, MailReply),
(0xf1, MailForward),
(0xef, MailSend),
)
}
}
impl ToDomBits for char {
fn to_dom_bits(&self) -> i32 {
map!(
self,
(0x26, 'a'),
(0x38, 'b'),
(0x36, 'c'),
(0x28, 'd'),
(0x1a, 'e'),
(0x29, 'f'),
(0x2a, 'g'),
(0x2b, 'h'),
(0x1f, 'i'),
(0x2c, 'j'),
(0x2d, 'k'),
(0x2e, 'l'),
(0x3a, 'm'),
(0x39, 'n'),
(0x20, 'o'),
(0x21, 'p'),
(0x18, 'q'),
(0x1b, 'r'),
(0x27, 's'),
(0x1c, 't'),
(0x1e, 'u'),
(0x37, 'v'),
(0x19, 'w'),
(0x35, 'x'),
(0x1d, 'y'),
(0x34, 'z'),
(0x26, 'A'),
(0x38, 'B'),
(0x36, 'C'),
(0x28, 'D'),
(0x1a, 'E'),
(0x29, 'F'),
(0x2a, 'G'),
(0x2b, 'H'),
(0x1f, 'I'),
(0x2c, 'J'),
(0x2d, 'K'),
(0x2e, 'L'),
(0x3a, 'M'),
(0x39, 'N'),
(0x20, 'O'),
(0x21, 'P'),
(0x18, 'Q'),
(0x1b, 'R'),
(0x27, 'S'),
(0x1c, 'T'),
(0x1e, 'U'),
(0x37, 'V'),
(0x19, 'W'),
(0x35, 'X'),
(0x1d, 'Y'),
(0x34, 'Z'),
(0x0a, '1'),
(0x0b, '2'),
(0x0c, '3'),
(0x0d, '4'),
(0x0e, '5'),
(0x0f, '6'),
(0x10, '7'),
(0x11, '8'),
(0x12, '9'),
(0x13, '0'),
(0x0a, '!'),
(0x0b, '@'),
(0x0c, '#'),
(0x0d, '$'),
(0x0e, '%'),
(0x0f, '^'),
(0x10, '&'),
(0x11, '*'),
(0x12, '('),
(0x13, ')'),
(0x31, '`'),
(0x31, '~'),
(0x14, '-'),
(0x14, '_'),
(0x15, '='),
(0x15, '+'),
(0x22, '['),
(0x22, '{'),
(0x23, ']'),
(0x23, '}'),
(0x33, '\\'),
(0x33, '|'),
(0x2f, ';'),
(0x2f, ':'),
(0x3b, ','),
(0x3b, '<'),
(0x3c, '.'),
(0x3c, '>'),
(0x30, '\''),
(0x30, '"'),
(0x3d, '/'),
(0x3d, '?'),
)
}
}

View file

@ -0,0 +1,19 @@
mod browser_process_app;
mod browser_process_client;
mod browser_process_handler;
mod browser_process_life_span_handler;
mod render_process_app;
mod render_process_handler;
mod render_process_v8_handler;
mod resource_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_handler::RenderHandlerImpl;
pub(super) use render_process_app::RenderProcessAppImpl;

View file

@ -0,0 +1,101 @@
#[cfg(target_os = "linux")]
use std::env;
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::SchemeHandlerFactoryImpl;
use crate::cef::CefEventHandler;
pub(crate) struct BrowserProcessAppImpl<H: CefEventHandler> {
object: *mut RcImpl<_cef_app_t, Self>,
event_handler: H,
}
impl<H: CefEventHandler + Clone> BrowserProcessAppImpl<H> {
pub(crate) fn new(event_handler: H) -> Self {
Self {
object: std::ptr::null_mut(),
event_handler,
}
}
}
impl<H: CefEventHandler + Clone> ImplApp for BrowserProcessAppImpl<H> {
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(BrowserProcessHandler::new(BrowserProcessHandlerImpl::new(self.event_handler.clone())))
}
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
SchemeHandlerFactoryImpl::<H>::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 {
#[cfg(not(feature = "accelerated_paint"))]
{
// Disable GPU acceleration when accelerated_paint feature is not enabled
cmd.append_switch(Some(&CefString::from("disable-gpu")));
cmd.append_switch(Some(&CefString::from("disable-gpu-compositing")));
}
#[cfg(feature = "accelerated_paint")]
{
// Enable GPU acceleration switches for better performance
cmd.append_switch(Some(&CefString::from("enable-gpu-rasterization")));
cmd.append_switch(Some(&CefString::from("enable-accelerated-2d-canvas")));
}
#[cfg(all(feature = "accelerated_paint", target_os = "linux"))]
{
// Use Vulkan for accelerated painting
cmd.append_switch_with_value(Some(&CefString::from("use-angle")), Some(&CefString::from("vulkan")));
}
// Tell CEF to use Wayland if available
#[cfg(target_os = "linux")]
{
let use_wayland = env::var("WAYLAND_DISPLAY")
.ok()
.filter(|var| !var.is_empty())
.or_else(|| env::var("WAYLAND_SOCKET").ok())
.filter(|var| !var.is_empty())
.is_some();
if use_wayland {
cmd.append_switch_with_value(Some(&CefString::from("ozone-platform")), Some(&CefString::from("wayland")));
}
}
}
}
fn get_raw(&self) -> *mut _cef_app_t {
self.object.cast()
}
}
impl<H: CefEventHandler + Clone> Clone for BrowserProcessAppImpl<H> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
event_handler: self.event_handler.clone(),
}
}
}
impl<H: CefEventHandler> Rc for BrowserProcessAppImpl<H> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<H: CefEventHandler + Clone> WrapApp for BrowserProcessAppImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
self.object = object;
}
}

View file

@ -0,0 +1,90 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_client_t, cef_base_ref_counted_t};
use cef::{ImplClient, LifeSpanHandler, RenderHandler, WrapClient};
use crate::cef::CefEventHandler;
use crate::cef::ipc::{MessageType, UnpackMessage, UnpackedMessage};
use super::browser_process_life_span_handler::BrowserProcessLifeSpanHandlerImpl;
pub(crate) struct BrowserProcessClientImpl<H: CefEventHandler> {
object: *mut RcImpl<_cef_client_t, Self>,
render_handler: RenderHandler,
event_handler: H,
}
impl<H: CefEventHandler> BrowserProcessClientImpl<H> {
pub(crate) fn new(render_handler: RenderHandler, event_handler: H) -> Self {
Self {
object: std::ptr::null_mut(),
render_handler,
event_handler,
}
}
}
impl<H: CefEventHandler> ImplClient for BrowserProcessClientImpl<H> {
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::os::raw::c_int {
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
match unpacked_message {
Some(UnpackedMessage {
message_type: MessageType::Initialized,
data: _,
}) => self.event_handler.initialized_web_communication(),
Some(UnpackedMessage {
message_type: MessageType::SendToNative,
data,
}) => self.event_handler.receive_web_message(data),
_ => {
tracing::error!("Unexpected message type received in browser process");
return 0;
}
}
1
}
fn render_handler(&self) -> Option<RenderHandler> {
Some(self.render_handler.clone())
}
fn life_span_handler(&self) -> Option<cef::LifeSpanHandler> {
Some(LifeSpanHandler::new(BrowserProcessLifeSpanHandlerImpl::new()))
}
fn get_raw(&self) -> *mut _cef_client_t {
self.object.cast()
}
}
impl<H: CefEventHandler> Clone for BrowserProcessClientImpl<H> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
render_handler: self.render_handler.clone(),
event_handler: self.event_handler.clone(),
}
}
}
impl<H: CefEventHandler> Rc for BrowserProcessClientImpl<H> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<H: CefEventHandler> WrapClient for BrowserProcessClientImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_client_t, Self>) {
self.object = object;
}
}

View file

@ -0,0 +1,70 @@
use std::time::{Duration, Instant};
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, SchemeHandlerFactory, WrapBrowserProcessHandler};
use super::scheme_handler_factory::SchemeHandlerFactoryImpl;
use crate::cef::CefEventHandler;
use crate::cef::consts::RESOURCE_SCHEME;
pub(crate) struct BrowserProcessHandlerImpl<H: CefEventHandler> {
object: *mut RcImpl<cef_browser_process_handler_t, Self>,
event_handler: H,
}
impl<H: CefEventHandler> BrowserProcessHandlerImpl<H> {
pub(crate) fn new(event_handler: H) -> Self {
Self {
object: std::ptr::null_mut(),
event_handler,
}
}
}
impl<H: CefEventHandler + Clone> ImplBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
fn on_context_initialized(&self) {
cef::register_scheme_handler_factory(
Some(&CefString::from(RESOURCE_SCHEME)),
None,
Some(&mut SchemeHandlerFactory::new(SchemeHandlerFactoryImpl::new(self.event_handler.clone()))),
);
}
fn on_schedule_message_pump_work(&self, delay_ms: i64) {
self.event_handler.schedule_cef_message_loop_work(Instant::now() + Duration::from_millis(delay_ms as u64));
}
fn on_already_running_app_relaunch(&self, _command_line: Option<&mut cef::CommandLine>, _current_directory: Option<&CefString>) -> ::std::os::raw::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<H: CefEventHandler + Clone> Clone for BrowserProcessHandlerImpl<H> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
event_handler: self.event_handler.clone(),
}
}
}
impl<H: CefEventHandler> Rc for BrowserProcessHandlerImpl<H> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<H: CefEventHandler + Clone> WrapBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_browser_process_handler_t, Self>) {
self.object = object;
}
}

View file

@ -0,0 +1,64 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_life_span_handler_t, cef_base_ref_counted_t};
use cef::{ImplLifeSpanHandler, WrapLifeSpanHandler};
pub(crate) struct BrowserProcessLifeSpanHandlerImpl {
object: *mut RcImpl<_cef_life_span_handler_t, Self>,
}
impl BrowserProcessLifeSpanHandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
}
impl ImplLifeSpanHandler for BrowserProcessLifeSpanHandlerImpl {
fn on_before_popup(
&self,
_browser: Option<&mut cef::Browser>,
_frame: Option<&mut cef::Frame>,
_popup_id: ::std::os::raw::c_int,
target_url: Option<&cef::CefString>,
_target_frame_name: Option<&cef::CefString>,
_target_disposition: cef::WindowOpenDisposition,
_user_gesture: ::std::os::raw::c_int,
_popup_features: Option<&cef::PopupFeatures>,
_window_info: Option<&mut cef::WindowInfo>,
_client: Option<&mut Option<impl cef::ImplClient>>,
_settings: Option<&mut cef::BrowserSettings>,
_extra_info: Option<&mut Option<cef::DictionaryValue>>,
_no_javascript_access: Option<&mut ::std::os::raw::c_int>,
) -> ::std::os::raw::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 BrowserProcessLifeSpanHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl Rc for BrowserProcessLifeSpanHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapLifeSpanHandler for BrowserProcessLifeSpanHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_life_span_handler_t, Self>) {
self.object = object;
}
}

View file

@ -0,0 +1,97 @@
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::cef::CefEventHandler;
use crate::render::FrameBufferRef;
pub(crate) struct RenderHandlerImpl<H: CefEventHandler> {
object: *mut RcImpl<_cef_render_handler_t, Self>,
event_handler: H,
}
impl<H: CefEventHandler> RenderHandlerImpl<H> {
pub(crate) fn new(event_handler: H) -> Self {
Self {
object: std::ptr::null_mut(),
event_handler,
}
}
}
impl<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
if let Some(rect) = rect {
let view = self.event_handler.window_size();
*rect = Rect {
x: 0,
y: 0,
width: view.width as i32,
height: view.height as i32,
};
}
}
fn on_paint(
&self,
_browser: Option<&mut Browser>,
_type_: PaintElementType,
_dirty_rect_count: usize,
_dirty_rects: Option<&Rect>,
buffer: *const u8,
width: ::std::os::raw::c_int,
height: ::std::os::raw::c_int,
) {
let buffer_size = (width * height * 4) as usize;
let buffer_slice = unsafe { std::slice::from_raw_parts(buffer, buffer_size) };
let frame_buffer = FrameBufferRef::new(buffer_slice, width as usize, height as usize).expect("Failed to create frame buffer");
self.event_handler.draw(frame_buffer)
}
#[cfg(feature = "accelerated_paint")]
fn on_accelerated_paint(&self, _browser: Option<&mut Browser>, type_: PaintElementType, _dirty_rect_count: usize, _dirty_rects: Option<&Rect>, info: Option<&cef::AcceleratedPaintInfo>) {
use crate::cef::texture_import::shared_texture_handle::SharedTextureHandle;
if type_ != PaintElementType::default() {
return;
}
let shared_handle = SharedTextureHandle::new(info.unwrap());
if let SharedTextureHandle::Unsupported = shared_handle {
tracing::error!("Platform does not support accelerated painting");
return;
}
self.event_handler.draw_gpu(shared_handle);
}
fn get_raw(&self) -> *mut _cef_render_handler_t {
self.object.cast()
}
}
impl<H: CefEventHandler> Clone for RenderHandlerImpl<H> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
event_handler: self.event_handler.clone(),
}
}
}
impl<H: CefEventHandler> Rc for RenderHandlerImpl<H> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<H: CefEventHandler> WrapRenderHandler for RenderHandlerImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_handler_t, Self>) {
self.object = object;
}
}

View file

@ -0,0 +1,60 @@
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::SchemeHandlerFactoryImpl;
use crate::cef::CefEventHandler;
pub(crate) struct RenderProcessAppImpl<H: CefEventHandler> {
object: *mut RcImpl<_cef_app_t, Self>,
render_process_handler: RenderProcessHandler,
}
impl<H: CefEventHandler> RenderProcessAppImpl<H> {
pub(crate) fn app() -> App {
App::new(Self {
object: std::ptr::null_mut(),
render_process_handler: RenderProcessHandler::new(RenderProcessHandlerImpl::new()),
})
}
}
impl<H: CefEventHandler> ImplApp for RenderProcessAppImpl<H> {
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
SchemeHandlerFactoryImpl::<H>::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<H: CefEventHandler> Clone for RenderProcessAppImpl<H> {
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<H: CefEventHandler> Rc for RenderProcessAppImpl<H> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<H: CefEventHandler> WrapApp for RenderProcessAppImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
self.object = object;
}
}

View file

@ -0,0 +1,131 @@
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::{CefString, ImplFrame, ImplRenderProcessHandler, ImplV8Context, ImplV8Value, V8Handler, V8Propertyattribute, V8Value, WrapRenderProcessHandler, v8_value_create_function};
use crate::cef::ipc::{MessageType, UnpackMessage, UnpackedMessage};
use super::render_process_v8_handler::BrowserProcessV8HandlerImpl;
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::os::raw::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(&CefString::from(property_name)),
Some(&mut value),
cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_READONLY.wrap_result(),
);
if global.value_bykey(Some(&CefString::from(function_name))).is_some() {
frame.execute_java_script(Some(&CefString::from(function_call.as_str())), 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(BrowserProcessV8HandlerImpl::new());
let Some(mut function) = v8_value_create_function(Some(&CefString::from(name)), 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(&CefString::from(name)), 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;
}
}

View file

@ -0,0 +1,86 @@
use cef::{ImplV8Handler, ImplV8Value, V8Value, WrapV8Handler, rc::Rc, v8_context_get_current_context};
use crate::cef::ipc::{MessageType, SendMessage};
pub struct BrowserProcessV8HandlerImpl {
object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>,
}
impl BrowserProcessV8HandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
}
impl ImplV8Handler for BrowserProcessV8HandlerImpl {
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::os::raw::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 BrowserProcessV8HandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl Rc for BrowserProcessV8HandlerImpl {
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 BrowserProcessV8HandlerImpl {
fn wrap_rc(&mut self, object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>) {
self.object = object;
}
}

View file

@ -0,0 +1,108 @@
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::cef::{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 {
let cef_mime = CefString::from(mimetype.as_str());
response.set_mime_type(Some(&cef_mime));
} else {
response.set_mime_type(None);
}
response.set_status(200);
} else {
response.set_status(404);
response.set_mime_type(Some(&CefString::from("text/plain")));
}
}
}
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;
}
}

View file

@ -0,0 +1,86 @@
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::cef::CefEventHandler;
use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
pub(crate) struct SchemeHandlerFactoryImpl<H: CefEventHandler> {
object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>,
event_handler: H,
}
impl<H: CefEventHandler> SchemeHandlerFactoryImpl<H> {
pub(crate) fn new(event_handler: H) -> Self {
Self {
object: std::ptr::null_mut(),
event_handler,
}
}
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(&CefString::from(RESOURCE_SCHEME)), scheme_options);
}
}
}
impl<H: CefEventHandler> ImplSchemeHandlerFactory for SchemeHandlerFactoryImpl<H> {
fn create(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, scheme_name: Option<&CefString>, request: Option<&mut Request>) -> Option<ResourceHandler> {
if let Some(scheme_name) = scheme_name {
if scheme_name.to_string() != RESOURCE_SCHEME {
return None;
}
if let Some(request) = request {
let url = CefString::from(&request.url()).to_string();
let path = url.strip_prefix(&format!("{RESOURCE_SCHEME}://")).unwrap();
let domain = path.split('/').next().unwrap_or("");
let path = path.strip_prefix(domain).unwrap_or("");
let path = path.trim_start_matches('/');
return match domain {
RESOURCE_DOMAIN => {
let resource = self.event_handler.load_resource(path.to_string().into());
Some(ResourceHandler::new(ResourceHandlerImpl::new(resource)))
}
_ => None,
};
}
return None;
}
None
}
fn get_raw(&self) -> *mut _cef_scheme_handler_factory_t {
self.object.cast()
}
}
impl<H: CefEventHandler> Clone for SchemeHandlerFactoryImpl<H> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
event_handler: self.event_handler.clone(),
}
}
}
impl<H: CefEventHandler> Rc for SchemeHandlerFactoryImpl<H> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<H: CefEventHandler> WrapSchemeHandlerFactory for SchemeHandlerFactoryImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>) {
self.object = object;
}
}

View file

@ -0,0 +1,61 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_task_t, cef_base_ref_counted_t};
use cef::{ImplTask, WrapTask};
use std::cell::RefCell;
// Closure-based task wrapper following CEF patterns
pub struct ClosureTask<F> {
pub(crate) object: *mut RcImpl<_cef_task_t, Self>,
pub(crate) closure: RefCell<Option<F>>,
}
impl<F: FnOnce() + Send + 'static> ClosureTask<F> {
pub fn new(closure: F) -> Self {
Self {
object: std::ptr::null_mut(),
closure: RefCell::new(Some(closure)),
}
}
}
impl<F: FnOnce() + Send + 'static> ImplTask for ClosureTask<F> {
fn execute(&self) {
if let Some(closure) = self.closure.borrow_mut().take() {
closure();
}
}
fn get_raw(&self) -> *mut _cef_task_t {
self.object.cast()
}
}
impl<F: FnOnce() + Send + 'static> Clone for ClosureTask<F> {
fn clone(&self) -> Self {
unsafe {
if !self.object.is_null() {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
}
Self {
object: self.object,
closure: RefCell::new(None), // Closure can only be executed once
}
}
}
impl<F: FnOnce() + Send + 'static> Rc for ClosureTask<F> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<F: FnOnce() + Send + 'static> WrapTask for ClosureTask<F> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_task_t, Self>) {
self.object = object;
}
}

112
desktop/src/cef/ipc.rs Normal file
View file

@ -0,0 +1,112 @@
use cef::{CefString, 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(&CefString::from(name.as_str()))) 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 })
}
}

View file

@ -0,0 +1,59 @@
#[cfg(feature = "accelerated_paint")]
pub 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") {
if 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
}
}

View file

@ -0,0 +1,99 @@
//! Common utilities and traits for texture import across platforms
use crate::cef::texture_import::*;
use ash::vk;
use cef::sys::cef_color_type_t;
use wgpu::Device;
/// Common format conversion utilities
pub mod format {
use super::*;
/// Convert CEF color type to wgpu texture format
pub fn cef_to_wgpu(format: cef_color_type_t) -> Result<wgpu::TextureFormat, TextureImportError> {
match format {
cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(wgpu::TextureFormat::Bgra8UnormSrgb),
cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(wgpu::TextureFormat::Rgba8UnormSrgb),
_ => Err(TextureImportError::UnsupportedFormat { format }),
}
}
#[cfg(not(target_os = "macos"))]
/// Convert CEF color type to Vulkan format
pub fn cef_to_vulkan(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 }),
}
}
}
/// Common texture creation utilities
pub mod texture {
use super::*;
/// Create a fallback CPU texture with the given dimensions and format
pub fn create_fallback(device: &Device, width: u32, height: u32, format: cef_color_type_t, label: &str) -> TextureImportResult {
let wgpu_format = format::cef_to_wgpu(format)?;
let texture = device.create_texture(&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,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
tracing::warn!(
"Using fallback CPU texture for CEF rendering ({}x{}, {:?}) - hardware acceleration failed or unavailable. Consider checking GPU driver support.",
width,
height,
format
);
Ok(texture)
}
}
/// Common Vulkan utilities
pub mod vulkan {
use super::*;
/// Find a suitable memory type index for Vulkan allocation
pub 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))
}
/// Check if the wgpu device is using Vulkan backend
#[cfg(not(target_os = "macos"))]
pub fn is_vulkan_backend(device: &Device) -> bool {
use wgpu::hal::api;
let mut is_vulkan = false;
unsafe {
device.as_hal::<api::Vulkan, _, _>(|device| {
is_vulkan = device.is_some();
});
}
is_vulkan
}
/// Check if the wgpu device is using D3D12 backend
#[cfg(target_os = "windows")]
pub fn is_d3d12_backend(device: &Device) -> bool {
use wgpu::hal::api;
let mut is_d3d12 = false;
unsafe {
device.as_hal::<api::Dx12, _, _>(|device| {
is_d3d12 = device.is_some();
});
}
is_d3d12
}
}

View file

@ -0,0 +1,290 @@
//! Windows D3D11 shared texture import implementation
use super::common::{format, texture, vulkan};
use super::{TextureImportError, TextureImportResult, TextureImporter};
use ash::vk;
use cef::{AcceleratedPaintInfo, 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 new(info: &AcceleratedPaintInfo) -> Self {
Self {
handle: info.shared_texture_handle,
format: *info.format.as_ref(),
width: info.extra.coded_size.width as u32,
height: info.extra.coded_size.height as u32,
}
}
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
// Try hardware acceleration first
if self.supports_hardware_acceleration(device) {
// Try D3D12 first (most efficient on Windows)
if vulkan::is_d3d12_backend(device) {
match self.import_via_d3d12(device) {
Ok(texture) => {
tracing::info!("Successfully imported D3D11 shared texture via D3D12");
return Ok(texture);
}
Err(e) => {
tracing::warn!("Failed to import D3D11 via D3D12: {}, trying Vulkan fallback", e);
}
}
}
// Try Vulkan as fallback
if vulkan::is_vulkan_backend(device) {
match self.import_via_vulkan(device) {
Ok(texture) => {
tracing::info!("Successfully imported D3D11 shared texture via Vulkan");
return Ok(texture);
}
Err(e) => {
tracing::warn!("Failed to import D3D11 via Vulkan: {}, falling back to CPU texture", e);
}
}
}
}
// Fallback to CPU texture
texture::create_fallback(device, self.width, self.height, self.format, "CEF D3D11 Texture (fallback)")
}
fn supports_hardware_acceleration(&self, device: &wgpu::Device) -> bool {
// Check if handle is valid
if self.handle.is_null() {
return false;
}
// Check if wgpu is using D3D12 or Vulkan backend
vulkan::is_d3d12_backend(device) || vulkan::is_vulkan_backend(device)
}
}
impl D3D11Importer {
fn import_via_d3d12(&self, device: &wgpu::Device) -> TextureImportResult {
// Get wgpu's D3D12 device
use wgpu::hal::api;
let hal_texture = unsafe {
device.as_hal::<api::Dx12, _, _>(|device| {
let Some(device) = device else {
return Err(TextureImportError::HardwareUnavailable {
reason: "Device is not using D3D12 backend".to_string(),
});
};
// Import D3D11 shared handle directly into D3D12 resource
let d3d12_resource = self.import_d3d11_handle_to_d3d12(device)?;
// Wrap D3D12 resource in wgpu-hal texture
let hal_texture = <api::Dx12 as wgpu::hal::Api>::Device::texture_from_raw(
d3d12_resource,
format::cef_to_wgpu(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(hal_texture)
})
}?;
// Import hal texture into wgpu
let texture = unsafe {
device.create_texture_from_hal::<api::Dx12>(
hal_texture,
&wgpu::TextureDescriptor {
label: Some("CEF D3D11→D3D12 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: format::cef_to_wgpu(self.format)?,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
)
};
Ok(texture)
}
fn import_via_vulkan(&self, device: &wgpu::Device) -> TextureImportResult {
// Get wgpu's Vulkan instance and device
use wgpu::{TextureUses, wgc::api::Vulkan};
let hal_texture = unsafe {
device.as_hal::<api::Vulkan, _, _>(|device| {
let Some(device) = device else {
return Err(TextureImportError::HardwareUnavailable {
reason: "Device is not using Vulkan backend".to_string(),
});
};
// Import D3D11 shared handle into Vulkan
let vk_image = self.import_d3d11_handle_to_vulkan(device)?;
// Wrap VkImage in wgpu-hal texture
let hal_texture = <api::Vulkan as wgpu::hal::Api>::Device::texture_from_raw(
vk_image,
&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: format::cef_to_wgpu(self.format)?,
usage: TextureUses::COPY_DST | TextureUses::RESOURCE,
memory_flags: wgpu::hal::MemoryFlags::empty(),
view_formats: vec![],
},
None, // drop_callback
);
Ok(hal_texture)
})
}?;
// Import hal texture into wgpu
let texture = unsafe {
device.create_texture_from_hal::<Vulkan>(
hal_texture,
&wgpu::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: format::cef_to_wgpu(self.format)?,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
)
};
Ok(texture)
}
fn import_d3d11_handle_to_vulkan(&self, hal_device: &<api::Vulkan as wgpu::hal::Api>::Device) -> Result<vk::Image, TextureImportError> {
// Get raw Vulkan handles
let device = hal_device.raw_device();
let _instance = hal_device.shared_instance().raw_instance();
// Validate dimensions
if self.width == 0 || self.height == 0 {
return Err(TextureImportError::InvalidHandle("Invalid D3D11 texture dimensions".to_string()));
}
// Create external memory image info
let mut external_memory_info = vk::ExternalMemoryImageCreateInfo::default().handle_types(vk::ExternalMemoryHandleTypeFlags::D3D11_TEXTURE);
// Create image create info
let image_create_info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(format::cef_to_vulkan(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::OPTIMAL)
.usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::COLOR_ATTACHMENT)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.push_next(&mut external_memory_info);
// Create the image
let image = unsafe {
device.create_image(&image_create_info, None).map_err(|e| TextureImportError::VulkanError {
operation: format!("Failed to create Vulkan image: {:?}", e),
})?
};
// Get memory requirements
let memory_requirements = unsafe { device.get_image_memory_requirements(image) };
// Import D3D11 handle
let mut import_memory_win32 = vk::ImportMemoryWin32HandleInfoKHR::default()
.handle_type(vk::ExternalMemoryHandleTypeFlags::D3D11_TEXTURE)
.handle(self.handle as isize);
// Find a suitable memory type
let memory_properties = unsafe { hal_device.shared_instance().raw_instance().get_physical_device_memory_properties(hal_device.raw_physical_device()) };
let memory_type_index =
vulkan::find_memory_type_index(memory_requirements.memory_type_bits, vk::MemoryPropertyFlags::empty(), &memory_properties).ok_or_else(|| TextureImportError::VulkanError {
operation: "Failed to find suitable memory type for D3D11 texture".to_string(),
})?;
let allocate_info = vk::MemoryAllocateInfo::default()
.allocation_size(memory_requirements.size)
.memory_type_index(memory_type_index)
.push_next(&mut import_memory_win32);
let device_memory = unsafe {
device.allocate_memory(&allocate_info, None).map_err(|e| TextureImportError::VulkanError {
operation: format!("Failed to allocate memory for D3D11 texture: {:?}", e),
})?
};
// Bind memory to image
unsafe {
device.bind_image_memory(image, device_memory, 0).map_err(|e| TextureImportError::VulkanError {
operation: format!("Failed to bind memory to image: {:?}", e),
})?;
}
Ok(image)
}
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::*;
use windows::core::*;
// Get D3D12 device from wgpu-hal
let d3d12_device = hal_device.raw_device();
// Validate dimensions
if self.width == 0 || self.height == 0 {
return Err(TextureImportError::InvalidHandle("Invalid D3D11 texture dimensions".to_string()));
}
// Open D3D11 shared handle on D3D12 device
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()))
}
}
}

View file

@ -0,0 +1,273 @@
//! Linux DMA-BUF texture import implementation
use super::common::{format, texture, vulkan};
use super::{TextureImportError, TextureImportResult, TextureImporter};
use ash::vk;
use cef::{AcceleratedPaintInfo, sys::cef_color_type_t};
use wgpu::hal::api;
pub(crate) struct DmaBufImporter {
fds: Vec<std::os::fd::RawFd>,
format: cef_color_type_t,
modifier: u64,
width: u32,
height: u32,
strides: Vec<u32>,
offsets: Vec<u32>,
}
impl TextureImporter for DmaBufImporter {
fn new(info: &AcceleratedPaintInfo) -> Self {
Self {
fds: extract_fds_from_info(info),
format: *info.format.as_ref(),
modifier: info.modifier,
width: info.extra.coded_size.width as u32,
height: info.extra.coded_size.height as u32,
strides: extract_strides_from_info(info),
offsets: extract_offsets_from_info(info),
}
}
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
// Try hardware acceleration first
if self.supports_hardware_acceleration(device) {
match self.import_via_vulkan(device) {
Ok(texture) => {
tracing::info!("Successfully imported DMA-BUF texture via Vulkan");
return Ok(texture);
}
Err(e) => {
tracing::warn!("Failed to import DMA-BUF via Vulkan: {}, falling back to CPU texture", e);
}
}
}
// Fallback to CPU texture
texture::create_fallback(device, self.width, self.height, self.format, "CEF DMA-BUF Texture (fallback)")
}
fn supports_hardware_acceleration(&self, device: &wgpu::Device) -> bool {
// Check if we have valid file descriptors
if self.fds.is_empty() {
return false;
}
for &fd in &self.fds {
if fd < 0 {
return false;
}
// Check if file descriptor is valid
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if flags == -1 {
return false;
}
}
// Check if wgpu is using Vulkan backend
vulkan::is_vulkan_backend(device)
}
}
impl DmaBufImporter {
fn import_via_vulkan(&self, device: &wgpu::Device) -> TextureImportResult {
// Get wgpu's Vulkan instance and device
use wgpu::{TextureUses, wgc::api::Vulkan};
let hal_texture = unsafe {
device.as_hal::<api::Vulkan, _, _>(|device| {
let Some(device) = device else {
return Err(TextureImportError::HardwareUnavailable {
reason: "Device is not using Vulkan backend".to_string(),
});
};
// Create VkImage from DMA-BUF using external memory
let vk_image = self.create_vulkan_image_from_dmabuf(device)?;
// Wrap VkImage in wgpu-hal texture
let hal_texture = <api::Vulkan as wgpu::hal::Api>::Device::texture_from_raw(
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: format::cef_to_wgpu(self.format)?,
usage: TextureUses::COPY_DST | TextureUses::RESOURCE,
memory_flags: wgpu::hal::MemoryFlags::empty(),
view_formats: vec![],
},
None, // drop_callback
);
Ok(hal_texture)
})
}?;
// Import hal texture into wgpu
let texture = unsafe {
device.create_texture_from_hal::<Vulkan>(
hal_texture,
&wgpu::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: format::cef_to_wgpu(self.format)?,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
)
};
Ok(texture)
}
fn create_vulkan_image_from_dmabuf(&self, hal_device: &<api::Vulkan as wgpu::hal::Api>::Device) -> Result<vk::Image, TextureImportError> {
// Get raw Vulkan handles
let device = hal_device.raw_device();
let _instance = hal_device.shared_instance().raw_instance();
// Validate dimensions
if self.width == 0 || self.height == 0 {
return Err(TextureImportError::InvalidHandle("Invalid DMA-BUF dimensions".to_string()));
}
// Create external memory image
let image_create_info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(format::cef_to_vulkan(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)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
// Set up DRM format modifier
let plane_layouts = self.create_subresource_layouts()?;
let mut drm_format_modifier = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default()
.drm_format_modifier(self.modifier)
.plane_layouts(&plane_layouts);
let image_create_info = image_create_info.push_next(&mut drm_format_modifier);
// Create the image
let image = unsafe {
device.create_image(&image_create_info, None).map_err(|e| TextureImportError::VulkanError {
operation: format!("Failed to create Vulkan image: {e:?}"),
})?
};
// Import memory from DMA-BUF
let memory_requirements = unsafe { device.get_image_memory_requirements(image) };
// Duplicate the file descriptor to avoid ownership issues
let dup_fd = unsafe { libc::dup(self.fds[0]) };
if dup_fd == -1 {
return Err(TextureImportError::PlatformError {
message: "Failed to duplicate DMA-BUF file descriptor".to_string(),
});
}
let mut import_memory_fd = vk::ImportMemoryFdInfoKHR::default().handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT).fd(dup_fd);
// Find a suitable memory type
let memory_properties = unsafe { hal_device.shared_instance().raw_instance().get_physical_device_memory_properties(hal_device.raw_physical_device()) };
let memory_type_index =
vulkan::find_memory_type_index(memory_requirements.memory_type_bits, vk::MemoryPropertyFlags::empty(), &memory_properties).ok_or_else(|| 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 = unsafe {
device.allocate_memory(&allocate_info, None).map_err(|e| TextureImportError::VulkanError {
operation: format!("Failed to allocate memory for DMA-BUF: {e:?}"),
})?
};
// Bind memory to image
unsafe {
device.bind_image_memory(image, device_memory, 0).map_err(|e| TextureImportError::VulkanError {
operation: format!("Failed to bind memory to image: {e:?}"),
})?;
}
Ok(image)
}
fn create_subresource_layouts(&self) -> Result<Vec<vk::SubresourceLayout>, TextureImportError> {
let mut layouts = Vec::new();
for i in 0..self.fds.len() {
layouts.push(vk::SubresourceLayout {
offset: self.offsets.get(i).copied().unwrap_or(0) as u64,
size: 0, // Will be calculated by driver
row_pitch: self.strides.get(i).copied().unwrap_or(0) as u64,
array_pitch: 0,
depth_pitch: 0,
});
}
Ok(layouts)
}
}
fn extract_fds_from_info(info: &cef::AcceleratedPaintInfo) -> Vec<std::os::fd::RawFd> {
let plane_count = info.plane_count as usize;
let mut fds = Vec::with_capacity(plane_count);
for i in 0..plane_count {
if let Some(plane) = info.planes.get(i) {
fds.push(plane.fd);
}
}
fds
}
fn extract_strides_from_info(info: &cef::AcceleratedPaintInfo) -> Vec<u32> {
let plane_count = info.plane_count as usize;
let mut strides = Vec::with_capacity(plane_count);
for i in 0..plane_count {
if let Some(plane) = info.planes.get(i) {
strides.push(plane.stride);
}
}
strides
}
fn extract_offsets_from_info(info: &cef::AcceleratedPaintInfo) -> Vec<u32> {
let plane_count = info.plane_count as usize;
let mut offsets = Vec::with_capacity(plane_count);
for i in 0..plane_count {
if let Some(plane) = info.planes.get(i) {
offsets.push(plane.offset as u32);
}
}
offsets
}

View file

@ -0,0 +1,182 @@
//! macOS IOSurface texture import implementation
use super::common::{format, texture};
use super::{TextureImportError, TextureImportResult, TextureImporter};
use cef::{AcceleratedPaintInfo, sys::cef_color_type_t};
use core_foundation::base::{CFType, TCFType};
use objc2_io_surface::{IOSurface, IOSurfaceRef};
use objc2_metal::{MTLDevice, MTLPixelFormat, MTLTexture, MTLTextureDescriptor, MTLTextureType, MTLTextureUsage};
use std::os::raw::c_void;
use wgpu::hal::api;
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 new(info: &AcceleratedPaintInfo) -> Self {
Self {
handle: info.shared_texture_handle,
format: *info.format.as_ref(),
width: info.extra.coded_size.width as u32,
height: info.extra.coded_size.height as u32,
}
}
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
// Try hardware acceleration first
if self.supports_hardware_acceleration(device) {
match self.import_via_metal(device) {
Ok(texture) => {
tracing::trace!("Successfully imported IOSurface texture via Metal");
return Ok(texture);
}
Err(e) => {
tracing::warn!("Failed to import IOSurface via Metal: {}, falling back to CPU texture", e);
}
}
}
// Fallback to CPU texture
texture::create_fallback(device, self.width, self.height, self.format, "CEF IOSurface Texture (fallback)")
}
fn supports_hardware_acceleration(&self, device: &wgpu::Device) -> bool {
// Check if handle is valid
if self.handle.is_null() {
return false;
}
// Check if wgpu is using Metal backend
self.is_metal_backend(device)
}
}
impl IOSurfaceImporter {
fn import_via_metal(&self, device: &wgpu::Device) -> TextureImportResult {
// Get wgpu's Metal device
use wgpu::{hal::Api, wgc::api::Metal};
let hal_texture = unsafe {
device.as_hal::<api::Metal, _, _>(|device| {
let Some(device) = device else {
return Err(TextureImportError::HardwareUnavailable {
reason: "Device is not using Metal backend".to_string(),
});
};
// Import IOSurface handle into Metal texture
let metal_texture = self.import_iosurface_to_metal(device)?;
// Wrap Metal texture in wgpu-hal texture
let hal_texture = <api::Metal as wgpu::hal::Api>::Device::texture_from_raw(
metal_texture,
&wgpu::hal::TextureDescriptor {
label: Some("CEF IOSurface 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: format::cef_to_wgpu(self.format)?,
usage: wgpu::hal::TextureUses::RESOURCE,
memory_flags: wgpu::hal::MemoryFlags::empty(),
view_formats: vec![],
},
None, // drop_callback
);
Ok(hal_texture)
})
}?;
// Import hal texture into wgpu
let texture = unsafe {
device.create_texture_from_hal::<Metal>(
hal_texture,
&wgpu::TextureDescriptor {
label: Some("CEF IOSurface 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: format::cef_to_wgpu(self.format)?,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
)
};
Ok(texture)
}
fn import_iosurface_to_metal(&self, hal_device: &<api::Metal as wgpu::hal::Api>::Device) -> Result<<api::Metal as wgpu::hal::Api>::Texture, TextureImportError> {
// Validate dimensions
if self.width == 0 || self.height == 0 {
return Err(TextureImportError::InvalidHandle("Invalid IOSurface texture dimensions".to_string()));
}
// Convert handle to IOSurface
let iosurface = unsafe {
let cf_type = CFType::wrap_under_get_rule(self.handle as IOSurfaceRef);
IOSurface::from(cf_type)
};
// Get the Metal device from wgpu-hal
let metal_device = hal_device.raw_device();
// Convert CEF format to Metal pixel format
let metal_format = self.cef_to_metal_format(self.format)?;
// Create Metal texture descriptor
let texture_descriptor = MTLTextureDescriptor::new();
texture_descriptor.setTextureType(MTLTextureType::Type2D);
texture_descriptor.setPixelFormat(metal_format);
texture_descriptor.setWidth(self.width as usize);
texture_descriptor.setHeight(self.height as usize);
texture_descriptor.setDepth(1);
texture_descriptor.setMipmapLevelCount(1);
texture_descriptor.setSampleCount(1);
texture_descriptor.setUsage(MTLTextureUsage::ShaderRead);
// Create Metal texture from IOSurface
let metal_texture = unsafe { metal_device.newTextureWithDescriptor_iosurface_plane(&texture_descriptor, &iosurface, 0) };
let Some(metal_texture) = metal_texture else {
return Err(TextureImportError::PlatformError {
message: "Failed to create Metal texture from IOSurface".to_string(),
});
};
tracing::trace!("Successfully created Metal texture from IOSurface");
Ok(metal_texture)
}
fn cef_to_metal_format(&self, format: cef_color_type_t) -> Result<MTLPixelFormat, TextureImportError> {
match format {
cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(MTLPixelFormat::BGRA8Unorm_sRGB),
cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(MTLPixelFormat::RGBA8Unorm_sRGB),
_ => Err(TextureImportError::UnsupportedFormat { format }),
}
}
fn is_metal_backend(&self, device: &wgpu::Device) -> bool {
use wgpu::hal::api;
let mut is_metal = false;
unsafe {
device.as_hal::<api::Metal, _, _>(|device| {
is_metal = device.is_some();
});
}
is_metal
}
}

View file

@ -0,0 +1,75 @@
//! Unified texture import system for CEF hardware acceleration
//!
//! This module provides a platform-agnostic interface for importing shared textures
//! from CEF into wgpu, with automatic fallback to CPU textures when hardware
//! acceleration is not available.
//!
//! # Supported Platforms
//!
//! - **Linux**: DMA-BUF via Vulkan external memory
//! - **Windows**: D3D11 shared textures via Vulkan interop
//! - **macOS**: IOSurface via Metal native API
//!
//! # Usage
//!
//! ```no_run
//! // Import texture with automatic platform detection
//! let texture = shared_handle.import_texture(&device)?;
//! ```
//!
//! # Features
//!
//! - `accelerated_paint` - Base feature for texture import
//! - `accelerated_paint_dmabuf` - Linux DMA-BUF support
//! - `accelerated_paint_d3d11` - Windows D3D11 support
//! - `accelerated_paint_iosurface` - macOS IOSurface support
pub(crate) mod common;
pub(crate) mod shared_texture_handle;
pub(crate) use shared_texture_handle::SharedTextureHandle;
#[cfg(target_os = "linux")]
pub(crate) mod dmabuf;
#[cfg(target_os = "windows")]
pub(crate) mod d3d11;
#[cfg(target_os = "macos")]
pub(crate) mod iosurface;
/// Result type for texture import operations
pub type TextureImportResult = Result<wgpu::Texture, TextureImportError>;
/// Errors that can occur during texture import
#[derive(Debug, thiserror::Error)]
pub enum TextureImportError {
#[error("Invalid texture handle: {0}")]
InvalidHandle(String),
#[error("Unsupported texture format: {format:?}")]
UnsupportedFormat { format: cef::sys::cef_color_type_t },
#[error("Hardware acceleration not available: {reason}")]
HardwareUnavailable { reason: String },
#[error("Vulkan operation failed: {operation}")]
VulkanError { operation: String },
#[error("Platform-specific error: {message}")]
PlatformError { message: String },
#[error("Unsupported platform for texture import")]
UnsupportedPlatform,
}
/// Trait for platform-specific texture importers
pub trait TextureImporter {
fn new(info: &cef::AcceleratedPaintInfo) -> Self;
/// Import the texture into wgpu, with automatic fallback to CPU texture
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult;
/// Check if hardware acceleration is available for this texture
fn supports_hardware_acceleration(&self, device: &wgpu::Device) -> bool;
}

View file

@ -0,0 +1,45 @@
use cef::AcceleratedPaintInfo;
use super::{TextureImportError, TextureImportResult, TextureImporter};
pub(crate) enum SharedTextureHandle {
#[cfg(target_os = "linux")]
DmaBuf(super::dmabuf::DmaBufImporter),
#[cfg(target_os = "windows")]
D3D11(super::d3d11::D3D11Importer),
#[cfg(target_os = "macos")]
IOSurface(super::iosurface::IOSurfaceImporter),
Unsupported,
}
impl SharedTextureHandle {
pub(crate) fn new(info: &AcceleratedPaintInfo) -> Self {
// Extract DMA-BUF information
#[cfg(target_os = "linux")]
return Self::DmaBuf(super::dmabuf::DmaBufImporter::new(info));
// Extract D3D11 shared handle with texture metadata
#[cfg(target_os = "windows")]
return Self::D3D11(super::d3d11::D3D11Importer::new(info));
// Extract IOSurface handle with texture metadata
#[cfg(target_os = "macos")]
return Self::IOSurface(super::iosurface::IOSurfaceImporter::new(info));
#[allow(unreachable_code)]
Self::Unsupported
}
/// Import a texture using the appropriate platform-specific importer
pub(crate) fn import_texture(self, device: &wgpu::Device) -> TextureImportResult {
match self {
#[cfg(target_os = "linux")]
SharedTextureHandle::DmaBuf(importer) => importer.import_to_wgpu(device),
#[cfg(target_os = "windows")]
SharedTextureHandle::D3D11(importer) => importer.import_to_wgpu(device),
#[cfg(target_os = "macos")]
SharedTextureHandle::IOSurface(importer) => importer.import_to_wgpu(device),
SharedTextureHandle::Unsupported => Err(TextureImportError::UnsupportedPlatform),
}
}
}

View file

@ -0,0 +1,6 @@
pub unsafe fn pointer_to_string(pointer: *mut cef::sys::_cef_string_utf16_t) -> String {
let str = unsafe { (*pointer).str_ };
let len = unsafe { (*pointer).length };
let slice = unsafe { std::slice::from_raw_parts(str, len) };
String::from_utf16(slice).unwrap()
}

11
desktop/src/consts.rs Normal file
View file

@ -0,0 +1,11 @@
pub(crate) static APP_NAME: &str = "Graphite";
pub(crate) static APP_ID: &str = "rs.graphite.GraphiteEditor";
pub(crate) static APP_DIRECTORY_NAME: &str = "graphite-editor";
pub(crate) static APP_STATE_FILE_NAME: &str = "state.ron";
pub(crate) static APP_PREFERENCES_FILE_NAME: &str = "preferences.ron";
pub(crate) static APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents";
// CEF configuration constants
pub(crate) const CEF_WINDOWLESS_FRAME_RATE: i32 = 60;
pub(crate) const CEF_MESSAGE_LOOP_MAX_ITERATIONS: usize = 10;

22
desktop/src/dirs.rs Normal file
View file

@ -0,0 +1,22 @@
use std::fs::create_dir_all;
use std::path::PathBuf;
use crate::consts::{APP_DIRECTORY_NAME, APP_DOCUMENTS_DIRECTORY_NAME};
pub(crate) fn ensure_dir_exists(path: &PathBuf) {
if !path.exists() {
create_dir_all(path).unwrap_or_else(|_| panic!("Failed to create directory at {path:?}"));
}
}
pub(crate) fn graphite_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 graphite_autosave_documents_dir() -> PathBuf {
let path = graphite_data_dir().join(APP_DOCUMENTS_DIRECTORY_NAME);
ensure_dir_exists(&path);
path
}

73
desktop/src/main.rs Normal file
View file

@ -0,0 +1,73 @@
use std::process::exit;
use std::time::Instant;
use cef::CefHandler;
use tracing_subscriber::EnvFilter;
use winit::event_loop::EventLoop;
pub(crate) mod consts;
mod cef;
mod render;
mod app;
use app::WinitApp;
mod dirs;
mod persist;
use graphite_desktop_wrapper::messages::DesktopWrapperMessage;
use graphite_desktop_wrapper::{NodeGraphExecutionResult, WgpuContext};
pub(crate) enum CustomEvent {
UiUpdate(wgpu::Texture),
ScheduleBrowserWork(Instant),
WebCommunicationInitialized,
DesktopWrapperMessage(DesktopWrapperMessage),
NodeGraphExecutionResult(NodeGraphExecutionResult),
CloseWindow,
}
fn main() {
tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init();
let cef_context_builder = cef::CefContextBuilder::<CefHandler>::new();
if cef_context_builder.is_sub_process() {
// We are in a CEF subprocess
// This will block until the CEF subprocess quits
let error = cef_context_builder.execute_sub_process();
tracing::error!("Cef subprocess failed with error: {error}");
return;
}
let wgpu_context = futures::executor::block_on(WgpuContext::new()).unwrap();
let event_loop = EventLoop::<CustomEvent>::with_user_event().build().unwrap();
let (window_size_sender, window_size_receiver) = std::sync::mpsc::channel();
let cef_handler = cef::CefHandler::new(window_size_receiver, event_loop.create_proxy(), wgpu_context.clone());
let cef_context = match cef_context_builder.initialize(cef_handler) {
Ok(c) => c,
Err(cef::InitError::AlreadyRunning) => {
tracing::error!("Another instance is already running, Exiting.");
exit(0);
}
Err(cef::InitError::InitializationFailed(code)) => {
tracing::error!("Cef initialization failed with code: {code}");
exit(1);
}
Err(cef::InitError::BrowserCreationFailed) => {
tracing::error!("Failed to create CEF browser");
exit(1);
}
};
tracing::info!("CEF initialized successfully");
let mut winit_app = WinitApp::new(Box::new(cef_context), window_size_sender, wgpu_context, event_loop.create_proxy());
event_loop.run_app(&mut winit_app).unwrap();
}

215
desktop/src/persist.rs Normal file
View file

@ -0,0 +1,215 @@
use graphite_desktop_wrapper::messages::{Document, DocumentId, Preferences};
#[derive(Default, serde::Serialize, serde::Deserialize)]
pub(crate) struct PersistentData {
documents: DocumentStore,
current_document: Option<DocumentId>,
#[serde(skip)]
document_order: Option<Vec<DocumentId>>,
preferences: Option<Preferences>,
}
impl PersistentData {
pub(crate) fn write_document(&mut self, id: DocumentId, document: Document) {
self.documents.write(id, document);
if let Some(order) = &self.document_order {
self.documents.force_order(order.clone());
}
self.flush();
}
pub(crate) fn delete_document(&mut self, id: &DocumentId) {
if Some(*id) == self.current_document {
self.current_document = None;
}
self.documents.delete(id);
self.flush();
}
pub(crate) fn current_document_id(&self) -> Option<DocumentId> {
match self.current_document {
Some(id) => Some(id),
None => Some(*self.documents.document_ids().first()?),
}
}
pub(crate) fn current_document(&self) -> Option<(DocumentId, Document)> {
let current_id = self.current_document_id()?;
Some((current_id, self.documents.read(&current_id)?))
}
pub(crate) fn documents_before_current(&self) -> Vec<(DocumentId, Document)> {
let Some(current_id) = self.current_document_id() else {
return Vec::new();
};
self.documents
.document_ids()
.into_iter()
.take_while(|id| *id != current_id)
.filter_map(|id| Some((id, self.documents.read(&id)?)))
.collect()
}
pub(crate) fn documents_after_current(&self) -> Vec<(DocumentId, Document)> {
let Some(current_id) = self.current_document_id() else {
return Vec::new();
};
self.documents
.document_ids()
.into_iter()
.skip_while(|id| *id != current_id)
.skip(1)
.filter_map(|id| Some((id, self.documents.read(&id)?)))
.collect()
}
pub(crate) fn set_current_document(&mut self, id: DocumentId) {
self.current_document = Some(id);
self.flush();
}
pub(crate) fn set_document_order(&mut self, order: Vec<DocumentId>) {
self.document_order = Some(order);
self.flush();
}
pub(crate) fn write_preferences(&mut self, preferences: Preferences) {
let Ok(preferences) = ron::ser::to_string_pretty(&preferences, Default::default()) else {
tracing::error!("Failed to serialize preferences");
return;
};
std::fs::write(Self::preferences_file_path(), &preferences).unwrap_or_else(|e| {
tracing::error!("Failed to write preferences to disk: {e}");
});
}
pub(crate) fn load_preferences(&self) -> Option<Preferences> {
let data = std::fs::read_to_string(Self::preferences_file_path()).ok()?;
let preferences = ron::from_str(&data).ok()?;
Some(preferences)
}
fn flush(&self) {
let data = match ron::ser::to_string_pretty(self, Default::default()) {
Ok(d) => d,
Err(e) => {
tracing::error!("Failed to serialize persistent data: {e}");
return;
}
};
if let Err(e) = std::fs::write(Self::state_file_path(), data) {
tracing::error!("Failed to write persistent data to disk: {e}");
}
}
pub(crate) fn load_from_disk(&mut self) {
let path = Self::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;
}
Err(e) => {
tracing::error!("Failed to read persistent data from disk: {e}");
return;
}
};
let loaded = match ron::from_str(&data) {
Ok(d) => d,
Err(e) => {
tracing::error!("Failed to deserialize persistent data: {e}");
return;
}
};
*self = loaded;
}
fn state_file_path() -> std::path::PathBuf {
let mut path = crate::dirs::graphite_data_dir();
path.push(crate::consts::APP_STATE_FILE_NAME);
path
}
fn preferences_file_path() -> std::path::PathBuf {
let mut path = crate::dirs::graphite_data_dir();
path.push(crate::consts::APP_PREFERENCES_FILE_NAME);
path
}
}
#[derive(Default, serde::Serialize, serde::Deserialize)]
struct DocumentStore(Vec<DocumentInfo>);
impl DocumentStore {
fn write(&mut self, id: DocumentId, document: Document) {
let meta = DocumentInfo::new(id, &document);
if let Some(existing) = self.0.iter_mut().find(|meta| meta.id == id) {
*existing = meta;
} else {
self.0.push(meta);
}
if let Err(e) = std::fs::write(Self::document_path(&id), document.content) {
tracing::error!("Failed to write document {id:?} to disk: {e}");
}
}
fn delete(&mut self, id: &DocumentId) {
self.0.retain(|meta| meta.id != *id);
if let Err(e) = std::fs::remove_file(Self::document_path(id)) {
tracing::error!("Failed to delete document {id:?} from disk: {e}");
}
}
fn read(&self, id: &DocumentId) -> Option<Document> {
let meta = self.0.iter().find(|meta| meta.id == *id)?;
let content = std::fs::read_to_string(Self::document_path(id)).ok()?;
Some(Document {
content,
name: meta.name.clone(),
path: meta.path.clone(),
is_saved: meta.is_saved,
})
}
fn force_order(&mut self, desired_order: Vec<DocumentId>) {
let mut ordered_prefix_len = 0;
for id in desired_order {
if let Some(offset) = self.0[ordered_prefix_len..].iter().position(|meta| meta.id == id) {
let found_index = ordered_prefix_len + offset;
if found_index != ordered_prefix_len {
self.0[ordered_prefix_len..=found_index].rotate_right(1);
}
ordered_prefix_len += 1;
}
}
self.0.truncate(ordered_prefix_len);
}
fn document_ids(&self) -> Vec<DocumentId> {
self.0.iter().map(|meta| meta.id).collect()
}
fn document_path(id: &DocumentId) -> std::path::PathBuf {
let mut path = crate::dirs::graphite_autosave_documents_dir();
path.push(format!("{:x}.graphite", id.0));
path
}
}
#[derive(serde::Serialize, serde::Deserialize)]
struct DocumentInfo {
id: DocumentId,
name: String,
path: Option<std::path::PathBuf>,
is_saved: bool,
}
impl DocumentInfo {
fn new(id: DocumentId, Document { name, path, is_saved, .. }: &Document) -> Self {
Self {
id,
name: name.clone(),
path: path.clone(),
is_saved: *is_saved,
}
}
}

5
desktop/src/render.rs Normal file
View file

@ -0,0 +1,5 @@
mod frame_buffer_ref;
pub(crate) use frame_buffer_ref::FrameBufferRef;
mod graphics_state;
pub(crate) use graphics_state::GraphicsState;

View file

@ -0,0 +1,103 @@
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(
// 1st triangle
vec2f( -1.0, -1.0), // center
vec2f( 1.0, -1.0), // right, center
vec2f( -1.0, 1.0), // center, top
// 2nd triangle
vec2f( -1.0, 1.0), // center, top
vec2f( 1.0, -1.0), // right, center
vec2f( 1.0, 1.0), // right, top
);
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;
}
struct Constants {
viewport_scale: vec2<f32>,
viewport_offset: vec2<f32>,
};
var<push_constant> constants: Constants;
@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_linear = textureSample(t_ui, s_diffuse, in.tex_coords);
if (ui_linear.a >= 0.999) {
return ui_linear;
}
let viewport_coordinate = (in.tex_coords - constants.viewport_offset) * constants.viewport_scale;
let overlay_srgb = textureSample(t_overlays, s_diffuse, viewport_coordinate);
let viewport_srgb = textureSample(t_viewport, s_diffuse, viewport_coordinate);
// UI texture is premultiplied, we need to unpremultiply before blending
let ui_srgb = linear_to_srgb(unpremultiply(ui_linear));
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);
}
}

View file

@ -0,0 +1,53 @@
use thiserror::Error;
pub(crate) struct FrameBufferRef<'a> {
buffer: &'a [u8],
width: usize,
height: usize,
}
impl<'a> FrameBufferRef<'a> {
pub(crate) fn new(buffer: &'a [u8], width: usize, height: usize) -> Result<Self, FrameBufferError> {
let fb = Self { buffer, width, height };
fb.validate_size()?;
Ok(fb)
}
pub(crate) fn buffer(&self) -> &[u8] {
self.buffer
}
pub(crate) fn width(&self) -> usize {
self.width
}
pub(crate) fn height(&self) -> usize {
self.height
}
fn validate_size(&self) -> Result<(), FrameBufferError> {
if self.buffer.len() != self.width * self.height * 4 {
Err(FrameBufferError::InvalidSize {
buffer_size: self.buffer.len(),
expected_size: self.width * self.height * 4,
width: self.width,
height: self.height,
})
} else {
Ok(())
}
}
}
impl<'a> std::fmt::Debug for FrameBufferRef<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FrameBuffer")
.field("width", &self.width)
.field("height", &self.height)
.field("len", &self.buffer.len())
.finish()
}
}
#[derive(Error, Debug)]
pub(crate) enum FrameBufferError {
#[error("Invalid buffer size {buffer_size}, expected {expected_size} for width {width} multiplied with height {height} multiplied by 4 channels")]
InvalidSize { buffer_size: usize, expected_size: usize, width: usize, height: usize },
}

View file

@ -0,0 +1,321 @@
use std::sync::Arc;
use winit::window::Window;
use graphite_desktop_wrapper::{Color, WgpuContext, WgpuExecutor};
#[derive(derivative::Derivative)]
#[derivative(Debug)]
pub(crate) struct GraphicsState {
surface: wgpu::Surface<'static>,
context: WgpuContext,
executor: WgpuExecutor,
config: wgpu::SurfaceConfiguration,
render_pipeline: wgpu::RenderPipeline,
transparent_texture: wgpu::Texture,
sampler: wgpu::Sampler,
viewport_scale: [f32; 2],
viewport_offset: [f32; 2],
viewport_texture: Option<wgpu::Texture>,
overlays_texture: Option<wgpu::Texture>,
ui_texture: Option<wgpu::Texture>,
bind_group: Option<wgpu::BindGroup>,
#[derivative(Debug = "ignore")]
overlays_scene: Option<vello::Scene>,
}
impl GraphicsState {
pub(crate) fn new(window: Arc<Window>, context: WgpuContext) -> Self {
let size = window.inner_size();
let surface = context.instance.create_surface(window).unwrap();
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: surface_caps.present_modes[0],
alpha_mode: surface_caps.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&context.device, &config);
let transparent_texture = 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::FilterMode::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: &[&texture_bind_group_layout],
push_constant_ranges: &[wgpu::PushConstantRange {
stages: wgpu::ShaderStages::FRAGMENT,
range: 0..size_of::<Constants>() 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: None,
cache: None,
});
let wgpu_executor = WgpuExecutor::with_context(context.clone()).expect("Failed to create WgpuExecutor");
Self {
surface,
context,
executor: wgpu_executor,
config,
render_pipeline,
transparent_texture,
sampler,
viewport_scale: [1.0, 1.0],
viewport_offset: [0.0, 0.0],
viewport_texture: None,
overlays_texture: None,
ui_texture: None,
bind_group: None,
overlays_scene: None,
}
}
pub(crate) fn resize(&mut self, width: u32, height: u32) {
if width > 0 && height > 0 && (self.config.width != width || self.config.height != height) {
self.config.width = width;
self.config.height = height;
self.surface.configure(&self.context.device, &self.config);
}
}
pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: wgpu::Texture) {
self.viewport_texture = Some(viewport_texture);
self.update_bindgroup();
}
pub(crate) fn bind_overlays_texture(&mut self, overlays_texture: wgpu::Texture) {
self.overlays_texture = Some(overlays_texture);
self.update_bindgroup();
}
pub(crate) fn bind_ui_texture(&mut self, bind_ui_texture: wgpu::Texture) {
self.ui_texture = Some(bind_ui_texture);
self.update_bindgroup();
}
pub(crate) fn set_viewport_scale(&mut self, scale: [f32; 2]) {
self.viewport_scale = scale;
}
pub(crate) fn set_viewport_offset(&mut self, offset: [f32; 2]) {
self.viewport_offset = offset;
}
pub(crate) fn set_overlays_scene(&mut self, scene: vello::Scene) {
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 texture = futures::executor::block_on(self.executor.render_vello_scene_to_texture(&scene, size, &Default::default(), Color::TRANSPARENT));
let Ok(texture) = texture else {
tracing::error!("Error rendering overlays");
return;
};
self.bind_overlays_texture(texture);
}
pub(crate) fn render(&mut self, window: &Window) -> Result<(), wgpu::SurfaceError> {
if let Some(scene) = self.overlays_scene.take() {
self.render_overlays(scene);
}
let output = self.surface.get_current_texture()?;
let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self.context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder") });
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("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.0 }),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_push_constants(
wgpu::ShaderStages::FRAGMENT,
0,
bytemuck::bytes_of(&Constants {
viewport_scale: self.viewport_scale,
viewport_offset: self.viewport_offset,
}),
);
if let Some(bind_group) = &self.bind_group {
render_pass.set_bind_group(0, bind_group, &[]);
render_pass.draw(0..6, 0..1); // Draw 3 vertices for fullscreen triangle
} else {
tracing::warn!("No bind group available - showing clear color only");
}
}
self.context.queue.submit(std::iter::once(encoder.finish()));
window.pre_present_notify();
output.present();
Ok(())
}
fn update_bindgroup(&mut self) {
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.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);
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Constants {
viewport_scale: [f32; 2],
viewport_offset: [f32; 2],
}

View file

@ -0,0 +1,33 @@
[package]
name = "graphite-desktop-wrapper"
version = "0.1.0"
description = "Graphite Desktop Wrapper"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[features]
default = ["gpu"]
gpu = ["graphite-editor/gpu"]
[dependencies]
# Local dependencies
graphite-editor = { path = "../../editor", features = [
"gpu",
"vello",
] }
graphene-std = { workspace = true }
graph-craft = { workspace = true }
wgpu-executor = { workspace = true }
wgpu = { workspace = true }
thiserror = { workspace = true }
futures = { workspace = true }
tracing = { workspace = true }
dirs = { workspace = true }
ron = { workspace = true}
vello = { workspace = true }
image = { workspace = true }
serde = { workspace = true }

View file

@ -0,0 +1,148 @@
use graphene_std::Color;
use graphene_std::raster::Image;
use graphite_editor::messages::app_window::app_window_message_handler::AppWindowPlatform;
use graphite_editor::messages::prelude::{AppWindowMessage, DocumentMessage, PortfolioMessage, PreferencesMessage};
use crate::messages::Platform;
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::OpenFileDialogResult { path, content, context } => match context {
OpenFileDialogContext::Document => {
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { 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 extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase();
match extension.as_str() {
"graphite" => {
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { path, content });
}
_ => {
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportFile { path, content });
}
}
}
DesktopWrapperMessage::OpenDocument { path, content } => {
let Ok(content) = String::from_utf8(content) else {
tracing::warn!("Document file is invalid: {}", path.display());
return;
};
let message = PortfolioMessage::OpenDocumentFile {
document_name: None,
document_path: Some(path),
document_serialized_content: content,
};
dispatcher.queue_editor_message(message.into());
}
DesktopWrapperMessage::ImportFile { path, content } => {
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase();
match extension.as_str() {
"svg" => {
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportSvg { path, content });
}
_ => {
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportImage { path, content });
}
}
}
DesktopWrapperMessage::ImportSvg { path, content } => {
let Ok(content) = String::from_utf8(content) else {
tracing::warn!("Svg file is invalid: {}", path.display());
return;
};
let message = PortfolioMessage::PasteSvg {
name: path.file_stem().map(|s| s.to_string_lossy().to_string()),
svg: content,
mouse: None,
parent_and_insert_index: None,
};
dispatcher.queue_editor_message(message.into());
}
DesktopWrapperMessage::ImportImage { path, content } => {
let name = path.file_stem().and_then(|s| s.to_str()).map(|s| s.to_string());
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase();
let Some(image_format) = image::ImageFormat::from_extension(&extension) else {
tracing::warn!("Unsupported file type: {}", path.display());
return;
};
let reader = image::ImageReader::with_format(std::io::Cursor::new(content), image_format);
let Ok(image) = reader.decode() else {
tracing::error!("Failed to decode image: {}", path.display());
return;
};
let width = image.width();
let height = image.height();
// TODO: Handle Image formats with more than 8 bits per channel
let image_data = image.to_rgba8();
let image = Image::<Color>::from_image_data(image_data.as_raw(), width, height);
let message = PortfolioMessage::PasteImage {
name,
image,
mouse: None,
parent_and_insert_index: None,
};
dispatcher.queue_editor_message(message.into());
}
DesktopWrapperMessage::PollNodeGraphEvaluation => dispatcher.poll_node_graph_evaluation(),
DesktopWrapperMessage::UpdatePlatform(platform) => {
let platform = match platform {
Platform::Windows => AppWindowPlatform::Windows,
Platform::Mac => AppWindowPlatform::Mac,
Platform::Linux => AppWindowPlatform::Linux,
};
let message = AppWindowMessage::AppWindowUpdatePlatform { platform };
dispatcher.queue_editor_message(message.into());
}
DesktopWrapperMessage::LoadDocument {
id,
document,
to_front,
select_after_open,
} => {
let message = PortfolioMessage::OpenDocumentFileWithId {
document_id: id,
document_name: Some(document.name),
document_path: document.path,
document_serialized_content: document.content,
document_is_auto_saved: true,
document_is_saved: document.is_saved,
to_front,
select_after_open,
};
dispatcher.queue_editor_message(message.into());
}
DesktopWrapperMessage::SelectDocument { id } => {
let message = PortfolioMessage::SelectDocument { document_id: id };
dispatcher.queue_editor_message(message.into());
}
DesktopWrapperMessage::LoadPreferences { preferences } => {
let message = PreferencesMessage::Load { preferences };
dispatcher.queue_editor_message(message.into());
}
}
}

View file

@ -0,0 +1,23 @@
use graphite_editor::messages::prelude::InputPreprocessorMessage;
use super::DesktopWrapperMessageDispatcher;
use super::messages::{DesktopFrontendMessage, EditorMessage};
pub(super) fn intercept_editor_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: EditorMessage) -> Option<EditorMessage> {
match message {
EditorMessage::InputPreprocessor(message) => {
if let InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports } = &message {
let top_left = bounds_of_viewports[0].top_left;
let bottom_right = bounds_of_viewports[0].bottom_right;
dispatcher.respond(DesktopFrontendMessage::UpdateViewportBounds {
x: top_left.x as f32,
y: top_left.y as f32,
width: (bottom_right.x - top_left.x) as f32,
height: (bottom_right.y - top_left.y) as f32,
});
}
Some(EditorMessage::InputPreprocessor(message))
}
m => Some(m),
}
}

View file

@ -0,0 +1,119 @@
use std::path::PathBuf;
use graphite_editor::messages::prelude::FrontendMessage;
use super::DesktopWrapperMessageDispatcher;
use super::messages::{DesktopFrontendMessage, Document, FileFilter, OpenFileDialogContext, SaveFileDialogContext};
pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option<FrontendMessage> {
match message {
FrontendMessage::RenderOverlays { context } => {
dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene()));
}
FrontendMessage::TriggerOpenDocument => {
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
title: "Open Document".to_string(),
filters: vec![FileFilter {
name: "Graphite".to_string(),
extensions: vec!["graphite".to_string()],
}],
context: OpenFileDialogContext::Document,
});
}
FrontendMessage::TriggerImport => {
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
title: "Import File".to_string(),
filters: vec![
FileFilter {
name: "Svg".to_string(),
extensions: vec!["svg".to_string()],
},
FileFilter {
name: "Image".to_string(),
extensions: vec!["png".to_string(), "jpg".to_string(), "jpeg".to_string(), "bmp".to_string()],
},
],
context: OpenFileDialogContext::Import,
});
}
FrontendMessage::TriggerSaveDocument { document_id, name, path, content } => {
if let Some(path) = path {
dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content });
} else {
dispatcher.respond(DesktopFrontendMessage::SaveFileDialog {
title: "Save Document".to_string(),
default_filename: name,
default_folder: path.and_then(|p| p.parent().map(PathBuf::from)),
filters: vec![FileFilter {
name: "Graphite".to_string(),
extensions: vec!["graphite".to_string()],
}],
context: SaveFileDialogContext::Document { document_id, content },
});
}
}
FrontendMessage::TriggerSaveFile { name, content } => {
dispatcher.respond(DesktopFrontendMessage::SaveFileDialog {
title: "Save File".to_string(),
default_filename: name,
default_folder: None,
filters: Vec::new(),
context: SaveFileDialogContext::File { content },
});
}
FrontendMessage::TriggerVisitLink { url } => {
dispatcher.respond(DesktopFrontendMessage::OpenUrl(url));
}
FrontendMessage::UpdateWindowState { maximized, minimized } => {
dispatcher.respond(DesktopFrontendMessage::UpdateWindowState { maximized, minimized });
// Forward this to update the UI
return Some(message);
}
FrontendMessage::CloseWindow => {
dispatcher.respond(DesktopFrontendMessage::CloseWindow);
}
FrontendMessage::TriggerPersistenceWriteDocument { document_id, document, details } => {
dispatcher.respond(DesktopFrontendMessage::PersistenceWriteDocument {
id: document_id,
document: Document {
name: details.name,
path: None,
content: document,
is_saved: details.is_saved,
},
});
}
FrontendMessage::TriggerPersistenceRemoveDocument { document_id } => {
dispatcher.respond(DesktopFrontendMessage::PersistenceDeleteDocument { id: document_id });
}
FrontendMessage::UpdateActiveDocument { document_id } => {
dispatcher.respond(DesktopFrontendMessage::PersistenceUpdateCurrentDocument { id: document_id });
// Forward this to update the UI
return Some(FrontendMessage::UpdateActiveDocument { document_id });
}
FrontendMessage::UpdateOpenDocumentsList { open_documents } => {
dispatcher.respond(DesktopFrontendMessage::PersistenceUpdateDocumentsList {
ids: open_documents.iter().map(|document| document.id).collect(),
});
// Forward this to update the UI
return Some(FrontendMessage::UpdateOpenDocumentsList { open_documents });
}
FrontendMessage::TriggerLoadFirstAutoSaveDocument => {
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadCurrentDocument);
}
FrontendMessage::TriggerLoadRestAutoSaveDocuments => {
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadRemainingDocuments);
}
FrontendMessage::TriggerSavePreferences { preferences } => {
dispatcher.respond(DesktopFrontendMessage::PersistenceWritePreferences { preferences });
}
FrontendMessage::TriggerLoadPreferences => {
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences);
}
m => return Some(m),
}
None
}

View file

@ -0,0 +1,79 @@
use graph_craft::wasm_application_io::WasmApplicationIo;
use graphite_editor::application::Editor;
use graphite_editor::messages::prelude::{FrontendMessage, Message};
// TODO: Remove usage of this reexport in desktop create and remove this line
pub use graphene_std::Color;
pub use wgpu_executor::Context as WgpuContext;
pub use wgpu_executor::WgpuExecutor;
pub mod messages;
use messages::{DesktopFrontendMessage, DesktopWrapperMessage};
mod message_dispatcher;
use message_dispatcher::DesktopWrapperMessageDispatcher;
mod handle_desktop_wrapper_message;
mod intercept_editor_message;
mod intercept_frontend_message;
pub struct DesktopWrapper {
editor: Editor,
}
impl DesktopWrapper {
pub fn new() -> Self {
Self { editor: Editor::new() }
}
pub fn init(&self, wgpu_context: WgpuContext) {
let application_io = WasmApplicationIo::new_with_context(wgpu_context);
futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io));
}
pub fn dispatch(&mut self, message: DesktopWrapperMessage) -> Vec<DesktopFrontendMessage> {
let mut executor = DesktopWrapperMessageDispatcher::new(&mut self.editor);
executor.queue_desktop_wrapper_message(message);
executor.execute()
}
pub async fn execute_node_graph() -> NodeGraphExecutionResult {
let result = graphite_editor::node_graph_executor::run_node_graph().await;
match result {
(true, texture) => NodeGraphExecutionResult::HasRun(texture.map(|t| t.texture)),
(false, _) => NodeGraphExecutionResult::NotRun,
}
}
}
impl Default for DesktopWrapper {
fn default() -> Self {
Self::new()
}
}
pub enum NodeGraphExecutionResult {
HasRun(Option<wgpu::Texture>),
NotRun,
}
pub fn deserialize_editor_message(data: &[u8]) -> Option<DesktopWrapperMessage> {
if let Ok(string) = std::str::from_utf8(data) {
if let Ok(message) = ron::de::from_str::<Message>(string) {
Some(DesktopWrapperMessage::FromWeb(message.into()))
} else {
None
}
} else {
None
}
}
pub fn serialize_frontend_messages(messages: Vec<FrontendMessage>) -> Option<Vec<u8>> {
if let Ok(serialized) = ron::ser::to_string(&messages) {
Some(serialized.into_bytes())
} else {
None
}
}

View file

@ -0,0 +1,76 @@
use graphite_editor::application::Editor;
use std::collections::VecDeque;
use super::handle_desktop_wrapper_message::handle_desktop_wrapper_message;
use super::intercept_editor_message::intercept_editor_message;
use super::intercept_frontend_message::intercept_frontend_message;
use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, EditorMessage};
pub(crate) struct DesktopWrapperMessageDispatcher<'a> {
editor: &'a mut Editor,
desktop_wrapper_message_queue: VecDeque<DesktopWrapperMessage>,
editor_message_queue: Vec<EditorMessage>,
responses: Vec<DesktopFrontendMessage>,
}
impl<'a> DesktopWrapperMessageDispatcher<'a> {
pub(crate) fn new(editor: &'a mut Editor) -> Self {
Self {
editor,
desktop_wrapper_message_queue: VecDeque::new(),
editor_message_queue: Vec::new(),
responses: Vec::new(),
}
}
pub(crate) fn execute(mut self) -> Vec<DesktopFrontendMessage> {
self.process_queue();
self.responses
}
pub(crate) fn queue_desktop_wrapper_message(&mut self, message: DesktopWrapperMessage) {
self.desktop_wrapper_message_queue.push_back(message);
}
pub(super) fn queue_editor_message(&mut self, message: EditorMessage) {
if let Some(message) = intercept_editor_message(self, message) {
self.editor_message_queue.push(message);
}
}
pub(super) fn respond(&mut self, response: DesktopFrontendMessage) {
self.responses.push(response);
}
pub(super) fn poll_node_graph_evaluation(&mut self) {
let mut responses = VecDeque::new();
if let Err(e) = self.editor.poll_node_graph_evaluation(&mut responses) {
if e != "No active document" {
tracing::error!("Error poling node graph: {}", e);
}
}
while let Some(message) = responses.pop_front() {
self.queue_editor_message(message);
}
}
fn process_queue(&mut self) {
let mut frontend_messages = Vec::new();
while !self.desktop_wrapper_message_queue.is_empty() || !self.editor_message_queue.is_empty() {
while let Some(message) = self.desktop_wrapper_message_queue.pop_front() {
handle_desktop_wrapper_message(self, message);
}
let current_frontend_messages = self
.editor
.handle_message(EditorMessage::Batched {
messages: std::mem::take(&mut self.editor_message_queue).into_boxed_slice(),
})
.into_iter()
.filter_map(|m| intercept_frontend_message(self, m));
frontend_messages.extend(current_frontend_messages);
}
self.respond(DesktopFrontendMessage::ToWeb(frontend_messages));
}
}

View file

@ -0,0 +1,135 @@
pub use graphite_editor::messages::prelude::DocumentId;
use graphite_editor::messages::prelude::FrontendMessage;
use std::path::PathBuf;
pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage;
pub use graphite_editor::messages::prelude::PreferencesMessageHandler as Preferences;
pub enum DesktopFrontendMessage {
ToWeb(Vec<FrontendMessage>),
OpenFileDialog {
title: String,
filters: Vec<FileFilter>,
context: OpenFileDialogContext,
},
SaveFileDialog {
title: String,
default_filename: String,
default_folder: Option<PathBuf>,
filters: Vec<FileFilter>,
context: SaveFileDialogContext,
},
WriteFile {
path: PathBuf,
content: Vec<u8>,
},
OpenUrl(String),
UpdateViewportBounds {
x: f32,
y: f32,
width: f32,
height: f32,
},
UpdateOverlays(vello::Scene),
UpdateWindowState {
maximized: bool,
minimized: bool,
},
PersistenceWriteDocument {
id: DocumentId,
document: Document,
},
PersistenceDeleteDocument {
id: DocumentId,
},
PersistenceUpdateCurrentDocument {
id: DocumentId,
},
PersistenceLoadCurrentDocument,
PersistenceLoadRemainingDocuments,
PersistenceUpdateDocumentsList {
ids: Vec<DocumentId>,
},
PersistenceWritePreferences {
preferences: Preferences,
},
PersistenceLoadPreferences,
CloseWindow,
}
pub enum DesktopWrapperMessage {
FromWeb(Box<EditorMessage>),
OpenFileDialogResult {
path: PathBuf,
content: Vec<u8>,
context: OpenFileDialogContext,
},
SaveFileDialogResult {
path: PathBuf,
context: SaveFileDialogContext,
},
OpenDocument {
path: PathBuf,
content: Vec<u8>,
},
OpenFile {
path: PathBuf,
content: Vec<u8>,
},
ImportFile {
path: PathBuf,
content: Vec<u8>,
},
ImportSvg {
path: PathBuf,
content: Vec<u8>,
},
ImportImage {
path: PathBuf,
content: Vec<u8>,
},
PollNodeGraphEvaluation,
UpdatePlatform(Platform),
LoadDocument {
id: DocumentId,
document: Document,
to_front: bool,
select_after_open: bool,
},
SelectDocument {
id: DocumentId,
},
LoadPreferences {
preferences: Preferences,
},
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]
pub struct Document {
pub content: String,
pub name: String,
pub path: Option<PathBuf>,
pub is_saved: bool,
}
pub struct FileFilter {
pub name: String,
pub extensions: Vec<String>,
}
pub enum OpenFileDialogContext {
Document,
Import,
}
pub enum SaveFileDialogContext {
Document { document_id: DocumentId, content: Vec<u8> },
File { content: Vec<u8> },
}
pub enum Platform {
Windows,
Mac,
Linux,
}

View file

@ -2,7 +2,7 @@
name = "graphite-editor"
publish = false
version = "0.0.0"
rust-version = "1.85"
rust-version = "1.88"
authors = ["Graphite Authors <contact@graphite.rs>"]
edition = "2024"
readme = "../README.md"
@ -12,20 +12,17 @@ license = "Apache-2.0"
[features]
default = ["wasm"]
wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"]
wasm = ["wasm-bindgen", "graphene-std/wasm"]
gpu = ["interpreted-executor/gpu", "wgpu-executor"]
tauri = ["ron", "decouple-execution"]
decouple-execution = []
resvg = ["graphene-std/resvg"]
vello = ["graphene-std/vello", "resvg"]
ron = ["dep:ron"]
[dependencies]
# Local dependencies
graphite-proc-macros = { workspace = true }
graph-craft = { workspace = true }
interpreted-executor = { workspace = true }
graphene-std = { workspace = true }
graphene-std = { workspace = true } # NOTE: `graphene-core` should not be added here because `graphene-std` re-exports its contents
preprocessor = { workspace = true }
# Workspace dependencies
@ -35,7 +32,6 @@ bitflags = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
bezier-rs = { workspace = true }
kurbo = { workspace = true }
futures = { workspace = true }
glam = { workspace = true }
@ -46,17 +42,15 @@ num_enum = { workspace = true }
usvg = { workspace = true }
once_cell = { workspace = true }
web-sys = { workspace = true }
# Required dependencies
spin = "0.9.8"
vello = { workspace = true }
base64 = { workspace = true }
spin = { workspace = true }
# Optional local dependencies
wgpu-executor = { workspace = true, optional = true }
# Optional workspace dependencies
wasm-bindgen = { workspace = true, optional = true }
wasm-bindgen-futures = { workspace = true, optional = true }
ron = { workspace = true, optional = true }
[dev-dependencies]
# Workspace dependencies

View file

@ -1,24 +1,56 @@
use std::env;
use std::process::Command;
const GRAPHITE_RELEASE_SERIES: &str = "Alpha 4";
fn main() {
// Execute a Git command for its stdout. Early exit if it fails for any of the possible reasons.
let try_git_command = |args: &[&str]| -> Option<String> {
let git_output = Command::new("git").args(args).output().ok()?;
let maybe_empty = String::from_utf8(git_output.stdout).ok()?;
let command_result = (!maybe_empty.is_empty()).then_some(maybe_empty)?;
Some(command_result)
};
// Execute a Git command for its output. Return "unknown" if it fails for any of the possible reasons.
let git_command = |args| -> String { try_git_command(args).unwrap_or_else(|| String::from("unknown")) };
// Instruct Cargo to rerun this build script if any of these environment variables change.
println!("cargo:rerun-if-env-changed=GRAPHITE_GIT_COMMIT_DATE");
println!("cargo:rerun-if-env-changed=GRAPHITE_GIT_COMMIT_HASH");
println!("cargo:rerun-if-env-changed=GRAPHITE_GIT_COMMIT_BRANCH");
println!("cargo:rerun-if-env-changed=GITHUB_HEAD_REF");
// Rather than printing to any terminal, these commands set environment variables in the Cargo toolchain.
// They are accessed with the `env!("...")` macro in the codebase.
println!("cargo:rustc-env=GRAPHITE_GIT_COMMIT_DATE={}", git_command(&["log", "-1", "--format=%cd"]));
println!("cargo:rustc-env=GRAPHITE_GIT_COMMIT_HASH={}", git_command(&["rev-parse", "HEAD"]));
let branch = std::env::var("GITHUB_HEAD_REF").unwrap_or_default();
let branch = if branch.is_empty() { git_command(&["name-rev", "--name-only", "HEAD"]) } else { branch };
println!("cargo:rustc-env=GRAPHITE_GIT_COMMIT_BRANCH={branch}");
// Try to get the commit information from the environment (e.g. set by CI), otherwise fall back to Git commands.
let commit_date = env_or_else("GRAPHITE_GIT_COMMIT_DATE", || git_or_unknown(&["log", "-1", "--format=%cI"]));
let commit_hash = env_or_else("GRAPHITE_GIT_COMMIT_HASH", || git_or_unknown(&["rev-parse", "HEAD"]));
let commit_branch = env_or_else("GRAPHITE_GIT_COMMIT_BRANCH", || {
let gh = env::var("GITHUB_HEAD_REF").unwrap_or_default();
if !gh.trim().is_empty() {
gh.trim().to_string()
} else {
git_or_unknown(&["rev-parse", "--abbrev-ref", "HEAD"])
}
});
// Instruct Cargo to set environment variables for compile time.
// They are accessed with the `env!("GRAPHITE_*")` macro in the codebase.
println!("cargo:rustc-env=GRAPHITE_GIT_COMMIT_DATE={commit_date}");
println!("cargo:rustc-env=GRAPHITE_GIT_COMMIT_HASH={commit_hash}");
println!("cargo:rustc-env=GRAPHITE_GIT_COMMIT_BRANCH={commit_branch}");
println!("cargo:rustc-env=GRAPHITE_RELEASE_SERIES={GRAPHITE_RELEASE_SERIES}");
}
/// Get an environment variable, or if it is not set or empty, use the provided fallback function. Returns a string with trimmed whitespace.
fn env_or_else(key: &str, fallback: impl FnOnce() -> String) -> String {
match env::var(key) {
Ok(v) if !v.trim().is_empty() => v.trim().to_string(),
_ => fallback().trim().to_string(),
}
}
/// Execute a Git command to obtain its output. Return "unknown" if it fails for any of the possible reasons.
fn git_or_unknown(args: &[&str]) -> String {
git(args).unwrap_or_else(|| "unknown".to_string())
}
/// Run a git command and capture trimmed stdout.
/// Returns None if git is missing, exits with error, or stdout is empty/non-UTF8.
fn git(args: &[&str]) -> Option<String> {
let output = Command::new("git").args(args).output().ok()?;
if !output.status.success() {
return None;
}
let s = String::from_utf8(output.stdout).ok()?;
let t = s.trim();
if t.is_empty() { None } else { Some(t.to_string()) }
}

View file

@ -51,7 +51,7 @@ pub fn commit_info_localized(localized_commit_date: &str) -> String {
{}",
GRAPHITE_RELEASE_SERIES,
GRAPHITE_GIT_COMMIT_BRANCH,
&GRAPHITE_GIT_COMMIT_HASH[..8],
GRAPHITE_GIT_COMMIT_HASH.get(..8).unwrap_or(GRAPHITE_GIT_COMMIT_HASH),
localized_commit_date
)
}

View file

@ -101,10 +101,12 @@ pub const MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY: f64 = 48.;
// PATH TOOL
pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 6.;
pub const SELECTION_THRESHOLD: f64 = 10.;
pub const DRILL_THROUGH_THRESHOLD: f64 = 10.;
pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.;
pub const SEGMENT_INSERTION_DISTANCE: f64 = 5.;
pub const SEGMENT_OVERLAY_SIZE: f64 = 10.;
pub const SEGMENT_SELECTED_THICKNESS: f64 = 3.;
pub const HANDLE_LENGTH_FACTOR: f64 = 0.5;
// PEN TOOL
@ -125,7 +127,11 @@ pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.;
pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9;
pub const NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION: f64 = 1.2;
pub const NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH: f64 = 10.;
pub const ARC_SNAP_THRESHOLD: f64 = 5.;
pub const ARC_SWEEP_GIZMO_RADIUS: f64 = 14.;
pub const ARC_SWEEP_GIZMO_TEXT_HEIGHT: f64 = 12.;
pub const GIZMO_HIDE_THRESHOLD: f64 = 20.;
pub const GRID_ROW_COLUMN_GIZMO_OFFSET: f64 = 15.;
// SCROLLBARS
pub const SCROLLBAR_SPACING: f64 = 0.1;
@ -134,25 +140,21 @@ pub const SCALE_EFFECT: f64 = 0.5;
// COLORS
pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff";
pub const COLOR_OVERLAY_BLUE_50: &str = "#00a8ff80";
pub const COLOR_OVERLAY_YELLOW: &str = "#ffc848";
pub const COLOR_OVERLAY_YELLOW_DULL: &str = "#d7ba8b";
pub const COLOR_OVERLAY_GREEN: &str = "#63ce63";
pub const COLOR_OVERLAY_RED: &str = "#ef5454";
pub const COLOR_OVERLAY_GRAY: &str = "#cccccc";
pub const COLOR_OVERLAY_GRAY_25: &str = "#cccccc40";
pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
pub const COLOR_OVERLAY_LABEL_BACKGROUND: &str = "#000000cc";
pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf";
// DOCUMENT
pub const FILE_EXTENSION: &str = "graphite";
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 15;
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1;
// INPUT
pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500;
/// SPIRAL NODE INPUT INDICES
pub const SPIRAL_TYPE_INDEX: usize = 1;
pub const SPIRAL_INNER_RADIUS: usize = 2;
pub const SPIRAL_OUTER_RADIUS_INDEX: usize = 3;
pub const SPIRAL_TURNS_INDEX: usize = 4;

View file

@ -1,11 +1,11 @@
use crate::messages::debug::utility_types::MessageLoggingVerbosity;
use crate::messages::dialog::DialogMessageData;
use crate::messages::portfolio::document::node_graph::document_node_definitions;
use crate::messages::defer::DeferMessageContext;
use crate::messages::dialog::DialogMessageContext;
use crate::messages::layout::layout_message_handler::LayoutMessageContext;
use crate::messages::prelude::*;
#[derive(Debug, Default)]
pub struct Dispatcher {
buffered_queue: Option<Vec<VecDeque<Message>>>,
message_queues: Vec<VecDeque<Message>>,
pub responses: Vec<FrontendMessage>,
pub message_handlers: DispatcherMessageHandlers,
@ -14,8 +14,10 @@ pub struct Dispatcher {
#[derive(Debug, Default)]
pub struct DispatcherMessageHandlers {
animation_message_handler: AnimationMessageHandler,
app_window_message_handler: AppWindowMessageHandler,
broadcast_message_handler: BroadcastMessageHandler,
debug_message_handler: DebugMessageHandler,
defer_message_handler: DeferMessageHandler,
dialog_message_handler: DialogMessageHandler,
globals_message_handler: GlobalsMessageHandler,
input_preprocessor_message_handler: InputPreprocessorMessageHandler,
@ -24,7 +26,6 @@ pub struct DispatcherMessageHandlers {
pub portfolio_message_handler: PortfolioMessageHandler,
preferences_message_handler: PreferencesMessageHandler,
tool_message_handler: ToolMessageHandler,
workspace_message_handler: WorkspaceMessageHandler,
}
impl DispatcherMessageHandlers {
@ -45,12 +46,19 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
))),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::DocumentStructureChanged)),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Draw))),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::NodeGraph(
NodeGraphMessageDiscriminant::RunDocumentGraph,
))),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderRulers)),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderScrollbars)),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
];
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::AnimationFrame))];
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)),
MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter),
];
// TODO: Find a way to combine these with the list above. We use strings for now since these are the standard variant names used by multiple messages. But having these also type-checked would be best.
const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"];
@ -90,14 +98,6 @@ impl Dispatcher {
pub fn handle_message<T: Into<Message>>(&mut self, message: T, process_after_all_current: bool) {
let message = message.into();
// Add all additional messages to the buffer if it exists (except from the end buffer message)
if !matches!(message, Message::EndBuffer(_)) {
if let Some(buffered_queue) = &mut self.buffered_queue {
Self::schedule_execution(buffered_queue, true, [message]);
return;
}
}
// If we are not maintaining the buffer, simply add to the current queue
Self::schedule_execution(&mut self.message_queues, process_after_all_current, [message]);
@ -126,71 +126,29 @@ impl Dispatcher {
// Process the action by forwarding it to the relevant message handler, or saving the FrontendMessage to be sent to the frontend
match message {
Message::StartBuffer => {
self.buffered_queue = Some(std::mem::take(&mut self.message_queues));
}
Message::EndBuffer(render_metadata) => {
// Assign the message queue to the currently buffered queue
if let Some(buffered_queue) = self.buffered_queue.take() {
self.cleanup_queues(false);
assert!(self.message_queues.is_empty(), "message queues are always empty when ending a buffer");
self.message_queues = buffered_queue;
};
let graphene_std::renderer::RenderMetadata {
upstream_footprints: footprints,
local_transforms,
first_instance_source_id,
click_targets,
clip_targets,
} = render_metadata;
// Run these update state messages immediately
let messages = [
DocumentMessage::UpdateUpstreamTransforms {
upstream_footprints: footprints,
local_transforms,
first_instance_source_id,
},
DocumentMessage::UpdateClickTargets { click_targets },
DocumentMessage::UpdateClipTargets { clip_targets },
];
Self::schedule_execution(&mut self.message_queues, false, messages.map(Message::from));
}
Message::NoOp => {}
Message::Init => {
// Load persistent data from the browser database
queue.add(FrontendMessage::TriggerLoadFirstAutoSaveDocument);
queue.add(FrontendMessage::TriggerLoadPreferences);
// Display the menu bar at the top of the window
queue.add(MenuBarMessage::SendLayout);
// Send the information for tooltips and categories for each node/input.
queue.add(FrontendMessage::SendUIMetadata {
node_descriptions: document_node_definitions::collect_node_descriptions(),
node_types: document_node_definitions::collect_node_types(),
});
// Finish loading persistent data from the browser database
queue.add(FrontendMessage::TriggerLoadRestAutoSaveDocuments);
}
Message::Animation(message) => {
self.message_handlers.animation_message_handler.process_message(message, &mut queue, ());
}
Message::Batched(messages) => {
messages.iter().for_each(|message| self.handle_message(message.to_owned(), false));
Message::AppWindow(message) => {
self.message_handlers.app_window_message_handler.process_message(message, &mut queue, ());
}
Message::Broadcast(message) => self.message_handlers.broadcast_message_handler.process_message(message, &mut queue, ()),
Message::Debug(message) => {
self.message_handlers.debug_message_handler.process_message(message, &mut queue, ());
}
Message::Defer(message) => {
let context = DeferMessageContext {
portfolio: &self.message_handlers.portfolio_message_handler,
};
self.message_handlers.defer_message_handler.process_message(message, &mut queue, context);
}
Message::Dialog(message) => {
let data = DialogMessageData {
let context = DialogMessageContext {
portfolio: &self.message_handlers.portfolio_message_handler,
preferences: &self.message_handlers.preferences_message_handler,
viewport_bounds: &self.message_handlers.input_preprocessor_message_handler.viewport_bounds,
};
self.message_handlers.dialog_message_handler.process_message(message, &mut queue, data);
self.message_handlers.dialog_message_handler.process_message(message, &mut queue, context);
}
Message::Frontend(message) => {
// Handle these messages immediately by returning early
@ -213,7 +171,7 @@ impl Dispatcher {
self.message_handlers
.input_preprocessor_message_handler
.process_message(message, &mut queue, InputPreprocessorMessageData { keyboard_platform });
.process_message(message, &mut queue, InputPreprocessorMessageContext { keyboard_platform });
}
Message::KeyMapping(message) => {
let input = &self.message_handlers.input_preprocessor_message_handler;
@ -221,12 +179,13 @@ impl Dispatcher {
self.message_handlers
.key_mapping_message_handler
.process_message(message, &mut queue, KeyMappingMessageData { input, actions });
.process_message(message, &mut queue, KeyMappingMessageContext { input, actions });
}
Message::Layout(message) => {
let action_input_mapping = &|action_to_find: &MessageDiscriminant| self.message_handlers.key_mapping_message_handler.action_input_mapping(action_to_find);
let context = LayoutMessageContext { action_input_mapping };
self.message_handlers.layout_message_handler.process_message(message, &mut queue, action_input_mapping);
self.message_handlers.layout_message_handler.process_message(message, &mut queue, context);
}
Message::Portfolio(message) => {
let ipp = &self.message_handlers.input_preprocessor_message_handler;
@ -240,7 +199,7 @@ impl Dispatcher {
self.message_handlers.portfolio_message_handler.process_message(
message,
&mut queue,
PortfolioMessageData {
PortfolioMessageContext {
ipp,
preferences,
current_tool,
@ -255,13 +214,16 @@ impl Dispatcher {
self.message_handlers.preferences_message_handler.process_message(message, &mut queue, ());
}
Message::Tool(message) => {
let document_id = self.message_handlers.portfolio_message_handler.active_document_id().unwrap();
let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) else {
let Some(document_id) = self.message_handlers.portfolio_message_handler.active_document_id() else {
warn!("Called ToolMessage without an active document.\nGot {message:?}");
return;
};
let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) else {
warn!("Called ToolMessage with an invalid active document.\nGot {message:?}");
return;
};
let data = ToolMessageData {
let context = ToolMessageContext {
document_id,
document,
input: &self.message_handlers.input_preprocessor_message_handler,
@ -270,10 +232,11 @@ impl Dispatcher {
preferences: &self.message_handlers.preferences_message_handler,
};
self.message_handlers.tool_message_handler.process_message(message, &mut queue, data);
self.message_handlers.tool_message_handler.process_message(message, &mut queue, context);
}
Message::Workspace(message) => {
self.message_handlers.workspace_message_handler.process_message(message, &mut queue, ());
Message::NoOp => {}
Message::Batched { messages } => {
messages.iter().for_each(|message| self.handle_message(message.to_owned(), false));
}
}
@ -483,7 +446,7 @@ mod test {
assert_eq!(layers_before_copy.len(), 3);
assert_eq!(layers_after_copy.len(), 6);
println!("{:?} {:?}", layers_after_copy, layers_before_copy);
println!("{layers_after_copy:?} {layers_before_copy:?}");
assert_eq!(layers_after_copy[5], shape_id);
}
@ -538,7 +501,8 @@ mod test {
);
let responses = editor.editor.handle_message(PortfolioMessage::OpenDocumentFile {
document_name: document_name.into(),
document_name: Some(document_name.to_string()),
document_path: None,
document_serialized_content,
});

View file

@ -1,6 +1,5 @@
use crate::messages::prelude::*;
use super::animation_message_handler::AnimationTimeMode;
use crate::messages::prelude::*;
#[impl_message(Message, Animation)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
@ -9,9 +8,9 @@ pub enum AnimationMessage {
EnableLivePreview,
DisableLivePreview,
RestartAnimation,
SetFrameIndex(f64),
SetTime(f64),
SetFrameIndex { frame: f64 },
SetTime { time: f64 },
UpdateTime,
IncrementFrameCounter,
SetAnimationTimeMode(AnimationTimeMode),
SetAnimationTimeMode { animation_time_mode: AnimationTimeMode },
}

View file

@ -59,7 +59,7 @@ impl AnimationMessageHandler {
#[message_handler_data]
impl MessageHandler<AnimationMessage, ()> for AnimationMessageHandler {
fn process_message(&mut self, message: AnimationMessage, responses: &mut VecDeque<Message>, _data: ()) {
fn process_message(&mut self, message: AnimationMessage, responses: &mut VecDeque<Message>, _: ()) {
match message {
AnimationMessage::ToggleLivePreview => match self.animation_state {
AnimationState::Stopped => responses.add(AnimationMessage::EnableLivePreview),
@ -82,13 +82,13 @@ impl MessageHandler<AnimationMessage, ()> for AnimationMessageHandler {
// Update the restart and pause/play buttons
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
AnimationMessage::SetFrameIndex(frame) => {
AnimationMessage::SetFrameIndex { frame } => {
self.frame_index = frame;
responses.add(PortfolioMessage::SubmitActiveGraphRender);
// Update the restart and pause/play buttons
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
AnimationMessage::SetTime(time) => {
AnimationMessage::SetTime { time } => {
self.timestamp = time;
responses.add(AnimationMessage::UpdateTime);
}
@ -101,7 +101,6 @@ impl MessageHandler<AnimationMessage, ()> for AnimationMessageHandler {
AnimationMessage::UpdateTime => {
if self.is_playing() {
responses.add(PortfolioMessage::SubmitActiveGraphRender);
if self.live_preview_recently_zero {
// Update the restart and pause/play buttons
responses.add(PortfolioMessage::UpdateDocumentWidgets);
@ -120,7 +119,7 @@ impl MessageHandler<AnimationMessage, ()> for AnimationMessageHandler {
// Update the restart and pause/play buttons
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
AnimationMessage::SetAnimationTimeMode(animation_time_mode) => {
AnimationMessage::SetAnimationTimeMode { animation_time_mode } => {
self.animation_time_mode = animation_time_mode;
}
}

View file

@ -0,0 +1,12 @@
use crate::messages::prelude::*;
use super::app_window_message_handler::AppWindowPlatform;
#[impl_message(Message, AppWindow)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum AppWindowMessage {
AppWindowMinimize,
AppWindowMaximize,
AppWindowUpdatePlatform { platform: AppWindowPlatform },
AppWindowClose,
}

View file

@ -0,0 +1,52 @@
use crate::messages::app_window::AppWindowMessage;
use crate::messages::prelude::*;
use graphite_proc_macros::{ExtractField, message_handler_data};
#[derive(Debug, Clone, Default, ExtractField)]
pub struct AppWindowMessageHandler {
platform: AppWindowPlatform,
maximized: bool,
minimized: bool,
}
#[message_handler_data]
impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
fn process_message(&mut self, message: AppWindowMessage, responses: &mut std::collections::VecDeque<Message>, _: ()) {
match message {
AppWindowMessage::AppWindowMaximize => {
self.maximized = !self.maximized;
responses.add(FrontendMessage::UpdateWindowState {
maximized: self.maximized,
minimized: self.minimized,
});
}
AppWindowMessage::AppWindowMinimize => {
self.minimized = !self.minimized;
responses.add(FrontendMessage::UpdateWindowState {
maximized: self.maximized,
minimized: self.minimized,
});
}
AppWindowMessage::AppWindowUpdatePlatform { platform } => {
self.platform = platform;
responses.add(FrontendMessage::UpdatePlatform { platform: self.platform });
}
AppWindowMessage::AppWindowClose => {
responses.add(FrontendMessage::CloseWindow);
}
}
}
fn actions(&self) -> ActionList {
actions!(AppWindowMessageDiscriminant;)
}
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum AppWindowPlatform {
#[default]
Web,
Windows,
Mac,
Linux,
}

View file

@ -0,0 +1,7 @@
mod app_window_message;
pub mod app_window_message_handler;
#[doc(inline)]
pub use app_window_message::{AppWindowMessage, AppWindowMessageDiscriminant};
#[doc(inline)]
pub use app_window_message_handler::AppWindowMessageHandler;

View file

@ -5,15 +5,15 @@ use crate::messages::prelude::*;
pub enum BroadcastMessage {
// Sub-messages
#[child]
TriggerEvent(BroadcastEvent),
TriggerEvent(EventMessage),
// Messages
SubscribeEvent {
on: BroadcastEvent,
on: EventMessage,
send: Box<Message>,
},
UnsubscribeEvent {
on: BroadcastEvent,
message: Box<Message>,
on: EventMessage,
send: Box<Message>,
},
}

View file

@ -2,27 +2,24 @@ use crate::messages::prelude::*;
#[derive(Debug, Clone, Default, ExtractField)]
pub struct BroadcastMessageHandler {
listeners: HashMap<BroadcastEvent, Vec<Message>>,
event: EventMessageHandler,
listeners: HashMap<EventMessage, Vec<Message>>,
}
#[message_handler_data]
impl MessageHandler<BroadcastMessage, ()> for BroadcastMessageHandler {
fn process_message(&mut self, message: BroadcastMessage, responses: &mut VecDeque<Message>, _data: ()) {
fn process_message(&mut self, message: BroadcastMessage, responses: &mut VecDeque<Message>, _: ()) {
match message {
// Sub-messages
BroadcastMessage::TriggerEvent(event) => {
for message in self.listeners.entry(event).or_default() {
responses.add_front(message.clone())
}
}
BroadcastMessage::TriggerEvent(message) => self.event.process_message(message, responses, EventMessageContext { listeners: &mut self.listeners }),
// Messages
BroadcastMessage::SubscribeEvent { on, send } => self.listeners.entry(on).or_default().push(*send),
BroadcastMessage::UnsubscribeEvent { on, message } => self.listeners.entry(on).or_default().retain(|msg| *msg != *message),
BroadcastMessage::UnsubscribeEvent { on, send } => self.listeners.entry(on).or_default().retain(|msg| *msg != *send),
}
}
fn actions(&self) -> ActionList {
actions!(BroadcastEventDiscriminant;)
actions!(EventMessageDiscriminant;)
}
}

View file

@ -1,8 +1,8 @@
use crate::messages::prelude::*;
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, Hash)]
#[impl_message(Message, BroadcastMessage, TriggerEvent)]
pub enum BroadcastEvent {
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, Hash)]
pub enum EventMessage {
/// Triggered by requestAnimationFrame in JS
AnimationFrame,
CanvasTransformed,

View file

@ -0,0 +1,22 @@
use crate::messages::prelude::*;
#[derive(ExtractField)]
pub struct EventMessageContext<'a> {
pub listeners: &'a mut HashMap<EventMessage, Vec<Message>>,
}
#[derive(Debug, Clone, Default, ExtractField)]
pub struct EventMessageHandler {}
#[message_handler_data]
impl MessageHandler<EventMessage, EventMessageContext<'_>> for EventMessageHandler {
fn process_message(&mut self, message: EventMessage, responses: &mut VecDeque<Message>, context: EventMessageContext) {
for message in context.listeners.entry(message).or_default() {
responses.add_front(message.clone())
}
}
fn actions(&self) -> ActionList {
actions!(EventMessageDiscriminant;)
}
}

View file

@ -0,0 +1,7 @@
mod event_message;
mod event_message_handler;
#[doc(inline)]
pub use event_message::{EventMessage, EventMessageDiscriminant};
#[doc(inline)]
pub use event_message_handler::{EventMessageContext, EventMessageHandler};

View file

@ -1,7 +1,7 @@
mod broadcast_message;
mod broadcast_message_handler;
pub mod broadcast_event;
pub mod event;
#[doc(inline)]
pub use broadcast_message::{BroadcastMessage, BroadcastMessageDiscriminant};

View file

@ -8,7 +8,7 @@ pub struct DebugMessageHandler {
#[message_handler_data]
impl MessageHandler<DebugMessage, ()> for DebugMessageHandler {
fn process_message(&mut self, message: DebugMessage, responses: &mut VecDeque<Message>, _data: ()) {
fn process_message(&mut self, message: DebugMessage, responses: &mut VecDeque<Message>, _: ()) {
match message {
DebugMessage::ToggleTraceLogs => {
if log::max_level() == log::LevelFilter::Debug {

View file

@ -0,0 +1,11 @@
use crate::messages::prelude::*;
#[impl_message(Message, Defer)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum DeferMessage {
SetGraphSubmissionIndex { execution_id: u64 },
TriggerGraphRun { execution_id: u64, document_id: DocumentId },
AfterGraphRun { messages: Vec<Message> },
TriggerNavigationReady,
AfterNavigationReady { messages: Vec<Message> },
}

View file

@ -0,0 +1,57 @@
use crate::messages::prelude::*;
#[derive(ExtractField)]
pub struct DeferMessageContext<'a> {
pub portfolio: &'a PortfolioMessageHandler,
}
#[derive(Debug, Default, ExtractField)]
pub struct DeferMessageHandler {
after_graph_run: HashMap<DocumentId, Vec<(u64, Message)>>,
after_viewport_resize: Vec<Message>,
current_graph_submission_id: u64,
}
#[message_handler_data]
impl MessageHandler<DeferMessage, DeferMessageContext<'_>> for DeferMessageHandler {
fn process_message(&mut self, message: DeferMessage, responses: &mut VecDeque<Message>, context: DeferMessageContext) {
match message {
DeferMessage::AfterGraphRun { mut messages } => {
let after_graph_run = self.after_graph_run.entry(context.portfolio.active_document_id.unwrap_or(DocumentId(0))).or_default();
after_graph_run.extend(messages.drain(..).map(|m| (self.current_graph_submission_id, m)));
responses.add(NodeGraphMessage::RunDocumentGraph);
}
DeferMessage::AfterNavigationReady { messages } => {
self.after_viewport_resize.extend_from_slice(&messages);
}
DeferMessage::SetGraphSubmissionIndex { execution_id } => {
self.current_graph_submission_id = execution_id + 1;
}
DeferMessage::TriggerGraphRun { execution_id, document_id } => {
let after_graph_run = self.after_graph_run.entry(document_id).or_default();
if after_graph_run.is_empty() {
return;
}
// Find the index of the last message we can process
let split = after_graph_run.partition_point(|&(id, _)| id <= execution_id);
let elements = after_graph_run.drain(..split);
for (_, message) in elements.rev() {
responses.add_front(message);
}
for (&document_id, messages) in self.after_graph_run.iter() {
if !messages.is_empty() {
responses.add(PortfolioMessage::SubmitGraphRender { document_id, ignore_hash: false });
}
}
}
DeferMessage::TriggerNavigationReady => {
for message in self.after_viewport_resize.drain(..).rev() {
responses.add_front(message);
}
}
}
}
advertise_actions!(DeferMessageDiscriminant;
);
}

View file

@ -0,0 +1,7 @@
mod defer_message;
mod defer_message_handler;
#[doc(inline)]
pub use defer_message::{DeferMessage, DeferMessageDiscriminant};
#[doc(inline)]
pub use defer_message_handler::{DeferMessageContext, DeferMessageHandler};

View file

@ -33,6 +33,9 @@ pub enum DialogMessage {
RequestLicensesDialogWithLocalizedCommitDate {
localized_commit_year: String,
},
RequestLicensesThirdPartyDialogWithLicenseText {
license_text: String,
},
RequestNewDocumentDialog,
RequestPreferencesDialog,
}

View file

@ -1,10 +1,14 @@
use super::new_document_dialog::NewDocumentDialogMessageContext;
use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog, DemoArtworkDialog, LicensesDialog};
use crate::messages::dialog::simple_dialogs::LicensesThirdPartyDialog;
use crate::messages::input_mapper::utility_types::input_mouse::ViewportBounds;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
#[derive(ExtractField)]
pub struct DialogMessageData<'a> {
pub struct DialogMessageContext<'a> {
pub portfolio: &'a PortfolioMessageHandler,
pub viewport_bounds: &'a ViewportBounds,
pub preferences: &'a PreferencesMessageHandler,
}
@ -17,14 +21,18 @@ pub struct DialogMessageHandler {
}
#[message_handler_data]
impl MessageHandler<DialogMessage, DialogMessageData<'_>> for DialogMessageHandler {
fn process_message(&mut self, message: DialogMessage, responses: &mut VecDeque<Message>, data: DialogMessageData) {
let DialogMessageData { portfolio, preferences } = data;
impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHandler {
fn process_message(&mut self, message: DialogMessage, responses: &mut VecDeque<Message>, context: DialogMessageContext) {
let DialogMessageContext {
portfolio,
preferences,
viewport_bounds,
} = context;
match message {
DialogMessage::ExportDialog(message) => self.export_dialog.process_message(message, responses, ExportDialogMessageData { portfolio }),
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, responses, ()),
DialogMessage::PreferencesDialog(message) => self.preferences_dialog.process_message(message, responses, PreferencesDialogMessageData { preferences }),
DialogMessage::ExportDialog(message) => self.export_dialog.process_message(message, responses, ExportDialogMessageContext { portfolio }),
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, responses, NewDocumentDialogMessageContext { viewport_bounds }),
DialogMessage::PreferencesDialog(message) => self.preferences_dialog.process_message(message, responses, PreferencesDialogMessageContext { preferences }),
DialogMessage::CloseAllDocumentsWithConfirmation => {
let dialog = simple_dialogs::CloseAllDocumentsDialog {
@ -96,6 +104,10 @@ impl MessageHandler<DialogMessage, DialogMessageData<'_>> for DialogMessageHandl
dialog.send_dialog_to_frontend(responses);
}
DialogMessage::RequestLicensesThirdPartyDialogWithLicenseText { license_text } => {
let dialog = LicensesThirdPartyDialog { license_text };
dialog.send_dialog_to_frontend(responses);
}
DialogMessage::RequestNewDocumentDialog => {
self.new_document_dialog = NewDocumentDialogMessageHandler {
name: portfolio.generate_new_document_name(),

View file

@ -4,10 +4,10 @@ use crate::messages::prelude::*;
#[impl_message(Message, DialogMessage, ExportDialog)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum ExportDialogMessage {
FileType(FileType),
ScaleFactor(f64),
TransparentBackground(bool),
ExportBounds(ExportBounds),
FileType { file_type: FileType },
ScaleFactor { factor: f64 },
TransparentBackground { transparent: bool },
ExportBounds { bounds: ExportBounds },
Submit,
}

View file

@ -4,7 +4,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::prelude::*;
#[derive(ExtractField)]
pub struct ExportDialogMessageData<'a> {
pub struct ExportDialogMessageContext<'a> {
pub portfolio: &'a PortfolioMessageHandler,
}
@ -33,18 +33,18 @@ impl Default for ExportDialogMessageHandler {
}
#[message_handler_data]
impl MessageHandler<ExportDialogMessage, ExportDialogMessageData<'_>> for ExportDialogMessageHandler {
fn process_message(&mut self, message: ExportDialogMessage, responses: &mut VecDeque<Message>, data: ExportDialogMessageData) {
let ExportDialogMessageData { portfolio } = data;
impl MessageHandler<ExportDialogMessage, ExportDialogMessageContext<'_>> for ExportDialogMessageHandler {
fn process_message(&mut self, message: ExportDialogMessage, responses: &mut VecDeque<Message>, context: ExportDialogMessageContext) {
let ExportDialogMessageContext { portfolio } = context;
match message {
ExportDialogMessage::FileType(export_type) => self.file_type = export_type,
ExportDialogMessage::ScaleFactor(factor) => self.scale_factor = factor,
ExportDialogMessage::TransparentBackground(transparent_background) => self.transparent_background = transparent_background,
ExportDialogMessage::ExportBounds(export_area) => self.bounds = export_area,
ExportDialogMessage::FileType { file_type } => self.file_type = file_type,
ExportDialogMessage::ScaleFactor { factor } => self.scale_factor = factor,
ExportDialogMessage::TransparentBackground { transparent } => self.transparent_background = transparent,
ExportDialogMessage::ExportBounds { bounds } => self.bounds = bounds,
ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport {
file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
file_type: self.file_type,
scale_factor: self.scale_factor,
bounds: self.bounds,
@ -84,24 +84,28 @@ impl LayoutHolder for ExportDialogMessageHandler {
fn layout(&self) -> Layout {
let entries = [(FileType::Png, "PNG"), (FileType::Jpg, "JPG"), (FileType::Svg, "SVG")]
.into_iter()
.map(|(val, name)| RadioEntryData::new(format!("{val:?}")).label(name).on_update(move |_| ExportDialogMessage::FileType(val).into()))
.map(|(file_type, name)| {
RadioEntryData::new(format!("{file_type:?}"))
.label(name)
.on_update(move |_| ExportDialogMessage::FileType { file_type }.into())
})
.collect();
let export_type = vec![
TextLabel::new("File Type").table_align(true).min_width(100).widget_holder(),
TextLabel::new("File Type").table_align(true).min_width("100px").widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
RadioInput::new(entries).selected_index(Some(self.file_type as u32)).widget_holder(),
];
let resolution = vec![
TextLabel::new("Scale Factor").table_align(true).min_width(100).widget_holder(),
TextLabel::new("Scale Factor").table_align(true).min_width("100px").widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
NumberInput::new(Some(self.scale_factor))
.unit("")
.min(0.)
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.disabled(self.file_type == FileType::Svg)
.on_update(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor(number_input.value.unwrap()).into())
.on_update(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor { factor: number_input.value.unwrap() }.into())
.min_width(200)
.widget_holder(),
];
@ -111,24 +115,24 @@ impl LayoutHolder for ExportDialogMessageHandler {
(ExportBounds::Selection, "Selection".to_string(), !self.has_selection),
];
let artboards = self.artboards.iter().map(|(&layer, name)| (ExportBounds::Artboard(layer), name.to_string(), false)).collect();
let groups = [standard_bounds, artboards];
let choices = [standard_bounds, artboards];
let current_bounds = if !self.has_selection && self.bounds == ExportBounds::Selection {
ExportBounds::AllArtwork
} else {
self.bounds
};
let index = groups.iter().flatten().position(|(bounds, _, _)| *bounds == current_bounds).unwrap();
let index = choices.iter().flatten().position(|(bounds, _, _)| *bounds == current_bounds).unwrap();
let mut entries = groups
let mut entries = choices
.into_iter()
.map(|group| {
group
.map(|choice| {
choice
.into_iter()
.map(|(val, name, disabled)| {
MenuListEntry::new(format!("{val:?}"))
.map(|(bounds, name, disabled)| {
MenuListEntry::new(format!("{bounds:?}"))
.label(name)
.on_commit(move |_| ExportDialogMessage::ExportBounds(val).into())
.on_commit(move |_| ExportDialogMessage::ExportBounds { bounds }.into())
.disabled(disabled)
})
.collect::<Vec<_>>()
@ -140,19 +144,19 @@ impl LayoutHolder for ExportDialogMessageHandler {
}
let export_area = vec![
TextLabel::new("Bounds").table_align(true).min_width(100).widget_holder(),
TextLabel::new("Bounds").table_align(true).min_width("100px").widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_holder(),
];
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
let transparent_background = vec![
TextLabel::new("Transparency").table_align(true).min_width(100).for_checkbox(&mut checkbox_id).widget_holder(),
TextLabel::new("Transparency").table_align(true).min_width("100px").for_checkbox(checkbox_id).widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
CheckboxInput::new(self.transparent_background)
.disabled(self.file_type == FileType::Jpg)
.on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground(value.checked).into())
.for_label(checkbox_id.clone())
.on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground { transparent: value.checked }.into())
.for_label(checkbox_id)
.widget_holder(),
];

View file

@ -4,4 +4,4 @@ mod export_dialog_message_handler;
#[doc(inline)]
pub use export_dialog_message::{ExportDialogMessage, ExportDialogMessageDiscriminant};
#[doc(inline)]
pub use export_dialog_message_handler::{ExportDialogMessageData, ExportDialogMessageHandler};
pub use export_dialog_message_handler::{ExportDialogMessageContext, ExportDialogMessageHandler};

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