Merge branch 'master' into merge_segments

This commit is contained in:
Adesh Gupta 2025-12-22 14:11:58 +05:30 committed by GitHub
commit f732d3b123
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
969 changed files with 47431 additions and 44197 deletions

2
.branding Normal file
View file

@ -0,0 +1,2 @@
https://github.com/Keavon/graphite-branded-assets/archive/f8b02e68c92f5bbd27626bdd7a51102303b70a40.tar.gz
d06fd7b79fa9b7509c23072fa56745415fdc6eb98575d15214b0acc47ea4dd42

View file

@ -10,3 +10,6 @@ rustflags = [
"link-arg=--max-memory=4294967296",
"--cfg=web_sys_unstable_apis",
]
[env]
CARGO_WORKSPACE_DIR = { value = "", relative = true }

View file

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

View file

@ -7,7 +7,7 @@ on:
pull_request: {}
env:
CARGO_TERM_COLOR: always
INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="dev.graphite.rs" data-api="https://graphite.rs/visit/event" src="https://graphite.rs/visit/script.hash.js"></script>
INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="dev.graphite.art" data-api="https://graphite.art/visit/event" src="https://graphite.art/visit/script.hash.js"></script>
jobs:
build:
@ -34,10 +34,10 @@ jobs:
with:
node-version: "latest"
- name: 🚧 Install Node dependencies
- name: 🚧 Install build dependencies
run: |
cd frontend
npm ci
npm run setup
- name: 🦀 Install the latest Rust
run: |
@ -47,6 +47,11 @@ 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
run: |
# Remove the INDEX_HTML_HEAD_REPLACEMENT environment variable for build links (not master deploys)
@ -103,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'

17
.github/workflows/build-nix-package.yml vendored Normal file
View file

@ -0,0 +1,17 @@
name: Build Nix Package
on:
workflow_dispatch: {}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build Nix Package Dev
run: nix build .nix#graphite-dev --print-build-logs

View file

@ -18,7 +18,7 @@ jobs:
RUSTC_WRAPPER: /usr/bin/sccache
CARGO_INCREMENTAL: 0
SCCACHE_DIR: /var/lib/github-actions/.cache
INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="editor.graphite.rs" data-api="https://graphite.rs/visit/event" src="https://graphite.rs/visit/script.hash.js"></script>
INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="editor.graphite.art" data-api="https://graphite.art/visit/event" src="https://graphite.art/visit/script.hash.js"></script>
steps:
- name: 📥 Clone and checkout repository
@ -32,10 +32,10 @@ jobs:
with:
node-version: "latest"
- name: 🚧 Install Node dependencies
- name: 🚧 Install build dependencies
run: |
cd frontend
npm ci
npm run setup
- name: 🦀 Install the latest Rust
run: |

View file

@ -59,10 +59,10 @@ jobs:
with:
node-version: "latest"
- name: 🚧 Install Node dependencies
- name: 🚧 Install build dependencies
run: |
cd frontend
npm ci
npm run setup
- name: 🦀 Install the latest Rust
run: |

View file

@ -2,6 +2,10 @@ name: Profiling Changes
on:
pull_request:
paths:
- 'node-graph/**'
- 'Cargo.toml'
- 'Cargo.lock'
env:
CARGO_TERM_COLOR: always
@ -9,6 +13,7 @@ env:
jobs:
profile:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
with:
@ -33,12 +38,12 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.cargo/bin/iai-callgrind-runner
key: ${{ runner.os }}-iai-callgrind-runner-0.12.3
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: |
@ -49,21 +54,30 @@ jobs:
id: master-sha
run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Get CPU info
id: cpu-info
run: |
# Get CPU model and create a short hash for cache key
CPU_MODEL=$(cat /proc/cpuinfo | grep "model name" | head -1 | cut -d: -f2 | xargs)
CPU_HASH=$(echo "$CPU_MODEL" | sha256sum | cut -c1-8)
echo "cpu-hash=$CPU_HASH" >> $GITHUB_OUTPUT
echo "CPU: $CPU_MODEL (hash: $CPU_HASH)"
- 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 }}
key: ${{ runner.os }}-${{ runner.arch }}-${{ steps.cpu-info.outputs.cpu-hash }}-benchmark-baselines-master-${{ steps.master-sha.outputs.sha }}
restore-keys: |
${{ runner.os }}-benchmark-baselines-master-
${{ runner.os }}-${{ runner.arch }}-${{ steps.cpu-info.outputs.cpu-hash }}-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
@ -74,34 +88,18 @@ jobs:
git checkout ${{ github.event.pull_request.head.sha }}
- name: Run PR benchmarks
id: benchmark
run: |
# Compile benchmarks
COMPILE_OUTPUT=$(cargo bench --bench compile_demo_art_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g')
# Runtime benchmarks
UPDATE_OUTPUT=$(cargo bench --bench update_executor_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g')
RUN_ONCE_OUTPUT=$(cargo bench --bench run_once_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g')
RUN_CACHED_OUTPUT=$(cargo bench --bench run_cached_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g')
# Store outputs
echo "COMPILE_OUTPUT<<EOF" >> $GITHUB_OUTPUT
echo "$COMPILE_OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "UPDATE_OUTPUT<<EOF" >> $GITHUB_OUTPUT
echo "$UPDATE_OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "RUN_ONCE_OUTPUT<<EOF" >> $GITHUB_OUTPUT
echo "$RUN_ONCE_OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "RUN_CACHED_OUTPUT<<EOF" >> $GITHUB_OUTPUT
echo "$RUN_CACHED_OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
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
# Only run if we have write permissions (not a fork)
if: github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
@ -126,17 +124,79 @@ jobs:
});
}
- name: Analyze profiling changes
id: analyze
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
function isSignificantChange(diffPct, absoluteChange, benchmarkType) {
const meetsPercentageThreshold = Math.abs(diffPct) > 5;
const meetsAbsoluteThreshold = absoluteChange > 200000;
const isCachedExecution = benchmarkType === 'run_cached' ||
benchmarkType.includes('Cached Execution');
return isCachedExecution
? (meetsPercentageThreshold && meetsAbsoluteThreshold)
: meetsPercentageThreshold;
}
const allOutputs = [
JSON.parse(fs.readFileSync('/tmp/compile_output.json', 'utf8')),
JSON.parse(fs.readFileSync('/tmp/update_output.json', 'utf8')),
JSON.parse(fs.readFileSync('/tmp/run_once_output.json', 'utf8')),
JSON.parse(fs.readFileSync('/tmp/run_cached_output.json', 'utf8'))
];
const outputNames = ['compile', 'update', 'run_once', 'run_cached'];
const sectionTitles = ['Compilation', 'Update', 'Run Once', 'Cached Execution'];
let hasSignificantChanges = false;
let regressionDetails = [];
for (let i = 0; i < allOutputs.length; i++) {
const benchmarkOutput = allOutputs[i];
const outputName = outputNames[i];
const sectionTitle = sectionTitles[i];
for (const benchmark of benchmarkOutput) {
if (benchmark.profiles?.[0]?.summaries?.parts?.[0]?.metrics_summary?.Callgrind?.Ir?.diffs?.diff_pct) {
const diffPct = parseFloat(benchmark.profiles[0].summaries.parts[0].metrics_summary.Callgrind.Ir.diffs.diff_pct);
const oldValue = benchmark.profiles[0].summaries.parts[0].metrics_summary.Callgrind.Ir.metrics.Both[1].Int;
const newValue = benchmark.profiles[0].summaries.parts[0].metrics_summary.Callgrind.Ir.metrics.Both[0].Int;
const absoluteChange = Math.abs(newValue - oldValue);
if (isSignificantChange(diffPct, absoluteChange, outputName)) {
hasSignificantChanges = true;
regressionDetails.push({
module_path: benchmark.module_path,
id: benchmark.id,
diffPct,
absoluteChange,
sectionTitle
});
}
}
}
}
core.setOutput('has-significant-changes', hasSignificantChanges);
core.setOutput('regression-details', JSON.stringify(regressionDetails));
- name: Comment PR
if: github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const compileOutput = JSON.parse(`${{ steps.benchmark.outputs.COMPILE_OUTPUT }}`);
const updateOutput = JSON.parse(`${{ steps.benchmark.outputs.UPDATE_OUTPUT }}`);
const runOnceOutput = JSON.parse(`${{ steps.benchmark.outputs.RUN_ONCE_OUTPUT }}`);
const runCachedOutput = JSON.parse(`${{ steps.benchmark.outputs.RUN_CACHED_OUTPUT }}`);
let significantChanges = false;
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'));
const hasSignificantChanges = '${{ steps.analyze.outputs.has-significant-changes }}' === 'true';
let commentBody = "";
function formatNumber(num) {
@ -160,13 +220,31 @@ jobs:
let sectionBody = "";
let hasResults = false;
let hasSignificantChanges = false;
function isSignificantChange(diffPct, absoluteChange, benchmarkType) {
const meetsPercentageThreshold = Math.abs(diffPct) > 5;
const meetsAbsoluteThreshold = absoluteChange > 200000;
const isCachedExecution = benchmarkType === 'run_cached' ||
benchmarkType.includes('Cached Execution');
return isCachedExecution
? (meetsPercentageThreshold && meetsAbsoluteThreshold)
: meetsPercentageThreshold;
}
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) {
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";
@ -178,19 +256,23 @@ jobs:
sectionBody += "<details>\n<summary>Detailed metrics</summary>\n\n```\n";
sectionBody += `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)}`;
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;
if (isSignificantChange(irDiff.diff_pct, Math.abs(irDiff.new - irDiff.old), sectionTitle)) {
significantChanges = true;
hasSignificantChanges = true;
}
}
}
}
}
@ -236,7 +318,7 @@ jobs:
if (commentBody.length > 0) {
const output = `<details open>\n<summary>Performance Benchmark Results</summary>\n\n${commentBody}\n</details>`;
if (significantChanges) {
if (hasSignificantChanges) {
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
@ -250,3 +332,13 @@ jobs:
} else {
console.log("No benchmark results to display.");
}
- name: Fail on significant regressions
if: steps.analyze.outputs.has-significant-changes == 'true'
uses: actions/github-script@v7
with:
script: |
const regressionDetails = JSON.parse('${{ steps.analyze.outputs.regression-details }}');
const firstRegression = regressionDetails[0];
core.setFailed(`Significant performance regression detected: ${firstRegression.module_path} ${firstRegression.id} increased by ${firstRegression.absoluteChange.toLocaleString()} instructions (${firstRegression.diffPct.toFixed(2)}%)`);

View file

@ -38,6 +38,13 @@ jobs:
- name: 📦 Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.6
continue-on-error: true
- name: 🔧 Fallback if sccache fails
if: failure()
run: |
echo "sccache failed, disabling it"
echo "RUSTC_WRAPPER=" >> $GITHUB_ENV
- name: 🔬 Check Rust formatting
run: |
@ -56,4 +63,4 @@ jobs:
- name: 📈 Run sccache stat for check
shell: bash
run: sccache --show-stats
run: sccache --show-stats || echo "sccache stats unavailable"

View file

@ -12,7 +12,7 @@ on:
workflow_dispatch: {}
env:
CARGO_TERM_COLOR: always
INDEX_HTML_HEAD_INCLUSION: <script defer data-domain="graphite.rs" data-api="/visit/event" src="/visit/script.hash.js"></script>
INDEX_HTML_HEAD_INCLUSION: <script defer data-domain="graphite.art" data-api="/visit/event" src="/visit/script.hash.js"></script>
jobs:
build:
@ -26,6 +26,14 @@ jobs:
- name: 📥 Clone and checkout repository
uses: actions/checkout@v3
# We can remove this step once `ubuntu-latest` has Node.js 22 or newer for its native TypeScript support. See:
# https://github.com/actions/runner-images?tab=readme-ov-file#available-images
# https://nodejs.org/en/learn/typescript/run-natively
- name: 📦 Install the latest Node.js
uses: actions/setup-node@v4
with:
node-version: "latest"
- name: 🕸 Install Zola
uses: taiki-e/install-action@v2
with:
@ -63,55 +71,24 @@ jobs:
mkdir artifacts
mv hierarchical_message_system_tree.txt artifacts/hierarchical_message_system_tree.txt
- name: 🚚 Move `artifacts` contents to `website/other/editor-structure`
- name: 🚚 Move `artifacts` contents to the project root
run: |
mv artifacts/* website/other/editor-structure
mv artifacts/* .
- name: 🔧 Build auto-generated code docs artifacts into HTML
run: |
cd website/other/editor-structure
node generate.js hierarchical_message_system_tree.txt replacement.html
cd website
npm run generate-editor-structure
- name: 🌐 Build Graphite website with Zola
env:
MODE: prod
run: |
cd website
npm run install-fonts
npm ci
npm run lint
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: 📤 Publish to Cloudflare Pages
id: cloudflare
uses: cloudflare/pages-action@1

4
.gitignore vendored
View file

@ -1,4 +1,6 @@
branding/
target/
result/
*.spv
*.exrc
perf.data*
@ -7,4 +9,6 @@ profile.json.gz
flamegraph.svg
.idea/
.direnv
.DS_Store
hierarchical_message_system_tree.txt
hierarchical_message_system_tree.html

26
.nix/deps/cef.nix Normal file
View file

@ -0,0 +1,26 @@
{ pkgs, inputs, ... }:
let
cef = pkgs.cef-binary.overrideAttrs (_: _: {
postInstall = ''
strip $out/Release/*.so*
'';
});
cefPath = pkgs.runCommand "cef-path" {} ''
mkdir -p $out
ln -s ${cef}/include $out/include
find ${cef}/Release -name "*" -type f -exec ln -s {} $out/ \;
find ${cef}/Resources -name "*" -maxdepth 1 -exec ln -s {} $out/ \;
echo '${builtins.toJSON {
type = "minimal";
name = builtins.baseNameOf cef.src.url;
sha1 = "";
}}' > $out/archive.json
'';
in
{
env.CEF_PATH = cefPath;
}

5
.nix/deps/crane.nix Normal file
View file

@ -0,0 +1,5 @@
{ pkgs, inputs, ... }:
{
lib = inputs.crane.mkLib pkgs;
}

58
.nix/deps/rust-gpu.nix Normal file
View file

@ -0,0 +1,58 @@
{ pkgs, inputs, ... }:
let
extensions = [
"rust-src"
"rust-analyzer"
"clippy"
"cargo"
"rustc-dev"
"llvm-tools"
];
toolchain = pkgs.rust-bin.nightly."2025-06-23".default.override {
inherit extensions;
};
cargo = 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 ${toolchain}/bin/cargo ${"\${filtered_args[@]}"}
'';
rustc_codegen_spirv =
(pkgs.makeRustPlatform {
cargo = toolchain;
rustc = toolchain;
}).buildRustPackage
(finalAttrs: {
pname = "rustc_codegen_spirv";
version = "0-unstable-2025-08-04";
src = pkgs.fetchFromGitHub {
owner = "Firestar99";
repo = "rust-gpu-new";
rev = "c12f216121820580731440ee79ebc7403d6ea04f";
hash = "sha256-rG1cZvOV0vYb1dETOzzbJ0asYdE039UZImobXZfKIno=";
};
cargoHash = "sha256-AEigcEc5wiBd3zLqWN/2HSbkfOVFneAqNvg9HsouZf4=";
cargoBuildFlags = [
"-p"
"rustc_codegen_spirv"
"--features=use-compiled-tools"
"--no-default-features"
];
doCheck = false;
});
in
{
toolchain = toolchain;
env = {
RUST_GPU_PATH_OVERRIDE = "${cargo}/bin:${toolchain}/bin";
RUSTC_CODEGEN_SPIRV_PATH = "${rustc_codegen_spirv}/lib/librustc_codegen_spirv.so";
};
}

22
.nix/dev.nix Normal file
View file

@ -0,0 +1,22 @@
{
pkgs,
deps,
libs,
tools,
...
}:
pkgs.mkShell (
{
packages = tools.all ++ libs.all;
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath libs.all}:${deps.cef.env.CEF_PATH}";
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";
shellHook = ''
alias cargo='mold --run cargo'
'';
}
// deps.cef.env
// deps.rustGPU.env
)

28
.nix/flake.lock generated
View file

@ -1,5 +1,20 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1763938834,
"narHash": "sha256-j8iB0Yr4zAvQLueCZ5abxfk6fnG/SJ5JnGUziETjwfg=",
"owner": "ipetkov",
"repo": "crane",
"rev": "d9e753122e51cee64eb8d2dddfe11148f339f5a2",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-compat": {
"locked": {
"lastModified": 1733328505,
@ -34,11 +49,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1754214453,
"narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=",
"lastModified": 1764242076,
"narHash": "sha256-sKoIWfnijJ0+9e4wRvIgm/HgE27bzwQxcEmo2J/gNpI=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376",
"rev": "2fad6eac6077f03fe109c4d4eb171cf96791faa4",
"type": "github"
},
"original": {
@ -50,6 +65,7 @@
},
"root": {
"inputs": {
"crane": "crane",
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
@ -63,11 +79,11 @@
]
},
"locked": {
"lastModified": 1753238793,
"narHash": "sha256-jmQeEpgX+++MEgrcikcwoSiI7vDZWLP0gci7XiWb9uQ=",
"lastModified": 1764297505,
"narHash": "sha256-qrLpVu2/hA9Cu6IovMEsgh9YRyvmmWS+bSx7C1JGChA=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "0ad7ab4ca8e83febf147197e65c006dff60623ab",
"rev": "9623580f8ce09ec444b9aca107566ec5db110e62",
"type": "github"
},
"original": {

View file

@ -12,8 +12,6 @@
# - 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 = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
rust-overlay = {
@ -21,102 +19,141 @@
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
crane.url = "github:ipetkov/crane";
# 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, rust-overlay, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
outputs =
inputs:
inputs.flake-utils.lib.eachDefaultSystem (
system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
inherit system overlays;
};
rustc-wasm = pkgs.rust-bin.stable.latest.default.override {
targets = [ "wasm32-unknown-unknown" ];
extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ];
info = {
pname = "graphite";
version = "unstable";
src = ./..;
};
libcef = pkgs.libcef.overrideAttrs (finalAttrs: previousAttrs: {
version = "138.0.26";
gitRevision = "84f2d27";
chromiumVersion = "138.0.7204.158";
srcHash = "sha256-d9jQJX7rgdoHfROD3zmOdMSesRdKE3slB5ZV+U2wlbQ=";
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ (import inputs.rust-overlay) ];
};
__intentionallyOverridingVersion = true;
deps = {
crane = import ./deps/crane.nix { inherit pkgs inputs; };
cef = import ./deps/cef.nix { inherit pkgs inputs; };
rustGPU = import ./deps/rust-gpu.nix { inherit pkgs inputs; };
};
postInstall = ''
strip $out/lib/*
'';
});
libs = rec {
desktop = [
pkgs.wayland
pkgs.openssl
pkgs.vulkan-loader
pkgs.libraw
pkgs.libGL
];
desktop-x11 = [
pkgs.libxkbcommon
pkgs.xorg.libXcursor
pkgs.xorg.libxcb
pkgs.xorg.libX11
];
desktop-all = desktop ++ desktop-x11;
all = desktop-all;
};
libcefPath = pkgs.runCommand "libcef-path" {} ''
mkdir -p $out
tools = rec {
desktop = [
pkgs.pkg-config
];
frontend = [
pkgs.lld
pkgs.nodejs
pkgs.nodePackages.npm
pkgs.binaryen
pkgs.wasm-bindgen-cli_0_2_100
pkgs.wasm-pack
pkgs.cargo-about
];
dev = [
pkgs.rustc
pkgs.cargo
pkgs.rust-analyzer
pkgs.clippy
pkgs.rustfmt
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/
pkgs.git
echo '${builtins.toJSON {
type = "minimal";
name = builtins.baseNameOf libcef.src.url;
sha1 = "";
}}' > $out/archive.json
'';
pkgs.cargo-watch
pkgs.cargo-nextest
pkgs.cargo-expand
# Shared build inputs - system libraries that need to be in LD_LIBRARY_PATH
buildInputs = with pkgs; [
# System libraries
wayland
openssl
vulkan-loader
libraw
libGL
];
# Linker
pkgs.mold
# Development tools that don't need to be in LD_LIBRARY_PATH
buildTools = [
rustc-wasm
pkgs.nodejs
pkgs.nodePackages.npm
pkgs.binaryen
pkgs.wasm-bindgen-cli
pkgs.wasm-pack
pkgs.pkg-config
pkgs.git
pkgs.cargo-about
# Profiling tools
pkgs.gnuplot
pkgs.samply
pkgs.cargo-flamegraph
# Linker
pkgs.mold
];
# Development tools that don't need to be in LD_LIBRARY_PATH
devTools = with pkgs; [
cargo-watch
cargo-nextest
cargo-expand
# Profiling tools
gnuplot
samply
cargo-flamegraph
];
# Plotting tools
pkgs.graphviz
];
all = desktop ++ frontend ++ dev;
};
in
{
# Development shell configuration
devShells.default = pkgs.mkShell {
packages = buildInputs ++ buildTools ++ devTools;
packages = rec {
graphiteWithArgs =
args:
(import ./pkgs/graphite.nix {
pkgs = pkgs // {
inherit raster-nodes-shaders;
};
inherit
info
inputs
deps
libs
tools
;
})
args;
graphite = graphiteWithArgs { };
graphite-dev = graphiteWithArgs { dev = true; };
graphite-without-resources = graphiteWithArgs { embeddedResources = false; };
graphite-without-resources-dev = graphiteWithArgs {
embeddedResources = false;
dev = true;
};
#TODO: graphene-cli = import ./pkgs/graphene-cli.nix { inherit info pkgs inputs deps libs tools; };
raster-nodes-shaders = import ./pkgs/raster-nodes-shaders.nix {
inherit
info
pkgs
inputs
deps
libs
tools
;
};
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";
shellHook = ''
alias cargo='mold --run cargo'
'';
default = graphite;
};
devShells.default = import ./dev.nix {
inherit
pkgs
deps
libs
tools
;
};
formatter = pkgs.nixfmt-tree;
}
);
}

136
.nix/pkgs/graphite.nix Normal file
View file

@ -0,0 +1,136 @@
{
info,
pkgs,
inputs,
deps,
libs,
tools,
...
}:
{
embeddedResources ? true,
dev ? false,
}:
let
brandingTar = pkgs.fetchurl (
let
lockContent = builtins.readFile "${info.src}/.branding";
lines = builtins.filter (s: s != [ ]) (builtins.split "\n" lockContent);
url = builtins.elemAt lines 0;
hash = builtins.elemAt lines 1;
in
{
url = url;
sha256 = hash;
}
);
branding = pkgs.runCommand "${info.pname}-branding" { } ''
mkdir -p $out
tar -xvf ${brandingTar} -C $out --strip-components 1
'';
resourcesCommon = {
pname = "${info.pname}-resources";
inherit (info) version src;
strictDeps = true;
doCheck = false;
nativeBuildInputs = tools.frontend;
env.CARGO_PROFILE = if dev then "dev" else "release";
cargoExtraArgs = "--target wasm32-unknown-unknown -p graphite-wasm --no-default-features --features native";
};
resources = deps.crane.lib.buildPackage (
resourcesCommon
// {
cargoArtifacts = deps.crane.lib.buildDepsOnly resourcesCommon;
# TODO: Remove the need for this hash by using individual package resolutions and hashes from package-lock.json
npmDeps = pkgs.fetchNpmDeps {
inherit (info) pname version;
src = "${info.src}/frontend";
hash = "sha256-D8VCNK+Ca3gxO+5wriBn8FszG8/x8n/zM6/MPo9E2j4=";
};
npmRoot = "frontend";
npmConfigScript = "setup";
makeCacheWritable = true;
nativeBuildInputs = tools.frontend ++ [ pkgs.npmHooks.npmConfigHook ];
prePatch = ''
mkdir branding
cp -r ${branding}/* branding
cp ${info.src}/.branding branding/.branding
'';
buildPhase = ''
export HOME="$TMPDIR"
pushd frontend
npm run native:build-${if dev then "dev" else "production"}
popd
'';
installPhase = ''
mkdir -p $out
cp -r frontend/dist/* $out/
'';
}
);
common = {
inherit (info) pname version src;
strictDeps = true;
buildInputs = libs.desktop-all;
nativeBuildInputs = tools.desktop ++ [ pkgs.makeWrapper ];
env = deps.cef.env // {
CARGO_PROFILE = if dev then "dev" else "release";
};
cargoExtraArgs = "-p graphite-desktop${
if embeddedResources then "" else " --no-default-features --features recommended"
}";
doCheck = false;
};
in
deps.crane.lib.buildPackage (
common
// {
cargoArtifacts = deps.crane.lib.buildDepsOnly common;
env =
common.env
// {
RASTER_NODES_SHADER_PATH = pkgs.raster-nodes-shaders;
}
// (
if embeddedResources then
{
EMBEDDED_RESOURCES = resources;
}
else
{ }
);
postUnpack = ''
mkdir ./branding
cp -r ${branding}/* ./branding
'';
installPhase = ''
mkdir -p $out/bin
cp target/${if dev then "debug" else "release"}/graphite $out/bin/graphite
mkdir -p $out/share/applications
cp $src/desktop/assets/*.desktop $out/share/applications/
mkdir -p $out/share/icons/hicolor/scalable/apps
cp ${branding}/app-icons/graphite.svg $out/share/icons/hicolor/scalable/apps/
'';
postFixup = ''
wrapProgram "$out/bin/graphite" \
--prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath libs.desktop-all}:${deps.cef.env.CEF_PATH}" \
--set CEF_PATH "${deps.cef.env.CEF_PATH}"
'';
}
)

View file

@ -0,0 +1,36 @@
{
info,
pkgs,
inputs,
deps,
libs,
tools,
...
}:
(deps.crane.lib.overrideToolchain (_: deps.rustGPU.toolchain)).buildPackage {
pname = "raster-nodes-shaders";
inherit (info) version src;
cargoVendorDir = deps.crane.lib.vendorMultipleCargoDeps {
inherit (deps.crane.lib.findCargoFiles (deps.crane.lib.cleanCargoSource info.src)) cargoConfigs;
cargoLockList = [
"${info.src}/Cargo.lock"
"${deps.rustGPU.toolchain.passthru.availableComponents.rust-src}/lib/rustlib/src/rust/library/Cargo.lock"
];
};
strictDeps = true;
env = deps.rustGPU.env;
buildPhase = ''
cargo build -r -p raster-nodes-shaders
'';
installPhase = ''
cp target/spirv-builder/spirv-unknown-naga-wgsl/release/deps/raster_nodes_shaders_entrypoint.wgsl $out
'';
doCheck = false;
}

View file

@ -16,16 +16,13 @@
# > 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
(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,

3160
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,72 +1,132 @@
[workspace]
members = [
"editor",
"desktop",
"proc-macros",
"desktop/wrapper",
"desktop/embedded-resources",
"desktop/bundle",
"desktop/platform/linux",
"desktop/platform/mac",
"desktop/platform/win",
"editor",
"frontend/wasm",
"node-graph/gapplication-io",
"node-graph/gbrush",
"node-graph/gcore",
"node-graph/gcore-shaders",
"node-graph/gstd",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"libraries/dyn-any",
"libraries/path-bool",
"libraries/math-parser",
"node-graph/libraries/application-io",
"node-graph/libraries/core-types",
"node-graph/libraries/no-std-types",
"node-graph/libraries/raster-types",
"node-graph/libraries/vector-types",
"node-graph/libraries/graphic-types",
"node-graph/libraries/rendering",
"node-graph/libraries/wgpu-executor",
"node-graph/nodes/blending",
"node-graph/nodes/brush",
"node-graph/nodes/gcore",
"node-graph/nodes/graphic",
"node-graph/nodes/math",
"node-graph/nodes/path-bool",
"node-graph/nodes/raster",
"node-graph/nodes/raster/shaders",
"node-graph/nodes/raster/shaders/entrypoint",
"node-graph/nodes/text",
"node-graph/nodes/transform",
"node-graph/nodes/vector",
"node-graph/graph-craft",
"node-graph/graphene-cli",
"node-graph/graster-nodes",
"node-graph/gsvg-renderer",
"node-graph/nodes/gstd",
"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",
"proc-macros",
"tools/crate-hierarchy-viz"
]
default-members = [
"editor",
"frontend/wasm",
"node-graph/gbrush",
"node-graph/gcore",
"node-graph/gcore-shaders",
"node-graph/gstd",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"libraries/dyn-any",
"libraries/path-bool",
"libraries/math-parser",
"node-graph/libraries/application-io",
"node-graph/libraries/core-types",
"node-graph/libraries/no-std-types",
"node-graph/libraries/raster-types",
"node-graph/libraries/vector-types",
"node-graph/libraries/graphic-types",
"node-graph/libraries/rendering",
"node-graph/libraries/wgpu-executor",
"node-graph/nodes/blending",
"node-graph/nodes/brush",
"node-graph/nodes/gcore",
"node-graph/nodes/graphic",
"node-graph/nodes/math",
"node-graph/nodes/path-bool",
"node-graph/nodes/raster",
"node-graph/nodes/raster/shaders",
"node-graph/nodes/text",
"node-graph/nodes/transform",
"node-graph/nodes/vector",
"node-graph/graph-craft",
"node-graph/graphene-cli",
"node-graph/graster-nodes",
"node-graph/gsvg-renderer",
"node-graph/nodes/gstd",
"node-graph/interpreted-executor",
"node-graph/node-macro",
"node-graph/preprocessor",
# blocked by https://github.com/rust-lang/cargo/issues/15890
# "proc-macros",
]
resolver = "2"
[workspace.package]
rust-version = "1.88"
edition = "2024"
authors = ["Graphite Authors <contact@graphite.art>"]
homepage = "https://graphite.art"
repository = "https://github.com/GraphiteEditor/Graphite"
license = "Apache-2.0"
version = "0.0.0"
readme = "README.md"
publish = false
[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" }
graphene-application-io = { path = "node-graph/libraries/application-io" }
core-types = { path = "node-graph/libraries/core-types" }
no-std-types = { path = "node-graph/libraries/no-std-types" }
raster-types = { path = "node-graph/libraries/raster-types" }
vector-types = { path = "node-graph/libraries/vector-types" }
graphic-types = { path = "node-graph/libraries/graphic-types" }
rendering = { path = "node-graph/libraries/rendering" }
brush-nodes = { path = "node-graph/nodes/brush" }
blending-nodes = { path = "node-graph/nodes/blending" }
graphene-core = { path = "node-graph/nodes/gcore" }
graphic-nodes = { path = "node-graph/nodes/graphic" }
text-nodes = { path = "node-graph/nodes/text" }
transform-nodes = { path = "node-graph/nodes/transform" }
vector-nodes = { path = "node-graph/nodes/vector" }
math-nodes = { path = "node-graph/nodes/math" }
path-bool-nodes = { path = "node-graph/nodes/path-bool" }
graph-craft = { path = "node-graph/graph-craft" }
graphene-raster-nodes = { path = "node-graph/graster-nodes" }
graphene-std = { path = "node-graph/gstd" }
graphene-svg-renderer = { path = "node-graph/gsvg-renderer" }
raster-nodes = { path = "node-graph/nodes/raster" }
graphene-std = { path = "node-graph/nodes/gstd" }
interpreted-executor = { path = "node-graph/interpreted-executor" }
node-macro = { path = "node-graph/node-macro" }
wgpu-executor = { path = "node-graph/wgpu-executor" }
wgpu-executor = { path = "node-graph/libraries/wgpu-executor" }
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"
@ -76,17 +136,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 = "25.0.2", features = [
wgpu = { version = "27.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",
@ -114,24 +173,34 @@ web-sys = { version = "=0.3.77", features = [
"HtmlImageElement",
"ImageBitmapRenderingContext",
] }
winit = { version = "0.30", features = ["wayland", "rwh_06"] }
winit = { git = "https://github.com/rust-windowing/winit.git" }
keyboard-types = "0.8"
url = "2.5"
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt"] }
vello = { git = "https://github.com/linebender/vello.git" } # TODO switch back to stable when a release is made
resvg = "0.44"
usvg = "0.44"
vello = { git = "https://github.com/linebender/vello" }
vello_encoding = { git = "https://github.com/linebender/vello" }
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.6"
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",
@ -148,32 +217,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"] }
kurbo = { version = "0.12", 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"
strum = { version = "0.26.3", features = ["derive"] }
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 = "138.5.0"
include_dir = "0.7.4"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracing = "0.1.41"
rfd = "0.15.4"
open = "5.3.2"
cef = "142"
cef-dll-sys = "142"
include_dir = "0.7"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing = "0.1"
rfd = "0.15"
open = "5.3"
polycool = "0.4"
spin = "0.10"
clap = "4.5"
spirv-std = { git = "https://github.com/Firestar99/rust-gpu-new", rev = "c12f216121820580731440ee79ebc7403d6ea04f", features = ["bytemuck"] }
cargo-gpu = { git = "https://github.com/Firestar99/cargo-gpu", rev = "3952a22d16edbd38689f3a876e417899f21e1fe7", default-features = false }
[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 }
no-std-types = { opt-level = 1 }
core-types= { 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
graphite-proc-macros = { opt-level = 1 }
image = { opt-level = 2 }
@ -181,6 +256,7 @@ rustc-hash = { opt-level = 3 }
serde_derive = { opt-level = 1 }
specta-macros = { opt-level = 1 }
syn = { opt-level = 1 }
node-macro = { opt-level = 2 }
[profile.release]
lto = "thin"
@ -189,3 +265,7 @@ debug = true
[profile.profiling]
inherits = "release"
debug = true
[patch.crates-io]
# Force cargo to use only one version of the dpi crate (vendoring breaks without this)
dpi = { git = "https://github.com/rust-windowing/winit.git" }

View file

@ -1,6 +1,6 @@
<a href="https://graphite.rs/">
<a href="https://graphite.art/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/9366c148-4405-484f-909a-9a3526eb9209">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/791508ab-bcd5-4e31-a3b9-1187cfd7a2f6">
@ -10,14 +10,14 @@
# Your procedural toolbox for 2D content creation
**Graphite is a free, open source vector and raster graphics engine, [available now](https://editor.graphite.rs) in alpha. Get creative with a fully nondestructive editing workflow that combines layer-based compositing with node-based generative design.**
**Graphite is a free, open source vector and raster graphics engine, [available now](https://editor.graphite.art) in alpha. Get creative with a fully nondestructive editing workflow that combines layer-based compositing with node-based generative design.**
Having begun life as a vector editor, Graphite continues evolving into a generalized, all-in-one graphics toolbox that's built more like a game engine than a conventional creative app. The editor's tools wrap its node graph core, providing user-friendly workflows for vector, raster, and beyond. Photo editing, motion graphics, digital painting, desktop publishing, and VFX compositing are additional competencies on the planned [roadmap](https://graphite.rs/features/#roadmap) making Graphite into a highly versatile content creation tool.
Having begun life as a vector editor, Graphite continues evolving into a generalized, all-in-one graphics toolbox that's built more like a game engine than a conventional creative app. The editor's tools wrap its node graph core, providing user-friendly workflows for vector, raster, and beyond. Photo editing, motion graphics, digital painting, desktop publishing, and VFX compositing are additional competencies on the planned [roadmap](https://graphite.art/features/#roadmap) making Graphite into a highly versatile content creation tool.
Learn more from the [website](https://graphite.rs/), subscribe to the [newsletter](https://graphite.rs/#newsletter), consider [volunteering](https://graphite.rs/volunteer/) or [donating](https://graphite.rs/donate/), and remember to give this repository a ⭐!
Learn more from the [website](https://graphite.art/), subscribe to the [newsletter](https://graphite.art/#newsletter), consider [volunteering](https://graphite.art/volunteer/) or [donating](https://graphite.art/donate/), and remember to give this repository a ⭐!
<br />
<a href="https://discord.graphite.rs/">
<a href="https://discord.graphite.art/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/ad185fac-3b48-446d-863c-2bcb0724abee">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/aa23f503-f3bf-444a-9080-8eaa19fa2fa8">
@ -62,7 +62,7 @@ https://github.com/user-attachments/assets/f4604aea-e8f1-45ce-9218-46ddc666f11d
## Support our mission ❤️
Graphite is 100% community built and funded. Please become a part of keeping the project alive and thriving with a [donation](https://graphite.rs/donate/) if you share a belief in our **mission**:
Graphite is 100% community built and funded. Please become a part of keeping the project alive and thriving with a [donation](https://graphite.art/donate/) if you share a belief in our **mission**:
> Graphite strives to unshackle the creativity of every budding artist and seasoned professional by building the best comprehensive art and design tool that's accessible to all.
>
@ -78,6 +78,6 @@ Graphite is 100% community built and funded. Please become a part of keeping the
## Contributing/building the code
Are you a graphics programmer or Rust developer? Graphite aims to be one of the most approachable projects for putting your engineering skills to use in the world of open source. See [instructions here](https://graphite.rs/volunteer/guide/) for setting up the project and getting started.
Are you a graphics programmer or Rust developer? Graphite aims to be one of the most approachable projects for putting your engineering skills to use in the world of open source. See [instructions here](https://graphite.art/volunteer/guide/) for setting up the project and getting started.
*By submitting code for inclusion in the project, you are agreeing to license your changes under the Apache 2.0 license, and that you have the authority to do so. Some directories may have other licenses, like dual-licensed MIT/Apache 2.0, and code submissions to those directories mean you agree to the applicable license(s).*

View file

@ -1,22 +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",
"bzip2-1.0.6",
"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

@ -39,11 +39,7 @@ db-urls = ["https://github.com/rustsec/advisory-db"]
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
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
@ -63,25 +59,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",
"bzip2-1.0.6",
"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

View file

@ -2,40 +2,68 @@
name = "graphite-desktop"
version = "0.1.0"
description = "Graphite Desktop"
authors = ["Graphite Authors <contact@graphite.rs>"]
authors = ["Graphite Authors <contact@graphite.art>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[[bin]]
name = "graphite"
path = "src/main.rs"
[features]
default = ["gpu"]
gpu = ["graphite-editor/gpu"]
default = ["recommended", "embedded_resources"]
recommended = ["gpu", "accelerated_paint"]
embedded_resources = ["dep:graphite-desktop-embedded-resources"]
gpu = ["graphite-desktop-wrapper/gpu"]
accelerated_paint = ["cef/accelerated_osr"]
[dependencies]
# # Local dependencies
graphite-editor = { path = "../editor", features = [
"gpu",
"ron",
"vello",
] }
graphene-std = { workspace = true }
graph-craft = { workspace = true }
wgpu-executor = { workspace = true }
# Local dependencies
graphite-desktop-wrapper = { path = "wrapper" }
graphite-desktop-embedded-resources = { path = "embedded-resources", optional = true }
wgpu = { workspace = true }
winit = { workspace = true, features = ["serde"] }
winit = { workspace = true, features = [ "wayland-csd-adwaita-notitlebar", "serde" ] }
thiserror = { workspace = true }
futures = { workspace = true }
cef = { workspace = true }
include_dir = { workspace = true }
cef-dll-sys = { workspace = true }
tracing-subscriber = { workspace = true }
tracing = { workspace = true }
dirs = { workspace = true }
ron = { workspace = true}
ron = { workspace = true }
bytemuck = { workspace = true }
glam = { workspace = true }
vello = { workspace = true }
derivative = { workspace = true }
rfd = { workspace = true }
open = { workspace = true }
rand = { workspace = true, features = ["thread_rng"] }
serde = { workspace = true }
clap = { workspace = true, features = ["derive"] }
pidlock = "0.2.2"
ctrlc = "3.5.1"
window_clipboard = "0.5"
# Windows-specific dependencies
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.58.0", features = [
"Win32_Foundation",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
"Win32_System_Com",
"Win32_UI_Controls",
"Win32_UI_WindowsAndMessaging",
"Win32_UI_HiDpi",
"Win32_UI_Shell",
] }
# macOS-specific dependencies
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = { version = "0.6.1", default-features = false }
objc2-foundation = { version = "0.3.2", default-features = false }
objc2-app-kit = { version = "0.3.2", default-features = false }
muda = { git = "https://github.com/tauri-apps/muda.git", rev = "3f460b8fbaed59cda6d95ceea6904f000f093f15", default-features = false }

View file

@ -2,10 +2,10 @@
Name=Graphite
GenericName=Vector & Raster Graphics Editor
Comment=Open-source vector & raster graphics editor. Featuring node based procedural nondestructive editing workflow.
Exec=graphite-editor
Exec=graphite
Terminal=false
Type=Application
Icon=graphite-icon-color
Icon=graphite
Categories=Graphics;VectorGraphics;RasterGraphics;
Keywords=graphite;editor;vector;raster;procedural;design;
StartupWMClass=rs.graphite.GraphiteEditor
StartupWMClass=art.graphite.Graphite

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

View file

@ -1,9 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 4.1 KiB

16
desktop/bundle/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "graphite-desktop-bundle"
version = "0.0.0"
description = "Graphite Desktop Bundle"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[dependencies]
cef-dll-sys = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
serde = { workspace = true }
plist = { version = "*" }

10
desktop/bundle/build.rs Normal file
View file

@ -0,0 +1,10 @@
fn main() {
println!("cargo:rerun-if-env-changed=CARGO_PROFILE");
println!("cargo:rerun-if-env-changed=PROFILE");
let profile = std::env::var("CARGO_PROFILE").or_else(|_| std::env::var("PROFILE")).unwrap();
println!("cargo:rustc-env=CARGO_PROFILE={profile}");
println!("cargo:rerun-if-env-changed=DEP_CEF_DLL_WRAPPER_CEF_DIR");
let cef_dir = std::env::var("DEP_CEF_DLL_WRAPPER_CEF_DIR").unwrap();
println!("cargo:rustc-env=CEF_PATH={cef_dir}");
}

View file

@ -0,0 +1,71 @@
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
pub(crate) const APP_NAME: &str = "Graphite";
pub(crate) const APP_BIN: &str = "graphite";
pub(crate) fn workspace_path() -> PathBuf {
PathBuf::from(env!("CARGO_WORKSPACE_DIR"))
}
fn profile_name() -> &'static str {
let mut profile = env!("CARGO_PROFILE");
if profile == "debug" {
profile = "dev";
}
profile
}
pub(crate) fn profile_path() -> PathBuf {
workspace_path().join(format!("target/{}", env!("CARGO_PROFILE")))
}
pub(crate) fn cef_path() -> PathBuf {
PathBuf::from(env!("CEF_PATH"))
}
pub(crate) fn build_bin(package: &str, bin: Option<&str>) -> Result<PathBuf, Box<dyn Error>> {
let profile = &profile_name();
let mut args = vec!["build", "--package", package, "--profile", profile];
if let Some(bin) = bin {
args.push("--bin");
args.push(bin);
}
run_command("cargo", &args)?;
let profile_path = profile_path();
let mut bin_path = if let Some(bin) = bin { profile_path.join(bin) } else { profile_path.join(APP_BIN) };
if cfg!(target_os = "windows") {
bin_path.set_extension("exe");
}
Ok(bin_path)
}
pub(crate) fn run_command(program: &str, args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
let status = Command::new(program).args(args).stdout(Stdio::inherit()).stderr(Stdio::inherit()).status()?;
if !status.success() {
std::process::exit(1);
}
Ok(())
}
pub(crate) fn clean_dir(dir: &Path) {
if dir.exists() {
fs::remove_dir_all(dir).unwrap();
}
fs::create_dir_all(dir).unwrap();
}
pub(crate) fn copy_dir(src: &Path, dst: &Path) {
fs::create_dir_all(dst).unwrap();
for entry in fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let dst_path = dst.join(entry.file_name());
if entry.file_type().unwrap().is_dir() {
copy_dir(&entry.path(), &dst_path);
} else {
fs::copy(entry.path(), &dst_path).unwrap();
}
}
}

View file

@ -0,0 +1,21 @@
use std::error::Error;
use crate::common::*;
pub fn main() -> Result<(), Box<dyn Error>> {
let app_bin = build_bin("graphite-desktop-platform-linux", None)?;
// TODO: Implement bundling for linux
// TODO: Consider adding more useful cli
if std::env::args().any(|a| a == "open") {
run_command(&app_bin.to_string_lossy(), &[]).expect("failed to open app");
} else {
println!("Binary built and placed at {}", app_bin.to_string_lossy());
eprintln!("Bundling for Linux is not yet implemented.");
eprintln!("You can still start the app with the `open` subcommand. `cargo run -p graphite-desktop-bundle -- open`");
std::process::exit(1);
}
Ok(())
}

127
desktop/bundle/src/mac.rs Normal file
View file

@ -0,0 +1,127 @@
use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
use crate::common::*;
const APP_ID: &str = "art.graphite.Graphite";
const ICONS_FILE_NAME: &str = "graphite.icns";
const EXEC_PATH: &str = "Contents/MacOS";
const FRAMEWORKS_PATH: &str = "Contents/Frameworks";
const RESOURCES_PATH: &str = "Contents/Resources";
const CEF_FRAMEWORK: &str = "Chromium Embedded Framework.framework";
pub fn main() -> Result<(), Box<dyn Error>> {
let app_bin = build_bin("graphite-desktop-platform-mac", None)?;
let helper_bin = build_bin("graphite-desktop-platform-mac", Some("helper"))?;
let profile_path = profile_path();
let app_dir = bundle(&profile_path, &app_bin, &helper_bin);
// TODO: Consider adding more useful cli
if std::env::args().any(|a| a == "open") {
let executable_path = app_dir.join(EXEC_PATH).join(APP_NAME);
run_command(&executable_path.to_string_lossy(), &[]).expect("failed to open app");
}
Ok(())
}
fn bundle(out_dir: &Path, app_bin: &Path, helper_bin: &Path) -> PathBuf {
let app_dir = out_dir.join(APP_NAME).with_extension("app");
clean_dir(&app_dir);
create_app(&app_dir, APP_ID, APP_NAME, app_bin, false);
for helper_type in [None, Some("GPU"), Some("Renderer")] {
let helper_id_suffix = helper_type.map(|t| format!(".{t}")).unwrap_or_default();
let helper_id = format!("{APP_ID}.helper{helper_id_suffix}");
let helper_name_suffix = helper_type.map(|t| format!(" ({t})")).unwrap_or_default();
let helper_name = format!("{APP_NAME} Helper{helper_name_suffix}");
let helper_app_dir = app_dir.join(FRAMEWORKS_PATH).join(&helper_name).with_extension("app");
create_app(&helper_app_dir, &helper_id, &helper_name, helper_bin, true);
}
copy_dir(&cef_path().join(CEF_FRAMEWORK), &app_dir.join(FRAMEWORKS_PATH).join(CEF_FRAMEWORK));
let resource_dir = app_dir.join(RESOURCES_PATH);
fs::create_dir_all(&resource_dir).expect("failed to create app resource dir");
let icon_file = workspace_path().join("branding/app-icons").join(ICONS_FILE_NAME);
fs::copy(icon_file, resource_dir.join(ICONS_FILE_NAME)).expect("failed to copy icon file");
app_dir
}
fn create_app(app_dir: &Path, id: &str, name: &str, bin: &Path, is_helper: bool) {
fs::create_dir_all(app_dir.join(EXEC_PATH)).unwrap();
let app_contents_dir: &Path = &app_dir.join("Contents");
create_info_plist(app_contents_dir, id, name, is_helper).unwrap();
fs::copy(bin, app_dir.join(EXEC_PATH).join(name)).unwrap();
}
fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) -> Result<(), Box<dyn std::error::Error>> {
let info = InfoPlist {
cf_bundle_name: exec_name.to_string(),
cf_bundle_identifier: id.to_string(),
cf_bundle_display_name: exec_name.to_string(),
cf_bundle_executable: exec_name.to_string(),
cf_bundle_icon_file: ICONS_FILE_NAME.to_string(),
cf_bundle_info_dictionary_version: "6.0".to_string(),
cf_bundle_package_type: "APPL".to_string(),
cf_bundle_signature: "????".to_string(),
cf_bundle_version: "0.0.0".to_string(),
cf_bundle_short_version_string: "0.0".to_string(),
cf_bundle_development_region: "en".to_string(),
ls_environment: [("MallocNanoZone".to_string(), "0".to_string())].iter().cloned().collect(),
ls_file_quarantine_enabled: true,
ls_minimum_system_version: "11.0".to_string(),
ls_ui_element: if is_helper { Some("1".to_string()) } else { None },
ns_supports_automatic_graphics_switching: true,
};
let plist_file = dir.join("Info.plist");
plist::to_file_xml(plist_file, &info)?;
Ok(())
}
#[derive(serde::Serialize)]
struct InfoPlist {
#[serde(rename = "CFBundleName")]
cf_bundle_name: String,
#[serde(rename = "CFBundleIdentifier")]
cf_bundle_identifier: String,
#[serde(rename = "CFBundleDisplayName")]
cf_bundle_display_name: String,
#[serde(rename = "CFBundleExecutable")]
cf_bundle_executable: String,
#[serde(rename = "CFBundleIconFile")]
cf_bundle_icon_file: String,
#[serde(rename = "CFBundleInfoDictionaryVersion")]
cf_bundle_info_dictionary_version: String,
#[serde(rename = "CFBundlePackageType")]
cf_bundle_package_type: String,
#[serde(rename = "CFBundleSignature")]
cf_bundle_signature: String,
#[serde(rename = "CFBundleVersion")]
cf_bundle_version: String,
#[serde(rename = "CFBundleShortVersionString")]
cf_bundle_short_version_string: String,
#[serde(rename = "CFBundleDevelopmentRegion")]
cf_bundle_development_region: String,
#[serde(rename = "LSEnvironment")]
ls_environment: HashMap<String, String>,
#[serde(rename = "LSFileQuarantineEnabled")]
ls_file_quarantine_enabled: bool,
#[serde(rename = "LSMinimumSystemVersion")]
ls_minimum_system_version: String,
#[serde(rename = "LSUIElement")]
ls_ui_element: Option<String>,
#[serde(rename = "NSSupportsAutomaticGraphicsSwitching")]
ns_supports_automatic_graphics_switching: bool,
}

View file

@ -0,0 +1,17 @@
mod common;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod mac;
#[cfg(target_os = "windows")]
mod win;
fn main() {
#[cfg(target_os = "linux")]
linux::main().unwrap();
#[cfg(target_os = "macos")]
mac::main().unwrap();
#[cfg(target_os = "windows")]
win::main().unwrap();
}

34
desktop/bundle/src/win.rs Normal file
View file

@ -0,0 +1,34 @@
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
use crate::common::*;
const EXECUTABLE: &str = "Graphite.exe";
pub fn main() -> Result<(), Box<dyn Error>> {
let app_bin = build_bin("graphite-desktop-platform-win", None)?;
let executable = bundle(&profile_path(), &app_bin);
// TODO: Consider adding more useful cli
if std::env::args().any(|a| a == "open") {
let executable_path = executable.to_string_lossy();
run_command(&executable_path, &[]).expect("failed to open app")
}
Ok(())
}
fn bundle(out_dir: &Path, app_bin: &Path) -> PathBuf {
let app_dir = out_dir.join(APP_NAME);
clean_dir(&app_dir);
copy_dir(&cef_path(), &app_dir);
let bin_path = app_dir.join(EXECUTABLE);
fs::copy(app_bin, &bin_path).unwrap();
bin_path
}

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.art>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[dependencies]
include_dir = { workspace = true }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(embedded_resources)'] }

View file

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

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;

View file

@ -0,0 +1,16 @@
[package]
name = "graphite-desktop-platform-linux"
version = "0.0.0"
description = "Graphite Desktop Platform Linux"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[[bin]]
name = "graphite"
path = "src/main.rs"
[dependencies]
graphite-desktop = { path = "../.." }

View file

@ -0,0 +1,3 @@
fn main() {
graphite_desktop::start();
}

View file

@ -0,0 +1,20 @@
[package]
name = "graphite-desktop-platform-mac"
version = "0.0.0"
description = "Graphite Desktop Platform Mac"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[[bin]]
name = "graphite"
path = "src/main.rs"
[[bin]]
name = "helper"
path = "src/helper.rs"
[dependencies]
graphite-desktop = { path = "../.." }

View file

@ -0,0 +1,3 @@
fn main() {
graphite_desktop::start_helper();
}

View file

@ -0,0 +1,3 @@
fn main() {
graphite_desktop::start();
}

View file

@ -0,0 +1,19 @@
[package]
name = "graphite-desktop-platform-win"
version = "0.0.0"
description = "Graphite Desktop Platform Windows"
authors = ["Graphite Authors <contact@graphite.art>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[[bin]]
name = "graphite"
path = "src/main.rs"
[dependencies]
graphite-desktop = { path = "../.." }
[target.'cfg(target_os = "windows")'.build-dependencies]
winres = "0.1"

View file

@ -0,0 +1,31 @@
fn main() {
#[cfg(target_os = "windows")]
{
let mut res = winres::WindowsResource::new();
res.set_icon("../../../branding/app-icons/graphite.ico");
res.set_language(0x0409); // English (US)
// TODO: Replace with actual version
res.set_version_info(winres::VersionInfo::FILEVERSION, {
const MAJOR: u64 = 0;
const MINOR: u64 = 0;
const PATCH: u64 = 0;
const RELEASE: u64 = 0;
(MAJOR << 48) | (MINOR << 32) | (PATCH << 16) | RELEASE
});
res.set("FileVersion", "0.0.0.0");
res.set("ProductVersion", "0.0.0.0");
res.set("OriginalFilename", "Graphite.exe");
res.set("FileDescription", "Graphite");
res.set("ProductName", "Graphite");
res.set("LegalCopyright", "Copyright © 2025 Graphite Labs, LLC");
res.set("CompanyName", "Graphite Labs, LLC");
res.compile().expect("Failed to compile Windows resources");
}
}

View file

@ -0,0 +1,4 @@
#![windows_subsystem = "windows"]
fn main() {
graphite_desktop::start();
}

View file

@ -1,291 +1,559 @@
use crate::CustomEvent;
use crate::WindowSize;
use crate::consts::APP_NAME;
use crate::dialogs::dialog_open_graphite_file;
use crate::dialogs::dialog_save_file;
use crate::dialogs::dialog_save_graphite_file;
use crate::render::GraphicsState;
use crate::render::WgpuContext;
use graph_craft::wasm_application_io::WasmApplicationIo;
use graphite_editor::application::Editor;
use graphite_editor::messages::prelude::*;
use std::sync::Arc;
use std::sync::mpsc::Sender;
use rfd::AsyncFileDialog;
use std::fs;
use std::path::PathBuf;
use std::sync::mpsc::{Receiver, Sender, SyncSender};
use std::thread;
use std::time::Duration;
use std::time::Instant;
use std::time::{Duration, Instant};
use winit::application::ApplicationHandler;
use winit::dpi::PhysicalSize;
use winit::event::StartCause;
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::event::{ButtonSource, ElementState, MouseButton, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::window::WindowId;
use crate::cef;
use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS;
use crate::event::{AppEvent, AppEventScheduler};
use crate::persist::PersistentData;
use crate::render::{RenderError, RenderState};
use crate::window::Window;
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Platform};
use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
pub(crate) struct WinitApp {
cef_context: cef::Context<cef::Initialized>,
window: Option<Arc<Window>>,
cef_schedule: Option<Instant>,
window_size_sender: Sender<WindowSize>,
graphics_state: Option<GraphicsState>,
pub(crate) struct App {
render_state: Option<RenderState>,
wgpu_context: WgpuContext,
event_loop_proxy: EventLoopProxy<CustomEvent>,
editor: Editor,
window: Option<Window>,
window_scale: f64,
window_size: PhysicalSize<u32>,
window_maximized: bool,
window_fullscreen: bool,
ui_scale: f64,
app_event_receiver: Receiver<AppEvent>,
app_event_scheduler: AppEventScheduler,
desktop_wrapper: DesktopWrapper,
cef_context: Box<dyn cef::CefContext>,
cef_schedule: Option<Instant>,
cef_view_info_sender: Sender<cef::ViewInfoUpdate>,
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,
launch_documents: Vec<PathBuf>,
}
impl WinitApp {
pub(crate) fn new(cef_context: cef::Context<cef::Initialized>, window_size_sender: Sender<WindowSize>, wgpu_context: WgpuContext, event_loop_proxy: EventLoopProxy<CustomEvent>) -> Self {
Self {
cef_context,
window: None,
cef_schedule: Some(Instant::now()),
graphics_state: None,
window_size_sender,
wgpu_context,
event_loop_proxy,
editor: Editor::new(),
}
impl App {
pub(crate) fn init() {
Window::init();
}
fn dispatch_message(&mut self, message: Message) {
let responses = self.editor.handle_message(message);
self.send_messages_to_editor(responses);
}
pub(crate) fn new(
cef_context: Box<dyn cef::CefContext>,
cef_view_info_sender: Sender<cef::ViewInfoUpdate>,
wgpu_context: WgpuContext,
app_event_receiver: Receiver<AppEvent>,
app_event_scheduler: AppEventScheduler,
launch_documents: Vec<PathBuf>,
) -> Self {
let ctrlc_app_event_scheduler = app_event_scheduler.clone();
ctrlc::set_handler(move || {
tracing::info!("Termination signal received, exiting...");
ctrlc_app_event_scheduler.schedule(AppEvent::CloseWindow);
})
.expect("Error setting Ctrl-C handler");
fn send_messages_to_editor(&mut self, mut responses: Vec<FrontendMessage>) {
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::RenderOverlays(_))) {
let FrontendMessage::RenderOverlays(overlay_context) = message else { unreachable!() };
if let Some(graphics_state) = &mut self.graphics_state {
let scene = overlay_context.take_scene();
graphics_state.set_overlays_scene(scene);
let rendering_app_event_scheduler = app_event_scheduler.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());
rendering_app_event_scheduler.schedule(AppEvent::NodeGraphExecutionResult(result));
let _ = start_render_receiver.recv();
}
});
let mut persistent_data = PersistentData::default();
persistent_data.load_from_disk();
Self {
render_state: None,
wgpu_context,
window: None,
window_scale: 1.,
window_size: PhysicalSize { width: 0, height: 0 },
window_maximized: false,
window_fullscreen: false,
ui_scale: 1.,
app_event_receiver,
app_event_scheduler,
desktop_wrapper: DesktopWrapper::new(),
last_ui_update: Instant::now(),
cef_context,
cef_schedule: Some(Instant::now()),
cef_view_info_sender,
avg_frame_time: 0.,
start_render_sender,
web_communication_initialized: false,
web_communication_startup_buffer: Vec::new(),
persistent_data,
launch_documents,
}
}
fn resize(&mut self) {
let Some(window) = &self.window else {
tracing::error!("Resize failed due to missing window");
return;
};
let maximized = window.is_maximized();
if maximized != self.window_maximized {
self.window_maximized = maximized;
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::UpdateMaximized { maximized }));
}
for _ in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerOpenDocument)) {
let event_loop_proxy = self.event_loop_proxy.clone();
let _ = thread::spawn(move || {
let path = futures::executor::block_on(dialog_open_graphite_file());
if let Some(path) = path {
let content = std::fs::read_to_string(&path).unwrap_or_else(|_| {
tracing::error!("Failed to read file: {}", path.display());
String::new()
});
let message = PortfolioMessage::OpenDocumentFile {
document_name: path.file_name().and_then(|s| s.to_str()).unwrap_or("unknown").to_string(),
document_serialized_content: content,
};
let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into()));
}
let fullscreen = window.is_fullscreen();
if fullscreen != self.window_fullscreen {
self.window_fullscreen = fullscreen;
self.app_event_scheduler
.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::UpdateFullscreen { fullscreen }));
}
let size = window.surface_size();
let scale = window.scale_factor() * self.ui_scale;
let is_new_size = size != self.window_size;
let is_new_scale = scale != self.window_scale;
if !is_new_size && !is_new_scale {
return;
}
if is_new_size {
let _ = self.cef_view_info_sender.send(cef::ViewInfoUpdate::Size {
width: size.width,
height: size.height,
});
}
if is_new_scale {
let _ = self.cef_view_info_sender.send(cef::ViewInfoUpdate::Scale(scale));
}
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerSaveDocument { .. })) {
let FrontendMessage::TriggerSaveDocument { document_id, name, path, content } = message else {
unreachable!()
};
if let Some(path) = path {
let _ = std::fs::write(&path, content);
} else {
let event_loop_proxy = self.event_loop_proxy.clone();
self.cef_context.notify_view_info_changed();
if let Some(render_state) = &mut self.render_state {
render_state.resize(size.width, size.height);
}
window.request_redraw();
self.window_size = size;
self.window_scale = scale;
}
fn handle_desktop_frontend_message(&mut self, message: DesktopFrontendMessage, responses: &mut Vec<DesktopWrapperMessage>) {
match message {
DesktopFrontendMessage::ToWeb(messages) => {
let Some(bytes) = serialize_frontend_messages(messages) else {
tracing::error!("Failed to serialize frontend messages");
return;
};
self.send_or_queue_web_message(bytes);
}
DesktopFrontendMessage::OpenFileDialog { title, filters, context } => {
let app_event_scheduler = self.app_event_scheduler.clone();
let _ = thread::spawn(move || {
let path = futures::executor::block_on(dialog_save_graphite_file(name));
if let Some(path) = path {
if let Err(e) = std::fs::write(&path, content) {
tracing::error!("Failed to save file: {}: {}", path.display(), e);
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) = fs::read(&path)
{
let message = DesktopWrapperMessage::OpenFileDialogResult { path, content, context };
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
}
});
}
DesktopFrontendMessage::SaveFileDialog {
title,
default_filename,
default_folder,
filters,
context,
} => {
let app_event_scheduler = self.app_event_scheduler.clone();
let _ = thread::spawn(move || {
let mut dialog = AsyncFileDialog::new().set_title(title).set_file_name(default_filename);
if let Some(folder) = default_folder {
dialog = dialog.set_directory(folder);
}
for filter in filters {
dialog = dialog.add_filter(filter.name, &filter.extensions);
}
let show_dialog = async move { dialog.save_file().await.map(|f| f.path().to_path_buf()) };
if let Some(path) = futures::executor::block_on(show_dialog) {
let message = DesktopWrapperMessage::SaveFileDialogResult { path, context };
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
}
});
}
DesktopFrontendMessage::WriteFile { path, content } => {
if let Err(e) = fs::write(&path, content) {
tracing::error!("Failed to write file {}: {}", path.display(), e);
}
}
DesktopFrontendMessage::OpenUrl(url) => {
let _ = thread::spawn(move || {
if let Err(e) = open::that(&url) {
tracing::error!("Failed to open URL: {}: {}", url, e);
}
});
}
DesktopFrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height } => {
if let Some(render_state) = &mut self.render_state
&& let Some(window) = &self.window
{
let window_size = window.surface_size();
let viewport_offset_x = x / window_size.width as f64;
let viewport_offset_y = y / window_size.height as f64;
render_state.set_viewport_offset([viewport_offset_x as f32, viewport_offset_y as f32]);
let viewport_scale_x = if width != 0.0 { window_size.width as f64 / width } else { 1.0 };
let viewport_scale_y = if height != 0.0 { window_size.height as f64 / height } else { 1.0 };
render_state.set_viewport_scale([viewport_scale_x as f32, viewport_scale_y as f32]);
}
}
DesktopFrontendMessage::UpdateUIScale { scale } => {
self.ui_scale = scale;
self.resize();
}
DesktopFrontendMessage::UpdateOverlays(scene) => {
if let Some(render_state) = &mut self.render_state {
render_state.set_overlays_scene(scene);
}
}
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::PersistenceWritePreferences { preferences } => {
self.persistent_data.write_preferences(preferences);
}
DesktopFrontendMessage::PersistenceLoadPreferences => {
let preferences = self.persistent_data.load_preferences();
let message = DesktopWrapperMessage::LoadPreferences { preferences };
responses.push(message);
}
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,
};
responses.push(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,
};
responses.push(message);
}
for (id, document) in self.persistent_data.documents_after_current() {
let message = DesktopWrapperMessage::LoadDocument {
id,
document,
to_front: false,
select_after_open: false,
};
responses.push(message);
}
if let Some(id) = self.persistent_data.current_document_id() {
let message = DesktopWrapperMessage::SelectDocument { id };
responses.push(message);
}
}
DesktopFrontendMessage::OpenLaunchDocuments => {
if self.launch_documents.is_empty() {
return;
}
let app_event_scheduler = self.app_event_scheduler.clone();
let launch_documents = std::mem::take(&mut self.launch_documents);
let _ = thread::spawn(move || {
for path in launch_documents {
tracing::info!("Opening file from command line: {}", path.display());
if let Ok(content) = fs::read(&path) {
let message = DesktopWrapperMessage::OpenFile { path, content };
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
} else {
let message = Message::Portfolio(PortfolioMessage::DocumentPassMessage {
document_id,
message: DocumentMessage::SavedDocument { path: Some(path) },
});
let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message));
tracing::error!("Failed to read file: {}", path.display());
}
}
});
}
DesktopFrontendMessage::UpdateMenu { entries } => {
if let Some(window) = &self.window {
window.update_menu(entries);
}
}
DesktopFrontendMessage::ClipboardRead => {
if let Some(window) = &self.window {
let content = window.clipboard_read();
let message = DesktopWrapperMessage::ClipboardReadResult { content };
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
}
}
DesktopFrontendMessage::ClipboardWrite { content } => {
if let Some(window) = &mut self.window {
window.clipboard_write(content);
}
}
DesktopFrontendMessage::WindowClose => {
self.app_event_scheduler.schedule(AppEvent::CloseWindow);
}
DesktopFrontendMessage::WindowMinimize => {
if let Some(window) = &self.window {
window.minimize();
}
}
DesktopFrontendMessage::WindowMaximize => {
if let Some(window) = &self.window {
window.toggle_maximize();
}
}
DesktopFrontendMessage::WindowDrag => {
if let Some(window) = &self.window {
window.start_drag();
}
}
DesktopFrontendMessage::WindowHide => {
if let Some(window) = &self.window {
window.hide();
}
}
DesktopFrontendMessage::WindowHideOthers => {
if let Some(window) = &self.window {
window.hide_others();
}
}
DesktopFrontendMessage::WindowShowAll => {
if let Some(window) = &self.window {
window.show_all();
}
}
}
}
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerSaveFile { .. })) {
let FrontendMessage::TriggerSaveFile { name, content } = message else { unreachable!() };
let _ = thread::spawn(move || {
let path = futures::executor::block_on(dialog_save_file(name));
if let Some(path) = path {
if let Err(e) = std::fs::write(&path, content) {
tracing::error!("Failed to save file: {}: {}", path.display(), e);
fn handle_desktop_frontend_messages(&mut self, messages: Vec<DesktopFrontendMessage>) {
let mut responses = Vec::new();
for message in messages {
self.handle_desktop_frontend_message(message, &mut responses);
}
for message in responses {
self.dispatch_desktop_wrapper_message(message);
}
}
fn dispatch_desktop_wrapper_message(&mut self, message: DesktopWrapperMessage) {
let responses = self.desktop_wrapper.dispatch(message);
self.handle_desktop_frontend_messages(responses);
}
fn send_or_queue_web_message(&mut self, message: Vec<u8>) {
if self.web_communication_initialized {
self.cef_context.send_web_message(message);
} else {
self.web_communication_startup_buffer.push(message);
}
}
fn user_event(&mut self, event_loop: &dyn ActiveEventLoop, event: AppEvent) {
match event {
AppEvent::WebCommunicationInitialized => {
self.web_communication_initialized = true;
for message in self.web_communication_startup_buffer.drain(..) {
self.cef_context.send_web_message(message);
}
}
AppEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message),
AppEvent::NodeGraphExecutionResult(result) => match result {
NodeGraphExecutionResult::HasRun(texture) => {
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::PollNodeGraphEvaluation);
if let Some(texture) = texture
&& let Some(render_state) = self.render_state.as_mut()
&& let Some(window) = self.window.as_ref()
{
render_state.bind_viewport_texture(texture);
window.request_redraw();
}
}
});
}
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerVisitLink { .. })) {
let _ = thread::spawn(move || {
let FrontendMessage::TriggerVisitLink { url } = message else { unreachable!() };
if let Err(e) = open::that(&url) {
tracing::error!("Failed to open URL: {}: {}", url, e);
}
});
}
if responses.is_empty() {
return;
}
let Ok(message) = ron::to_string(&responses) else {
tracing::error!("Failed to serialize Messages");
return;
};
self.cef_context.send_web_message(message.as_bytes());
}
}
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));
self.cef_context.work();
event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until));
}
fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
if let Some(schedule) = self.cef_schedule
&& schedule < Instant::now()
{
self.cef_schedule = None;
self.cef_context.work();
}
if let StartCause::ResumeTimeReached { .. } = cause {
if let Some(window) = &self.window {
window.request_redraw();
}
}
}
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_family = "unix")]
{
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");
let application_io = WasmApplicationIo::new_with_context(self.wgpu_context.clone());
futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io));
}
fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) {
match event {
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);
NodeGraphExecutionResult::NotRun => {}
},
AppEvent::UiUpdate(texture) => {
if let Some(render_state) = self.render_state.as_mut() {
render_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) => {
AppEvent::ScheduleBrowserWork(instant) => {
if instant <= Instant::now() {
self.cef_context.work();
} else {
self.cef_schedule = Some(instant);
}
}
CustomEvent::DispatchMessage(message) => {
self.dispatch_message(message);
AppEvent::CursorChange(cursor) => {
if let Some(window) = &mut self.window {
window.set_cursor(event_loop, cursor);
}
}
CustomEvent::MessageReceived(message) => {
if let Message::InputPreprocessor(_) = &message {
if let Some(window) = &self.window {
window.request_redraw();
}
}
if let Message::InputPreprocessor(InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports }) = &message {
if let Some(graphic_state) = &mut self.graphics_state {
let window_size = self.window.as_ref().unwrap().inner_size();
let window_size = glam::Vec2::new(window_size.width as f32, window_size.height as f32);
let top_left = bounds_of_viewports[0].top_left.as_vec2() / window_size;
let bottom_right = bounds_of_viewports[0].bottom_right.as_vec2() / window_size;
let offset = top_left.to_array();
let scale = (bottom_right - top_left).recip();
graphic_state.set_viewport_offset(offset);
graphic_state.set_viewport_scale(scale.to_array());
} else {
panic!("graphics state not intialized, viewport offset might be lost");
}
}
AppEvent::CloseWindow => {
// TODO: Implement graceful shutdown
self.dispatch_message(message);
tracing::info!("Exiting main event loop");
event_loop.exit();
}
CustomEvent::NodeGraphRan(texture) => {
if let Some(texture) = texture
&& let Some(graphics_state) = &mut self.graphics_state
{
graphics_state.bind_viewport_texture(texture);
}
let mut responses = VecDeque::new();
let err = self.editor.poll_node_graph_evaluation(&mut responses);
if let Err(e) = err {
if e != "No active document" {
tracing::error!("Error poling node graph: {}", e);
}
}
for message in responses {
self.dispatch_message(message);
}
#[cfg(target_os = "macos")]
AppEvent::MenuEvent { id } => {
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id });
}
}
}
}
impl ApplicationHandler for App {
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
let window = Window::new(event_loop, self.app_event_scheduler.clone());
self.window = Some(window);
fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
let Some(event) = self.cef_context.handle_window_event(event) else { return };
let render_state = RenderState::new(self.window.as_ref().unwrap(), self.wgpu_context.clone());
self.render_state = Some(render_state);
if let Some(window) = &self.window.as_ref() {
window.show();
}
self.resize();
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 proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) {
while let Ok(event) = self.app_event_receiver.try_recv() {
self.user_event(event_loop, event);
}
}
fn window_event(&mut self, event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
self.cef_context.handle_window_event(&event);
match event {
WindowEvent::CloseRequested => {
tracing::info!("The close button was pressed; stopping");
event_loop.exit();
self.app_event_scheduler.schedule(AppEvent::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::SurfaceResized(_) | WindowEvent::ScaleFactorChanged { .. } => {
self.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
#[cfg(target_os = "macos")]
self.resize();
match graphics_state.render() {
Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => {
tracing::warn!("lost surface");
let Some(render_state) = &mut self.render_state else { return };
if let Some(window) = &self.window {
if !window.can_render() {
return;
}
Err(wgpu::SurfaceError::OutOfMemory) => {
event_loop.exit();
match render_state.render(window) {
Ok(_) => {}
Err(RenderError::OutdatedUITextureError) => {
self.cef_context.notify_view_info_changed();
}
Err(RenderError::SurfaceError(wgpu::SurfaceError::Lost)) => {
tracing::warn!("lost surface");
}
Err(RenderError::SurfaceError(wgpu::SurfaceError::OutOfMemory)) => {
tracing::error!("GPU out of memory");
event_loop.exit();
}
Err(RenderError::SurfaceError(e)) => tracing::error!("Render error: {:?}", e),
}
Err(e) => tracing::error!("{:?}", e),
let _ = self.start_render_sender.try_send(());
}
}
WindowEvent::DragDropped { paths, .. } => {
for path in paths {
match fs::read(&path) {
Ok(content) => {
let message = DesktopWrapperMessage::OpenFile { path, content };
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
}
Err(e) => {
tracing::error!("Failed to read dropped file {}: {}", path.display(), e);
return;
}
};
}
}
// Forward and Back buttons are not supported by CEF and thus need to be directly forwarded the editor
WindowEvent::PointerButton {
button: ButtonSource::Mouse(button),
state: ElementState::Pressed,
..
} => {
let mouse_keys = match button {
MouseButton::Back => Some(MouseKeys::BACK),
MouseButton::Forward => Some(MouseKeys::FORWARD),
_ => None,
};
if let Some(mouse_keys) = mouse_keys {
let message = DesktopWrapperMessage::Input(InputMessage::PointerDown {
editor_mouse_state: MouseState { mouse_keys, ..Default::default() },
modifier_keys: Default::default(),
});
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
let message = DesktopWrapperMessage::Input(InputMessage::PointerUp {
editor_mouse_state: Default::default(),
modifier_keys: Default::default(),
});
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
}
}
_ => {}
@ -294,4 +562,24 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
// Notify cef of possible input events
self.cef_context.work();
}
fn about_to_wait(&mut self, event_loop: &dyn 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));
}
}

View file

@ -1,80 +1,152 @@
use crate::{CustomEvent, WgpuContext, render::FrameBufferRef};
use std::{
sync::{Arc, Mutex, mpsc::Receiver},
time::Instant,
};
//! 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 std::fs::File;
use std::io;
use std::io::Read;
use std::path::PathBuf;
use std::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use crate::event::{AppEvent, AppEventScheduler};
use crate::render::FrameBufferRef;
use crate::window::Cursor;
use crate::wrapper::{WgpuContext, deserialize_editor_message};
mod consts;
mod context;
mod dirs;
mod input;
mod internal;
mod ipc;
mod scheme_handler;
mod platform;
mod utility;
pub(crate) use context::{Context, InitError, Initialized, Setup, SetupError};
use winit::event_loop::EventLoopProxy;
#[cfg(feature = "accelerated_paint")]
use cef::osr_texture_import::SharedTextureHandle;
pub(crate) trait CefEventHandler: Clone {
fn window_size(&self) -> WindowSize;
pub(crate) use context::{CefContext, CefContextBuilder, InitError};
pub(crate) trait CefEventHandler: Send + Sync + 'static {
fn view_info(&self) -> ViewInfo;
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>);
/// Scheudule the main event loop to run the cef event loop after the timeout
/// [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation.
#[cfg(feature = "accelerated_paint")]
fn draw_gpu(&self, shared_texture: SharedTextureHandle);
fn load_resource(&self, path: PathBuf) -> Option<Resource>;
fn cursor_change(&self, cursor: Cursor);
/// 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]);
fn duplicate(&self) -> Self
where
Self: Sized;
}
#[derive(Clone, Copy)]
pub(crate) struct WindowSize {
pub(crate) width: usize,
pub(crate) height: usize,
pub(crate) struct ViewInfo {
width: u32,
height: u32,
scale: f64,
}
impl ViewInfo {
pub(crate) fn new() -> Self {
Self { width: 1, height: 1, scale: 1. }
}
pub(crate) fn apply_update(&mut self, update: ViewInfoUpdate) {
match update {
ViewInfoUpdate::Size { width, height } if width > 0 && height > 0 => {
self.width = width;
self.height = height;
}
ViewInfoUpdate::Scale(scale) if scale > 0. => {
self.scale = scale;
}
_ => {}
}
}
pub(crate) fn zoom(&self) -> f64 {
self.scale.ln() / 1.2_f64.ln()
}
pub(crate) fn width(&self) -> u32 {
self.width
}
pub(crate) fn height(&self) -> u32 {
self.height
}
}
impl Default for ViewInfo {
fn default() -> Self {
Self::new()
}
}
impl WindowSize {
pub(crate) fn new(width: usize, height: usize) -> Self {
Self { width, height }
}
pub(crate) enum ViewInfoUpdate {
Size { width: u32, height: u32 },
Scale(f64),
}
#[derive(Clone)]
pub(crate) struct CefHandler {
window_size_receiver: Arc<Mutex<WindowSizeReceiver>>,
event_loop_proxy: EventLoopProxy<CustomEvent>,
wgpu_context: WgpuContext,
pub(crate) struct Resource {
pub(crate) reader: ResourceReader,
pub(crate) mimetype: Option<String>,
}
struct WindowSizeReceiver {
receiver: Receiver<WindowSize>,
window_size: WindowSize,
#[expect(dead_code)]
#[derive(Clone)]
pub(crate) enum ResourceReader {
Embedded(io::Cursor<&'static [u8]>),
File(Arc<File>),
}
impl WindowSizeReceiver {
fn new(window_size_receiver: Receiver<WindowSize>) -> Self {
Self {
window_size: WindowSize { width: 1, height: 1 },
receiver: window_size_receiver,
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),
}
}
}
pub(crate) struct CefHandler {
wgpu_context: WgpuContext,
app_event_scheduler: AppEventScheduler,
view_info_receiver: Arc<Mutex<ViewInfoReceiver>>,
}
impl CefHandler {
pub(crate) fn new(window_size_receiver: Receiver<WindowSize>, event_loop_proxy: EventLoopProxy<CustomEvent>, wgpu_context: WgpuContext) -> Self {
pub(crate) fn new(wgpu_context: WgpuContext, app_event_scheduler: AppEventScheduler, view_info_receiver: Receiver<ViewInfoUpdate>) -> Self {
Self {
window_size_receiver: Arc::new(Mutex::new(WindowSizeReceiver::new(window_size_receiver))),
event_loop_proxy,
wgpu_context,
app_event_scheduler,
view_info_receiver: Arc::new(Mutex::new(ViewInfoReceiver::new(view_info_receiver))),
}
}
}
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);
fn view_info(&self) -> ViewInfo {
let Ok(mut guard) = self.view_info_receiver.lock() else {
tracing::error!("Failed to lock view_info_receiver");
return ViewInfo::new();
};
let WindowSizeReceiver { receiver, window_size } = &mut *guard;
for new_window_size in receiver.try_iter() {
*window_size = new_window_size;
let ViewInfoReceiver { receiver, view_info } = &mut *guard;
for update in receiver.try_iter() {
view_info.apply_update(update);
}
*window_size
*view_info
}
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>) {
let width = frame_buffer.width() as u32;
@ -89,7 +161,7 @@ impl CefEventHandler for CefHandler {
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Bgra8UnormSrgb,
format: wgpu::TextureFormat::Bgra8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
@ -113,22 +185,114 @@ impl CefEventHandler for CefHandler {
},
);
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
self.app_event_scheduler.schedule(AppEvent::UiUpdate(texture));
}
fn schedule_cef_message_loop_work(&self, scheduled_time: std::time::Instant) {
let _ = self.event_loop_proxy.send_event(CustomEvent::ScheduleBrowserWork(scheduled_time));
}
fn receive_web_message(&self, message: &[u8]) {
let str = std::str::from_utf8(message).unwrap();
match ron::from_str(str) {
Ok(message) => {
let _ = self.event_loop_proxy.send_event(CustomEvent::MessageReceived(message));
#[cfg(feature = "accelerated_paint")]
fn draw_gpu(&self, shared_texture: SharedTextureHandle) {
match shared_texture.import_texture(&self.wgpu_context.device) {
Ok(texture) => {
self.app_event_scheduler.schedule(AppEvent::UiUpdate(texture));
}
Err(e) => {
tracing::error!("Failed to deserialize message {:?}", 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(io::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 cursor_change(&self, cursor: Cursor) {
self.app_event_scheduler.schedule(AppEvent::CursorChange(cursor));
}
fn schedule_cef_message_loop_work(&self, scheduled_time: std::time::Instant) {
self.app_event_scheduler.schedule(AppEvent::ScheduleBrowserWork(scheduled_time));
}
fn initialized_web_communication(&self) {
self.app_event_scheduler.schedule(AppEvent::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;
};
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(desktop_wrapper_message));
}
fn duplicate(&self) -> Self
where
Self: Sized,
{
Self {
wgpu_context: self.wgpu_context.clone(),
app_event_scheduler: self.app_event_scheduler.clone(),
view_info_receiver: self.view_info_receiver.clone(),
}
}
}
struct ViewInfoReceiver {
view_info: ViewInfo,
receiver: Receiver<ViewInfoUpdate>,
}
impl ViewInfoReceiver {
fn new(receiver: Receiver<ViewInfoUpdate>) -> Self {
Self { view_info: ViewInfo::new(), receiver }
}
}

22
desktop/src/cef/consts.rs Normal file
View file

@ -0,0 +1,22 @@
use std::time::Duration;
pub(crate) const RESOURCE_SCHEME: &str = "resources";
pub(crate) const RESOURCE_DOMAIN: &str = "resources";
pub(crate) const SCROLL_LINE_HEIGHT: usize = 40;
pub(crate) const SCROLL_LINE_WIDTH: usize = 40;
#[cfg(target_os = "linux")]
pub(crate) const SCROLL_SPEED_X: f32 = 3.0;
#[cfg(target_os = "linux")]
pub(crate) const SCROLL_SPEED_Y: f32 = 3.0;
#[cfg(not(target_os = "linux"))]
pub(crate) const SCROLL_SPEED_X: f32 = 1.0;
#[cfg(not(target_os = "linux"))]
pub(crate) const SCROLL_SPEED_Y: f32 = 1.0;
pub(crate) const PINCH_ZOOM_SPEED: f64 = 300.0;
pub(crate) const MULTICLICK_TIMEOUT: Duration = Duration::from_millis(500);
pub(crate) const MULTICLICK_ALLOWED_TRAVEL: usize = 4;

View file

@ -1,161 +1,16 @@
use cef::sys::{CEF_API_VERSION_LAST, cef_resultcode_t};
use cef::{App, BrowserSettings, Client, DictionaryValue, ImplBrowser, ImplBrowserHost, ImplCommandLine, RenderHandler, RequestContext, WindowInfo, browser_host_create_browser_sync, initialize};
use cef::{Browser, CefString, Settings, api_hash, args::Args, execute_process};
use thiserror::Error;
use winit::event::WindowEvent;
#[cfg(not(target_os = "macos"))]
mod multithreaded;
mod singlethreaded;
use crate::cef::dirs::{cef_cache_dir, cef_data_dir};
mod builder;
pub(crate) use builder::{CefContextBuilder, InitError};
use super::input::InputState;
use super::ipc::{MessageType, SendMessage};
use super::scheme_handler::{FRONTEND_DOMAIN, GRAPHITE_SCHEME};
use super::{CefEventHandler, input};
pub(crate) trait CefContext {
fn work(&mut self);
use super::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderHandlerImpl, RenderProcessAppImpl};
fn handle_window_event(&mut self, event: &winit::event::WindowEvent);
pub(crate) struct Setup {}
pub(crate) struct Initialized {}
pub(crate) trait ContextState {}
impl ContextState for Setup {}
impl ContextState for Initialized {}
fn notify_view_info_changed(&self);
pub(crate) struct Context<S: ContextState> {
args: Args,
pub(crate) browser: Option<Browser>,
pub(crate) input_state: InputState,
marker: std::marker::PhantomData<S>,
}
impl Context<Setup> {
pub(crate) fn new() -> Result<Context<Setup>, SetupError> {
#[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_browser_process = cmd.has_switch(Some(&switch)) != 1;
if !is_browser_process {
let process_type = CefString::from(&cmd.switch_value(Some(&switch)));
let mut app = RenderProcessAppImpl::app();
let ret = execute_process(Some(args.as_main_args()), Some(&mut app), std::ptr::null_mut());
if ret >= 0 {
return Err(SetupError::SubprocessFailed(process_type.to_string()));
} else {
return Err(SetupError::Subprocess);
}
}
Ok(Context {
args,
browser: None,
input_state: InputState::default(),
marker: std::marker::PhantomData::<Setup>,
})
}
pub(crate) fn init(self, event_handler: impl CefEventHandler) -> Result<Context<Initialized>, 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()
};
// Attention! Wrapping this in an extra App is necessary, otherwise the program still compiles but segfaults
let mut cef_app = App::new(BrowserProcessAppImpl::new(event_handler.clone()));
let result = initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut());
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));
}
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!("{GRAPHITE_SCHEME}://{FRONTEND_DOMAIN}/").as_str());
let window_info = WindowInfo {
windowless_rendering_enabled: 1,
..Default::default()
};
let settings = BrowserSettings {
windowless_frame_rate: 60,
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,
);
Ok(Context {
args: self.args.clone(),
browser,
input_state: self.input_state.clone(),
marker: std::marker::PhantomData::<Initialized>,
})
}
}
impl Context<Initialized> {
pub(crate) fn work(&mut self) {
cef::do_message_loop_work();
}
pub(crate) fn handle_window_event(&mut self, event: WindowEvent) -> Option<WindowEvent> {
input::handle_window_event(self, event)
}
pub(crate) fn notify_of_resize(&self) {
if let Some(browser) = &self.browser {
browser.host().unwrap().was_resized();
}
}
pub(crate) fn send_web_message(&self, message: &[u8]) {
self.send_message(MessageType::SendToJS, message);
}
}
impl<S: ContextState> Drop for Context<S> {
fn drop(&mut self) {
if self.browser.is_some() {
cef::shutdown();
}
}
}
#[derive(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(Error, Debug)]
pub(crate) enum InitError {
#[error("initialization failed")]
InitializationFailed(u32),
#[error("Another instance is already running")]
AlreadyRunning,
fn send_web_message(&self, message: Vec<u8>);
}

View file

@ -0,0 +1,230 @@
use std::path::{Path, PathBuf};
use cef::args::Args;
use cef::sys::{CEF_API_VERSION_LAST, cef_resultcode_t};
use cef::{
App, BrowserSettings, CefString, Client, DictionaryValue, ImplCommandLine, ImplRequestContext, RequestContextSettings, SchemeHandlerFactory, 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::{create_instance_dir, delete_instance_dirs};
use crate::cef::input::InputState;
use crate::cef::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderProcessAppImpl, SchemeHandlerFactoryImpl};
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 {
Self::new_inner(false)
}
pub(crate) fn new_helper() -> Self {
Self::new_inner(true)
}
fn new_inner(helper: bool) -> Self {
#[cfg(target_os = "macos")]
let _loader = {
let loader = cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), helper);
assert!(loader.load());
loader
};
#[cfg(not(target_os = "macos"))]
let _ = helper;
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
}
}
fn common_settings(instance_dir: &Path) -> Settings {
Settings {
windowless_rendering_enabled: 1,
root_cache_path: instance_dir.to_str().map(CefString::from).unwrap(),
cache_path: CefString::from(""),
disable_signal_handlers: 1,
..Default::default()
}
}
#[cfg(target_os = "macos")]
pub(crate) fn initialize(self, event_handler: H, disable_gpu_acceleration: bool) -> Result<impl CefContext, InitError> {
delete_instance_dirs();
let instance_dir = create_instance_dir();
let exe = std::env::current_exe().expect("cannot get current exe path");
let app_root = exe.parent().and_then(|p| p.parent()).expect("bad path structure").parent().expect("bad path structure");
let settings = Settings {
main_bundle_path: CefString::from(app_root.to_str().unwrap()),
multi_threaded_message_loop: 0,
external_message_pump: 1,
no_sandbox: 1, // GPU helper crashes when running with sandbox
..Self::common_settings(&instance_dir)
};
self.initialize_inner(&event_handler, settings)?;
create_browser(event_handler, instance_dir, disable_gpu_acceleration)
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn initialize(self, event_handler: H, disable_gpu_acceleration: bool) -> Result<impl CefContext, InitError> {
delete_instance_dirs();
let instance_dir = create_instance_dir();
let settings = Settings {
multi_threaded_message_loop: 1,
..Self::common_settings(&instance_dir)
};
self.initialize_inner(&event_handler, settings)?;
super::multithreaded::run_on_ui_thread(move || match create_browser(event_handler, instance_dir, disable_gpu_acceleration) {
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> {
// Attention! Wrapping this in an extra App is necessary, otherwise the program still compiles but segfaults
let mut cef_app = App::new(BrowserProcessAppImpl::new(event_handler.duplicate()));
let result = cef::initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut());
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, instance_dir: PathBuf, disable_gpu_acceleration: bool) -> Result<SingleThreadedCefContext, InitError> {
let mut client = Client::new(BrowserProcessClientImpl::new(&event_handler));
#[cfg(feature = "accelerated_paint")]
let use_accelerated_paint = if disable_gpu_acceleration {
false
} else {
crate::cef::platform::should_enable_hardware_acceleration()
};
let window_info = WindowInfo {
windowless_rendering_enabled: 1,
#[cfg(feature = "accelerated_paint")]
shared_texture_enabled: use_accelerated_paint as i32,
..Default::default()
};
let settings = BrowserSettings {
windowless_frame_rate: crate::consts::CEF_WINDOWLESS_FRAME_RATE,
background_color: 0x0,
..Default::default()
};
let Some(mut incognito_request_context) = cef::request_context_create_context(
Some(&RequestContextSettings {
persist_session_cookies: 0,
cache_path: CefString::from(""),
..Default::default()
}),
Option::<&mut cef::RequestContextHandler>::None,
) else {
return Err(InitError::RequestContextCreationFailed);
};
let mut scheme_handler_factory = SchemeHandlerFactory::new(SchemeHandlerFactoryImpl::new(event_handler.duplicate()));
incognito_request_context.clear_scheme_handler_factories();
incognito_request_context.register_scheme_handler_factory(Some(&CefString::from(RESOURCE_SCHEME)), Some(&CefString::from(RESOURCE_DOMAIN)), Some(&mut scheme_handler_factory));
let url = CefString::from(format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/").as_str());
let browser = browser_host_create_browser_sync(
Some(&window_info),
Some(&mut client),
Some(&url),
Some(&settings),
Option::<&mut DictionaryValue>::None,
Some(&mut incognito_request_context),
);
if let Some(browser) = browser {
Ok(SingleThreadedCefContext {
event_handler: Box::new(event_handler),
browser,
input_state: InputState::default(),
instance_dir,
})
} 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("Request context creation failed")]
RequestContextCreationFailed,
#[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_view_info_changed(&self) {
run_on_ui_thread(move || {
CONTEXT.with(|b| {
if let Some(context) = b.borrow_mut().as_mut() {
context.notify_view_info_changed();
}
});
});
}
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,71 @@
use cef::{Browser, ImplBrowser, ImplBrowserHost};
use winit::event::WindowEvent;
use crate::cef::input::InputState;
use crate::cef::ipc::{MessageType, SendMessage};
use crate::cef::{CefEventHandler, input};
use super::CefContext;
pub(super) struct SingleThreadedCefContext {
pub(super) event_handler: Box<dyn CefEventHandler>,
pub(super) browser: Browser,
pub(super) input_state: InputState,
pub(super) instance_dir: std::path::PathBuf,
}
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_view_info_changed(&self) {
let view_info = self.event_handler.view_info();
let host = self.browser.host().unwrap();
host.set_zoom_level(view_info.zoom());
host.was_resized();
// Fix for CEF not updating the view after resize on windows and mac
// TODO: remove once https://github.com/chromiumembedded/cef/issues/3822 is fixed
#[cfg(any(target_os = "windows", target_os = "macos"))]
host.invalidate(cef::PaintElementType::default());
}
fn send_web_message(&self, message: Vec<u8>) {
self.send_message(MessageType::SendToJS, &message);
}
}
impl Drop for SingleThreadedCefContext {
fn drop(&mut self) {
cef::shutdown();
// Sometimes some CEF processes still linger at this point and hold file handles to the cache directory.
// To mitigate this, we try to remove the directory multiple times with some delay.
// TODO: find a better solution if possible.
for _ in 0..30 {
match std::fs::remove_dir_all(&self.instance_dir) {
Ok(_) => break,
Err(e) => {
tracing::warn!("Failed to remove CEF cache directory, retrying...: {e}");
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
}
}
}
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);
}
}

View file

@ -1,17 +1,24 @@
use std::path::PathBuf;
use crate::dirs::{ensure_dir_exists, graphite_data_dir};
use crate::dirs::{app_data_dir, ensure_dir_exists};
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 delete_instance_dirs() {
let cef_dir = app_data_dir().join(CEF_DIR_NAME);
if let Ok(entries) = std::fs::read_dir(&cef_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let _ = std::fs::remove_dir_all(&path);
}
}
}
}
pub(crate) fn cef_cache_dir() -> PathBuf {
let path = cef_data_dir().join("cache");
pub(crate) fn create_instance_dir() -> PathBuf {
let instance_id: String = (0..32).map(|_| format!("{:x}", rand::random::<u8>() % 16)).collect();
let path = app_data_dir().join(CEF_DIR_NAME).join(instance_id);
ensure_dir_exists(&path);
path
}

View file

@ -1,277 +1,177 @@
use cef::sys::{cef_event_flags_t, cef_key_event_type_t, cef_mouse_button_type_t};
use cef::{ImplBrowser, ImplBrowserHost, KeyEvent, KeyEventType, MouseEvent};
use winit::dpi::PhysicalPosition;
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
use super::context::{Context, Initialized};
use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, MouseEvent};
use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, WindowEvent};
use winit::keyboard::Key;
mod keymap;
use keymap::{ToDomBits, ToVKBits};
use keymap::{ToCharRepresentation, ToNativeKeycode, ToVKBits};
pub(crate) fn handle_window_event(context: &mut Context<Initialized>, event: WindowEvent) -> Option<WindowEvent> {
mod state;
pub(crate) use state::{CefModifiers, InputState};
use super::consts::{PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y};
pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputState, event: &WindowEvent) {
match event {
WindowEvent::CursorMoved { position, .. } => {
if let Some(browser) = &context.browser {
if let Some(host) = browser.host() {
host.set_focus(1);
}
context.input_state.update_mouse_position(&position);
let mouse_event: MouseEvent = (&context.input_state).into();
browser.host().unwrap().send_mouse_move_event(Some(&mouse_event), 0);
WindowEvent::PointerMoved { position, .. } | WindowEvent::PointerEntered { position, .. } => {
if !input_state.cursor_move(position) {
return;
}
let Some(host) = browser.host() else { return };
host.send_mouse_move_event(Some(&input_state.into()), 0);
}
WindowEvent::MouseInput { state, button, .. } => {
if let Some(browser) = &context.browser {
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 = context.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,
}
}
_ => {}
};
context.input_state.update_mouse_state(mouse_state);
let mouse_event: MouseEvent = (&context.input_state).into();
if let Some(button) = cef_button {
host.send_mouse_click_event(
Some(&mouse_event),
button,
mouse_up,
1, // click count
);
}
}
WindowEvent::PointerLeft { position, .. } => {
if let Some(position) = position {
let _ = input_state.cursor_move(position);
}
let Some(host) = browser.host() else { return };
host.send_mouse_move_event(Some(&(input_state.into())), 1);
}
WindowEvent::PointerButton { state, button, .. } => {
let mouse_button = match button {
ButtonSource::Mouse(mouse_button) => mouse_button,
_ => {
return; // TODO: Handle touch input
}
};
let cef_click_count = input_state.mouse_input(mouse_button, state).into();
let cef_mouse_up = match state {
ElementState::Pressed => 0,
ElementState::Released => 1,
};
let cef_button = match mouse_button {
MouseButton::Left => cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_LEFT),
MouseButton::Right => cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_RIGHT),
MouseButton::Middle => cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_MIDDLE),
_ => return,
};
let Some(host) = browser.host() else { return };
host.send_mouse_click_event(Some(&input_state.into()), cef_button, cef_mouse_up, cef_click_count);
}
WindowEvent::MouseWheel { delta, phase: _, device_id: _, .. } => {
if let Some(browser) = &context.browser {
if let Some(host) = browser.host() {
let mouse_event = (&context.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);
}
}
let mouse_event = input_state.into();
let (mut delta_x, mut delta_y) = match delta {
MouseScrollDelta::LineDelta(x, y) => (x * SCROLL_LINE_WIDTH as f32, y * SCROLL_LINE_HEIGHT as f32),
MouseScrollDelta::PixelDelta(physical_position) => (physical_position.x as f32, physical_position.y as f32),
};
delta_x *= SCROLL_SPEED_X;
delta_y *= SCROLL_SPEED_Y;
let Some(host) = browser.host() else { return };
host.send_mouse_wheel_event(Some(&mouse_event), delta_x as i32, delta_y as i32);
}
WindowEvent::ModifiersChanged(modifiers) => {
context.input_state.update_modifiers(&modifiers.state());
input_state.modifiers_changed(&modifiers.state());
}
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
if let Some(browser) = &context.browser {
if let Some(host) = browser.host() {
host.set_focus(1);
let Some(host) = browser.host() else { return };
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 None,
};
let mut key_event = KeyEvent {
size: size_of::<KeyEvent>(),
focus_on_editable_field: 1,
modifiers: context.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));
}
};
let mut key_event = KeyEvent {
type_: match (event.state, &event.logical_key) {
(ElementState::Pressed, winit::keyboard::Key::Character(_)) => cef_key_event_type_t::KEYEVENT_CHAR,
(ElementState::Pressed, _) => cef_key_event_type_t::KEYEVENT_RAWKEYDOWN,
(ElementState::Released, _) => cef_key_event_type_t::KEYEVENT_KEYUP,
}
.into(),
..Default::default()
};
key_event.modifiers = input_state.cef_modifiers(&event.location, event.repeat).into();
match (&event.logical_key, event.state) {
(Key::Named(winit::keyboard::NamedKey::Control), ElementState::Pressed) => {
key_event.modifiers |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0;
}
(Key::Named(winit::keyboard::NamedKey::Control), ElementState::Released) => {
key_event.modifiers &= !(cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0);
}
(Key::Named(winit::keyboard::NamedKey::Shift), ElementState::Pressed) => {
key_event.modifiers |= cef_event_flags_t::EVENTFLAG_SHIFT_DOWN.0;
}
(Key::Named(winit::keyboard::NamedKey::Shift), ElementState::Released) => {
key_event.modifiers &= !(cef_event_flags_t::EVENTFLAG_SHIFT_DOWN.0);
}
(Key::Named(winit::keyboard::NamedKey::Alt), ElementState::Pressed) => {
key_event.modifiers |= cef_event_flags_t::EVENTFLAG_ALT_DOWN.0;
}
(Key::Named(winit::keyboard::NamedKey::Alt), ElementState::Released) => {
key_event.modifiers &= !(cef_event_flags_t::EVENTFLAG_ALT_DOWN.0);
}
(Key::Named(winit::keyboard::NamedKey::Meta), ElementState::Pressed) => {
key_event.modifiers |= cef_event_flags_t::EVENTFLAG_COMMAND_DOWN.0;
}
(Key::Named(winit::keyboard::NamedKey::Meta), ElementState::Released) => {
key_event.modifiers &= !(cef_event_flags_t::EVENTFLAG_COMMAND_DOWN.0);
}
_ => {}
}
key_event.windows_key_code = match &event.logical_key {
winit::keyboard::Key::Named(named) => named.to_vk_bits(),
winit::keyboard::Key::Character(char) => char.chars().next().unwrap_or_default().to_vk_bits(),
_ => 0,
};
key_event.native_key_code = event.physical_key.to_native_keycode();
key_event.character = event.logical_key.to_char_representation() as u16;
if event.state == ElementState::Pressed && key_event.character != 0 {
key_event.type_ = cef_key_event_type_t::KEYEVENT_CHAR.into();
}
// Mitigation for CEF on Mac bug to prevent NSMenu being triggered by this key event.
//
// CEF converts the key event into an `NSEvent` internally and passes that to Chromium.
// In some cases the `NSEvent` gets to the native Cocoa application, is considered "unhandled" and can trigger menus.
//
// Why mitigation works:
// Leaving `key_event.unmodified_character = 0` still leads to CEF forwarding a "unhandled" event to the native application
// but that event is discarded because `key_event.unmodified_character = 0` is considered non-printable and not used for shortcut matching.
//
// See https://github.com/chromiumembedded/cef/issues/3857
//
// TODO: Remove mitigation once bug is fixed or a better solution is found.
#[cfg(not(target_os = "macos"))]
{
key_event.unmodified_character = event.key_without_modifiers.to_char_representation() as u16;
}
#[cfg(target_os = "macos")] // See https://www.magpcss.org/ceforum/viewtopic.php?start=10&t=11650
if key_event.character == 0 && key_event.unmodified_character == 0 && event.text_with_all_modifiers.is_some() {
key_event.character = 1;
}
if key_event.type_ == cef_key_event_type_t::KEYEVENT_CHAR.into() {
let mut key_down_event = key_event.clone();
key_down_event.type_ = cef_key_event_type_t::KEYEVENT_RAWKEYDOWN.into();
host.send_key_event(Some(&key_down_event));
key_event.windows_key_code = event.logical_key.to_char_representation() as i32;
}
host.send_key_event(Some(&key_event));
}
e => return Some(e),
}
None
}
WindowEvent::PinchGesture { delta, .. } => {
if !delta.is_normal() {
return;
}
let Some(host) = browser.host() else { return };
#[derive(Default, Clone)]
pub(crate) struct MouseState {
left: bool,
right: bool,
middle: bool,
}
let mouse_event = MouseEvent {
modifiers: CefModifiers::PINCH_MODIFIERS.into(),
..input_state.into()
};
#[derive(Default, Clone, Debug)]
pub(crate) struct MousePosition {
x: usize,
y: usize,
}
let delta = (delta * PINCH_ZOOM_SPEED).round() as i32;
impl From<&PhysicalPosition<f64>> for MousePosition {
fn from(position: &PhysicalPosition<f64>) -> Self {
Self {
x: position.x as usize,
y: position.y as usize,
host.send_mouse_wheel_event(Some(&mouse_event), 0, delta);
}
}
}
#[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(),
}
}
}
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

@ -1,3 +1,48 @@
use winit::keyboard::{Key, NamedKey, PhysicalKey};
pub(crate) trait ToCharRepresentation {
fn to_char_representation(&self) -> char;
}
impl ToCharRepresentation for Key {
fn to_char_representation(&self) -> char {
match self {
Key::Named(named) => match named {
NamedKey::Tab => '\t',
NamedKey::Enter => '\r',
NamedKey::Backspace => '\x08',
NamedKey::Escape => '\x1b',
_ => '\0',
},
Key::Character(char) => char.chars().next().unwrap_or_default(),
_ => '\0',
}
}
}
pub(crate) trait ToNativeKeycode {
fn to_native_keycode(&self) -> i32;
}
impl ToNativeKeycode for PhysicalKey {
fn to_native_keycode(&self) -> i32 {
use winit::platform::scancode::PhysicalKeyExtScancode;
#[cfg(target_os = "linux")]
{
self.to_scancode().map(|evdev| (evdev + 8) as i32).unwrap_or_default()
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
self.to_scancode().map(|c| c as i32).unwrap_or_default()
}
}
}
pub(crate) trait ToVKBits {
fn to_vk_bits(&self) -> i32;
}
macro_rules! map_enum {
($target:expr, $enum:ident, $( ($code:expr, $variant:ident), )+ ) => {
match $target {
@ -8,26 +53,8 @@ macro_rules! map_enum {
}
};
}
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,
@ -39,14 +66,12 @@ impl ToVKBits for winit::keyboard::NamedKey {
(0x91, ScrollLock),
(0x10, Shift),
(0x5B, Meta),
(0x5C, Super),
(0x0D, Enter),
(0x09, Tab),
(0x20, Space),
(0x28, ArrowDown),
(0x25, ArrowLeft),
(0x27, ArrowRight),
(0x26, ArrowUp),
(0x27, ArrowRight),
(0x28, ArrowDown),
(0x23, End),
(0x24, Home),
(0x22, PageDown),
@ -136,6 +161,16 @@ impl ToVKBits for winit::keyboard::NamedKey {
}
}
macro_rules! map {
($target:expr, $( ($code:expr, $variant:literal), )+ ) => {
match $target {
$(
$variant => $code,
)+
_ => 0,
}
};
}
impl ToVKBits for char {
fn to_vk_bits(&self) -> i32 {
map!(
@ -234,215 +269,7 @@ impl ToVKBits for char {
(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, '?'),
(0x20, ' '),
)
}
}

View file

@ -0,0 +1,256 @@
use cef::MouseEvent;
use cef::sys::cef_event_flags_t;
use std::time::Instant;
use winit::dpi::PhysicalPosition;
use winit::event::{ElementState, MouseButton};
use winit::keyboard::{KeyLocation, ModifiersState};
use crate::cef::consts::{MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT};
#[derive(Default)]
pub(crate) struct InputState {
modifiers: ModifiersState,
mouse_position: MousePosition,
mouse_state: MouseState,
mouse_click_tracker: ClickTracker,
}
impl InputState {
pub(crate) fn modifiers_changed(&mut self, modifiers: &ModifiersState) {
self.modifiers = *modifiers;
}
pub(crate) fn cursor_move(&mut self, position: &PhysicalPosition<f64>) -> bool {
let new = position.into();
if self.mouse_position == new {
return false;
}
self.mouse_position = new;
true
}
pub(crate) fn mouse_input(&mut self, button: &MouseButton, state: &ElementState) -> ClickCount {
self.mouse_state.update(button, state);
self.mouse_click_tracker.input(button, state, self.mouse_position)
}
pub(crate) fn cef_modifiers(&self, location: &KeyLocation, is_repeat: bool) -> CefModifiers {
CefModifiers::new(self, location, is_repeat)
}
pub(crate) fn cef_mouse_modifiers(&self) -> CefModifiers {
self.cef_modifiers(&KeyLocation::Standard, false)
}
}
impl From<InputState> for CefModifiers {
fn from(val: InputState) -> Self {
CefModifiers::new(&val, &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_mouse_modifiers().into(),
}
}
}
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_mouse_modifiers().into(),
}
}
}
#[derive(Default, Clone, Copy, Eq, PartialEq)]
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 MouseState {
left: bool,
right: bool,
middle: bool,
}
impl MouseState {
pub(crate) fn update(&mut self, button: &MouseButton, state: &ElementState) {
match state {
ElementState::Pressed => match button {
MouseButton::Left => self.left = true,
MouseButton::Right => self.right = true,
MouseButton::Middle => self.middle = true,
_ => {}
},
ElementState::Released => match button {
MouseButton::Left => self.left = false,
MouseButton::Right => self.right = false,
MouseButton::Middle => self.middle = false,
_ => {}
},
}
}
}
#[derive(Default)]
struct ClickTracker {
left: Option<ClickRecord>,
middle: Option<ClickRecord>,
right: Option<ClickRecord>,
}
impl ClickTracker {
fn input(&mut self, button: &MouseButton, state: &ElementState, position: MousePosition) -> ClickCount {
let record = match button {
MouseButton::Left => &mut self.left,
MouseButton::Right => &mut self.right,
MouseButton::Middle => &mut self.middle,
_ => return ClickCount::Single,
};
let Some(record) = record else {
*record = Some(ClickRecord { position, ..Default::default() });
return ClickCount::Single;
};
let prev_time = record.time;
let prev_position = record.position;
let now = Instant::now();
record.time = now;
record.position = position;
match state {
ElementState::Pressed if record.down_count == ClickCount::Double => {
*record = ClickRecord {
down_count: ClickCount::Single,
..*record
};
return ClickCount::Single;
}
ElementState::Released if record.up_count == ClickCount::Double => {
*record = ClickRecord {
up_count: ClickCount::Single,
..*record
};
return ClickCount::Single;
}
_ => {}
}
let dx = position.x.abs_diff(prev_position.x);
let dy = position.y.abs_diff(prev_position.y);
let within_dist = dx <= MULTICLICK_ALLOWED_TRAVEL && dy <= MULTICLICK_ALLOWED_TRAVEL;
let within_time = now.saturating_duration_since(prev_time) <= MULTICLICK_TIMEOUT;
let count = if within_time && within_dist { ClickCount::Double } else { ClickCount::Single };
*record = match state {
ElementState::Pressed => ClickRecord { down_count: count, ..*record },
ElementState::Released => ClickRecord { up_count: count, ..*record },
};
count
}
}
#[derive(Clone, Copy, PartialEq, Default)]
pub(crate) enum ClickCount {
#[default]
Single,
Double,
}
impl From<ClickCount> for i32 {
fn from(count: ClickCount) -> i32 {
match count {
ClickCount::Single => 1,
ClickCount::Double => 2,
}
}
}
#[derive(Clone, Copy)]
struct ClickRecord {
time: Instant,
position: MousePosition,
down_count: ClickCount,
up_count: ClickCount,
}
impl Default for ClickRecord {
fn default() -> Self {
Self {
time: Instant::now(),
position: Default::default(),
down_count: Default::default(),
up_count: Default::default(),
}
}
}
pub(crate) struct CefModifiers(cef_event_flags_t);
impl CefModifiers {
fn new(input_state: &InputState, location: &KeyLocation, is_repeat: bool) -> Self {
let mut inner = cef_event_flags_t::EVENTFLAG_NONE;
if input_state.modifiers.shift_key() {
inner |= cef_event_flags_t::EVENTFLAG_SHIFT_DOWN;
}
if input_state.modifiers.control_key() {
inner |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN;
}
if input_state.modifiers.alt_key() {
inner |= cef_event_flags_t::EVENTFLAG_ALT_DOWN;
}
if input_state.modifiers.meta_key() {
inner |= cef_event_flags_t::EVENTFLAG_COMMAND_DOWN;
}
if input_state.mouse_state.left {
inner |= cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON;
}
if input_state.mouse_state.right {
inner |= cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON;
}
if input_state.mouse_state.middle {
inner |= cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON;
}
if is_repeat {
inner |= cef_event_flags_t::EVENTFLAG_IS_REPEAT;
}
inner |= match location {
KeyLocation::Left => cef_event_flags_t::EVENTFLAG_IS_LEFT,
KeyLocation::Right => cef_event_flags_t::EVENTFLAG_IS_RIGHT,
KeyLocation::Numpad => cef_event_flags_t::EVENTFLAG_IS_KEY_PAD,
KeyLocation::Standard => cef_event_flags_t::EVENTFLAG_NONE,
};
Self(inner)
}
pub(super) const PINCH_MODIFIERS: Self = Self(cef_event_flags_t(
cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0 | cef_event_flags_t::EVENTFLAG_PRECISION_SCROLLING_DELTA.0,
));
}
impl From<CefModifiers> for u32 {
fn from(val: CefModifiers) -> Self {
#[cfg(not(target_os = "windows"))]
return val.0.0;
#[cfg(target_os = "windows")]
return val.0.0 as u32;
}
}

View file

@ -1,13 +1,24 @@
mod browser_process_app;
mod browser_process_client;
mod browser_process_handler;
mod browser_process_life_span_handler;
mod render_handler;
mod render_process_app;
mod render_process_handler;
mod render_process_v8_handler;
mod context_menu_handler;
mod display_handler;
mod life_span_handler;
mod load_handler;
mod resource_handler;
mod scheme_handler_factory;
pub(super) mod render_handler;
#[cfg(not(target_os = "macos"))]
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;
pub(super) use scheme_handler_factory::SchemeHandlerFactoryImpl;

View file

@ -1,20 +1,19 @@
#[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 crate::cef::CefEventHandler;
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
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> {
impl<H: CefEventHandler> BrowserProcessAppImpl<H> {
pub(crate) fn new(event_handler: H) -> Self {
Self {
object: std::ptr::null_mut(),
@ -23,23 +22,49 @@ impl<H: CefEventHandler + Clone> BrowserProcessAppImpl<H> {
}
}
impl<H: CefEventHandler + Clone> ImplApp for BrowserProcessAppImpl<H> {
impl<H: CefEventHandler> ImplApp for BrowserProcessAppImpl<H> {
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(BrowserProcessHandler::new(BrowserProcessHandlerImpl::new(self.event_handler.clone())))
Some(BrowserProcessHandler::new(BrowserProcessHandlerImpl::new(self.event_handler.duplicate())))
}
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
GraphiteSchemeHandlerFactory::register_schemes(registrar);
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 {
// Disable GPU acceleration, because it is not supported for Offscreen Rendering and can cause crashes.
cmd.append_switch(Some(&CefString::from("disable-gpu")));
cmd.append_switch(Some(&CefString::from("disable-gpu-compositing")));
cmd.append_switch_with_value(Some(&CefString::from("renderer-process-limit")), Some(&CefString::from("1")));
cmd.append_switch_with_value(Some(&CefString::from("disk-cache-size")), Some(&CefString::from("0")));
cmd.append_switch(Some(&CefString::from("incognito")));
cmd.append_switch(Some(&CefString::from("no-first-run")));
cmd.append_switch(Some(&CefString::from("disable-file-system")));
cmd.append_switch(Some(&CefString::from("disable-local-storage")));
cmd.append_switch(Some(&CefString::from("disable-background-networking")));
cmd.append_switch(Some(&CefString::from("disable-audio-input")));
cmd.append_switch(Some(&CefString::from("disable-audio-output")));
#[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(not(any(target_os = "macos", target_os = "windows")))]
#[cfg(target_os = "linux")]
{
let use_wayland = env::var("WAYLAND_DISPLAY")
.ok()
@ -51,6 +76,20 @@ impl<H: CefEventHandler + Clone> ImplApp for BrowserProcessAppImpl<H> {
cmd.append_switch_with_value(Some(&CefString::from("ozone-platform")), Some(&CefString::from("wayland")));
}
}
#[cfg(target_os = "macos")]
{
// Hide user prompt asking for keychain access
cmd.append_switch(Some(&CefString::from("use-mock-keychain")));
}
// Enable browser debugging via environment variable
if let Some(env) = std::env::var("GRAPHITE_BROWSER_DEBUG_PORT").ok()
&& let Some(port) = env.parse::<u16>().ok()
{
cmd.append_switch_with_value(Some(&CefString::from("remote-debugging-port")), Some(&CefString::from(port.to_string().as_str())));
cmd.append_switch_with_value(Some(&CefString::from("remote-allow-origins")), Some(&CefString::from("*")));
}
}
}
@ -59,7 +98,7 @@ impl<H: CefEventHandler + Clone> ImplApp for BrowserProcessAppImpl<H> {
}
}
impl<H: CefEventHandler + Clone> Clone for BrowserProcessAppImpl<H> {
impl<H: CefEventHandler> Clone for BrowserProcessAppImpl<H> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
@ -67,7 +106,7 @@ impl<H: CefEventHandler + Clone> Clone for BrowserProcessAppImpl<H> {
}
Self {
object: self.object,
event_handler: self.event_handler.clone(),
event_handler: self.event_handler.duplicate(),
}
}
}
@ -79,7 +118,7 @@ impl<H: CefEventHandler> Rc for BrowserProcessAppImpl<H> {
}
}
}
impl<H: CefEventHandler + Clone> WrapApp for BrowserProcessAppImpl<H> {
impl<H: CefEventHandler> WrapApp for BrowserProcessAppImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
self.object = object;
}

View file

@ -1,23 +1,31 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_client_t, cef_base_ref_counted_t};
use cef::{ImplClient, LifeSpanHandler, RenderHandler, WrapClient};
use cef::{ContextMenuHandler, DisplayHandler, ImplClient, LifeSpanHandler, LoadHandler, RenderHandler, WrapClient};
use crate::cef::CefEventHandler;
use crate::cef::ipc::{MessageType, UnpackMessage, UnpackedMessage};
use super::browser_process_life_span_handler::BrowserProcessLifeSpanHandlerImpl;
use super::context_menu_handler::ContextMenuHandlerImpl;
use super::display_handler::DisplayHandlerImpl;
use super::life_span_handler::LifeSpanHandlerImpl;
use super::load_handler::LoadHandlerImpl;
use super::render_handler::RenderHandlerImpl;
pub(crate) struct BrowserProcessClientImpl<H: CefEventHandler> {
object: *mut RcImpl<_cef_client_t, Self>,
render_handler: RenderHandler,
event_handler: H,
load_handler: LoadHandler,
render_handler: RenderHandler,
display_handler: DisplayHandler,
}
impl<H: CefEventHandler> BrowserProcessClientImpl<H> {
pub(crate) fn new(render_handler: RenderHandler, event_handler: H) -> Self {
pub(crate) fn new(event_handler: &H) -> Self {
Self {
object: std::ptr::null_mut(),
render_handler,
event_handler,
event_handler: event_handler.duplicate(),
load_handler: LoadHandler::new(LoadHandlerImpl::new(event_handler.duplicate())),
render_handler: RenderHandler::new(RenderHandlerImpl::new(event_handler.duplicate())),
display_handler: DisplayHandler::new(DisplayHandlerImpl::new(event_handler.duplicate())),
}
}
}
@ -29,9 +37,13 @@ impl<H: CefEventHandler> ImplClient for BrowserProcessClientImpl<H> {
_frame: Option<&mut cef::Frame>,
_source_process: cef::ProcessId,
message: Option<&mut cef::ProcessMessage>,
) -> ::std::os::raw::c_int {
) -> std::ffi::c_int {
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
match unpacked_message {
Some(UnpackedMessage {
message_type: MessageType::Initialized,
data: _,
}) => self.event_handler.initialized_web_communication(),
Some(UnpackedMessage {
message_type: MessageType::SendToNative,
data,
@ -45,12 +57,24 @@ impl<H: CefEventHandler> ImplClient for BrowserProcessClientImpl<H> {
1
}
fn load_handler(&self) -> Option<cef::LoadHandler> {
Some(self.load_handler.clone())
}
fn render_handler(&self) -> Option<RenderHandler> {
Some(self.render_handler.clone())
}
fn life_span_handler(&self) -> Option<cef::LifeSpanHandler> {
Some(LifeSpanHandler::new(BrowserProcessLifeSpanHandlerImpl::new()))
Some(LifeSpanHandler::new(LifeSpanHandlerImpl::new()))
}
fn display_handler(&self) -> Option<cef::DisplayHandler> {
Some(self.display_handler.clone())
}
fn context_menu_handler(&self) -> Option<cef::ContextMenuHandler> {
Some(ContextMenuHandler::new(ContextMenuHandlerImpl::new()))
}
fn get_raw(&self) -> *mut _cef_client_t {
@ -66,8 +90,10 @@ impl<H: CefEventHandler> Clone for BrowserProcessClientImpl<H> {
}
Self {
object: self.object,
event_handler: self.event_handler.duplicate(),
load_handler: self.load_handler.clone(),
render_handler: self.render_handler.clone(),
event_handler: self.event_handler.clone(),
display_handler: self.display_handler.clone(),
}
}
}

View file

@ -2,10 +2,9 @@ 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 cef::{CefString, ImplBrowserProcessHandler, WrapBrowserProcessHandler};
use crate::cef::CefEventHandler;
use crate::cef::scheme_handler::{GRAPHITE_SCHEME, GraphiteSchemeHandlerFactory};
pub(crate) struct BrowserProcessHandlerImpl<H: CefEventHandler> {
object: *mut RcImpl<cef_browser_process_handler_t, Self>,
@ -20,16 +19,12 @@ impl<H: CefEventHandler> BrowserProcessHandlerImpl<H> {
}
}
impl<H: CefEventHandler + Clone> ImplBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
fn on_context_initialized(&self) {
cef::register_scheme_handler_factory(Some(&CefString::from(GRAPHITE_SCHEME)), None, Some(&mut SchemeHandlerFactory::new(GraphiteSchemeHandlerFactory::new())));
}
impl<H: CefEventHandler> ImplBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
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 {
fn on_already_running_app_relaunch(&self, _command_line: Option<&mut cef::CommandLine>, _current_directory: Option<&CefString>) -> std::ffi::c_int {
1 // Return 1 to prevent default behavior of opening a empty browser window
}
@ -38,7 +33,7 @@ impl<H: CefEventHandler + Clone> ImplBrowserProcessHandler for BrowserProcessHan
}
}
impl<H: CefEventHandler + Clone> Clone for BrowserProcessHandlerImpl<H> {
impl<H: CefEventHandler> Clone for BrowserProcessHandlerImpl<H> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
@ -46,7 +41,7 @@ impl<H: CefEventHandler + Clone> Clone for BrowserProcessHandlerImpl<H> {
}
Self {
object: self.object,
event_handler: self.event_handler.clone(),
event_handler: self.event_handler.duplicate(),
}
}
}
@ -58,7 +53,7 @@ impl<H: CefEventHandler> Rc for BrowserProcessHandlerImpl<H> {
}
}
}
impl<H: CefEventHandler + Clone> WrapBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
impl<H: CefEventHandler> 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,66 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_context_menu_handler_t, cef_base_ref_counted_t};
use cef::{ImplContextMenuHandler, WrapContextMenuHandler};
pub(crate) struct ContextMenuHandlerImpl {
object: *mut RcImpl<_cef_context_menu_handler_t, Self>,
}
impl ContextMenuHandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
}
impl ImplContextMenuHandler for ContextMenuHandlerImpl {
fn run_context_menu(
&self,
_browser: Option<&mut cef::Browser>,
_frame: Option<&mut cef::Frame>,
_params: Option<&mut cef::ContextMenuParams>,
_model: Option<&mut cef::MenuModel>,
_callback: Option<&mut cef::RunContextMenuCallback>,
) -> std::ffi::c_int {
// Prevent context menu
1
}
fn run_quick_menu(
&self,
_browser: Option<&mut cef::Browser>,
_frame: Option<&mut cef::Frame>,
_location: Option<&cef::Point>,
_size: Option<&cef::Size>,
_edit_state_flags: cef::QuickMenuEditStateFlags,
_callback: Option<&mut cef::RunQuickMenuCallback>,
) -> std::ffi::c_int {
// Prevent quick menu
1
}
fn get_raw(&self) -> *mut _cef_context_menu_handler_t {
self.object.cast()
}
}
impl Clone for ContextMenuHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl Rc for ContextMenuHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapContextMenuHandler for ContextMenuHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_context_menu_handler_t, Self>) {
self.object = object;
}
}

View file

@ -0,0 +1,147 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_display_handler_t, cef_base_ref_counted_t, cef_cursor_type_t::*, cef_log_severity_t::*};
use cef::{CefString, ImplDisplayHandler, Point, Size, WrapDisplayHandler};
use winit::cursor::CursorIcon;
use crate::cef::CefEventHandler;
pub(crate) struct DisplayHandlerImpl<H: CefEventHandler> {
object: *mut RcImpl<_cef_display_handler_t, Self>,
event_handler: H,
}
impl<H: CefEventHandler> DisplayHandlerImpl<H> {
pub fn new(event_handler: H) -> Self {
Self {
object: std::ptr::null_mut(),
event_handler,
}
}
}
#[cfg(not(target_os = "macos"))]
type CefCursorHandle = cef::CursorHandle;
#[cfg(target_os = "macos")]
type CefCursorHandle = *mut u8;
impl<H: CefEventHandler> ImplDisplayHandler for DisplayHandlerImpl<H> {
fn on_cursor_change(&self, _browser: Option<&mut cef::Browser>, _cursor: CefCursorHandle, cursor_type: cef::CursorType, custom_cursor_info: Option<&cef::CursorInfo>) -> std::ffi::c_int {
if let Some(custom_cursor_info) = custom_cursor_info {
let Size { width, height } = custom_cursor_info.size;
let Point { x: hotspot_x, y: hotspot_y } = custom_cursor_info.hotspot;
let buffer_size = (width * height * 4) as usize;
let buffer_ptr = custom_cursor_info.buffer as *const u8;
if !buffer_ptr.is_null() && buffer_ptr.align_offset(std::mem::align_of::<u8>()) == 0 {
let buffer = unsafe { std::slice::from_raw_parts(buffer_ptr, buffer_size) }.to_vec();
let cursor = winit::cursor::CustomCursorSource::from_rgba(buffer, width as u16, height as u16, hotspot_x as u16, hotspot_y as u16).unwrap();
self.event_handler.cursor_change(cursor.into());
return 1; // We handled the cursor change.
}
}
let cursor = match cursor_type.into() {
CT_POINTER => CursorIcon::Default,
CT_CROSS => CursorIcon::Crosshair,
CT_HAND => CursorIcon::Pointer,
CT_IBEAM => CursorIcon::Text,
CT_WAIT => CursorIcon::Wait,
CT_HELP => CursorIcon::Help,
CT_EASTRESIZE => CursorIcon::EResize,
CT_NORTHRESIZE => CursorIcon::NResize,
CT_NORTHEASTRESIZE => CursorIcon::NeResize,
CT_NORTHWESTRESIZE => CursorIcon::NwResize,
CT_SOUTHRESIZE => CursorIcon::SResize,
CT_SOUTHEASTRESIZE => CursorIcon::SeResize,
CT_SOUTHWESTRESIZE => CursorIcon::SwResize,
CT_WESTRESIZE => CursorIcon::WResize,
CT_NORTHSOUTHRESIZE => CursorIcon::NsResize,
CT_EASTWESTRESIZE => CursorIcon::EwResize,
CT_NORTHEASTSOUTHWESTRESIZE => CursorIcon::NeswResize,
CT_NORTHWESTSOUTHEASTRESIZE => CursorIcon::NwseResize,
CT_COLUMNRESIZE => CursorIcon::ColResize,
CT_ROWRESIZE => CursorIcon::RowResize,
CT_MIDDLEPANNING => CursorIcon::AllScroll,
CT_EASTPANNING => CursorIcon::AllScroll,
CT_NORTHPANNING => CursorIcon::AllScroll,
CT_NORTHEASTPANNING => CursorIcon::AllScroll,
CT_NORTHWESTPANNING => CursorIcon::AllScroll,
CT_SOUTHPANNING => CursorIcon::AllScroll,
CT_SOUTHEASTPANNING => CursorIcon::AllScroll,
CT_SOUTHWESTPANNING => CursorIcon::AllScroll,
CT_WESTPANNING => CursorIcon::AllScroll,
CT_MOVE => CursorIcon::Move,
CT_VERTICALTEXT => CursorIcon::VerticalText,
CT_CELL => CursorIcon::Cell,
CT_CONTEXTMENU => CursorIcon::ContextMenu,
CT_ALIAS => CursorIcon::Alias,
CT_PROGRESS => CursorIcon::Progress,
CT_NODROP => CursorIcon::NoDrop,
CT_COPY => CursorIcon::Copy,
CT_NONE => CursorIcon::Default,
CT_NOTALLOWED => CursorIcon::NotAllowed,
CT_ZOOMIN => CursorIcon::ZoomIn,
CT_ZOOMOUT => CursorIcon::ZoomOut,
CT_GRAB => CursorIcon::Grab,
CT_GRABBING => CursorIcon::Grabbing,
CT_MIDDLE_PANNING_VERTICAL => CursorIcon::AllScroll,
CT_MIDDLE_PANNING_HORIZONTAL => CursorIcon::AllScroll,
CT_DND_NONE => CursorIcon::Default,
CT_DND_MOVE => CursorIcon::Move,
CT_DND_COPY => CursorIcon::Copy,
CT_DND_LINK => CursorIcon::Alias,
CT_NUM_VALUES => CursorIcon::Default,
_ => CursorIcon::Default,
};
self.event_handler.cursor_change(cursor.into());
1 // We handled the cursor change.
}
fn on_console_message(&self, _browser: Option<&mut cef::Browser>, level: cef::LogSeverity, message: Option<&CefString>, source: Option<&CefString>, line: std::ffi::c_int) -> std::ffi::c_int {
let message = message.map(|m| m.to_string()).unwrap_or_default();
let source = source.map(|s| s.to_string()).unwrap_or_default();
let line = line as i64;
let browser_source = format!("{source}:{line}");
static BROWSER: &str = "browser";
match level.as_ref() {
LOGSEVERITY_FATAL | LOGSEVERITY_ERROR => tracing::error!(target: BROWSER, "{browser_source} {message}"),
LOGSEVERITY_WARNING => tracing::warn!(target: BROWSER, "{browser_source} {message}"),
LOGSEVERITY_INFO => tracing::info!(target: BROWSER, "{browser_source} {message}"),
LOGSEVERITY_DEFAULT | LOGSEVERITY_VERBOSE => tracing::debug!(target: BROWSER, "{browser_source} {message}"),
_ => tracing::trace!(target: BROWSER, "{browser_source} {message}"),
}
0
}
fn get_raw(&self) -> *mut _cef_display_handler_t {
self.object.cast()
}
}
impl<H: CefEventHandler> Clone for DisplayHandlerImpl<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.duplicate(),
}
}
}
impl<H: CefEventHandler> Rc for DisplayHandlerImpl<H> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<H: CefEventHandler> WrapDisplayHandler for DisplayHandlerImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_display_handler_t, Self>) {
self.object = object;
}
}

View file

@ -2,32 +2,32 @@ 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 {
pub(crate) struct LifeSpanHandlerImpl {
object: *mut RcImpl<_cef_life_span_handler_t, Self>,
}
impl BrowserProcessLifeSpanHandlerImpl {
impl LifeSpanHandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
}
impl ImplLifeSpanHandler for BrowserProcessLifeSpanHandlerImpl {
impl ImplLifeSpanHandler for LifeSpanHandlerImpl {
fn on_before_popup(
&self,
_browser: Option<&mut cef::Browser>,
_frame: Option<&mut cef::Frame>,
_popup_id: ::std::os::raw::c_int,
_popup_id: std::ffi::c_int,
target_url: Option<&cef::CefString>,
_target_frame_name: Option<&cef::CefString>,
_target_disposition: cef::WindowOpenDisposition,
_user_gesture: ::std::os::raw::c_int,
_user_gesture: std::ffi::c_int,
_popup_features: Option<&cef::PopupFeatures>,
_window_info: Option<&mut cef::WindowInfo>,
_client: Option<&mut Option<impl cef::ImplClient>>,
_client: Option<&mut Option<cef::Client>>,
_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 {
_no_javascript_access: Option<&mut std::ffi::c_int>,
) -> std::ffi::c_int {
let target = target_url.map(|url| url.to_string()).unwrap_or("unknown".to_string());
tracing::error!("Browser tried to open a popup at URL: {}", target);
@ -40,7 +40,7 @@ impl ImplLifeSpanHandler for BrowserProcessLifeSpanHandlerImpl {
}
}
impl Clone for BrowserProcessLifeSpanHandlerImpl {
impl Clone for LifeSpanHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
@ -49,7 +49,7 @@ impl Clone for BrowserProcessLifeSpanHandlerImpl {
Self { object: self.object }
}
}
impl Rc for BrowserProcessLifeSpanHandlerImpl {
impl Rc for LifeSpanHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
@ -57,7 +57,7 @@ impl Rc for BrowserProcessLifeSpanHandlerImpl {
}
}
}
impl WrapLifeSpanHandler for BrowserProcessLifeSpanHandlerImpl {
impl WrapLifeSpanHandler for LifeSpanHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_life_span_handler_t, Self>) {
self.object = object;
}

View file

@ -0,0 +1,60 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_load_handler_t, cef_base_ref_counted_t, cef_load_handler_t};
use cef::{ImplBrowser, ImplBrowserHost, ImplLoadHandler, WrapLoadHandler};
use crate::cef::CefEventHandler;
pub(crate) struct LoadHandlerImpl<H: CefEventHandler> {
object: *mut RcImpl<cef_load_handler_t, Self>,
event_handler: H,
}
impl<H: CefEventHandler> LoadHandlerImpl<H> {
pub(crate) fn new(event_handler: H) -> Self {
Self {
object: std::ptr::null_mut(),
event_handler,
}
}
}
impl<H: CefEventHandler> ImplLoadHandler for LoadHandlerImpl<H> {
fn on_loading_state_change(&self, browser: Option<&mut cef::Browser>, is_loading: std::ffi::c_int, _can_go_back: std::ffi::c_int, _can_go_forward: std::ffi::c_int) {
let view_info = self.event_handler.view_info();
if let Some(browser) = browser
&& is_loading == 0
{
browser.host().unwrap().set_zoom_level(view_info.zoom());
}
}
fn get_raw(&self) -> *mut _cef_load_handler_t {
self.object.cast()
}
}
impl<H: CefEventHandler> Clone for LoadHandlerImpl<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.duplicate(),
}
}
}
impl<H: CefEventHandler> Rc for LoadHandlerImpl<H> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<H: CefEventHandler> WrapLoadHandler for LoadHandlerImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_load_handler_t, Self>) {
self.object = object;
}
}

View file

@ -17,29 +17,21 @@ impl<H: CefEventHandler> RenderHandlerImpl<H> {
}
}
}
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();
let view_info = self.event_handler.view_info();
*rect = Rect {
x: 0,
y: 0,
width: view.width as i32,
height: view.height as i32,
width: view_info.width() as i32,
height: view_info.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,
) {
fn on_paint(&self, _browser: Option<&mut Browser>, _type_: PaintElementType, _dirty_rects: Option<&[Rect]>, buffer: *const u8, width: std::ffi::c_int, height: std::ffi::c_int) {
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");
@ -47,6 +39,23 @@ impl<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
self.event_handler.draw(frame_buffer)
}
#[cfg(feature = "accelerated_paint")]
fn on_accelerated_paint(&self, _browser: Option<&mut Browser>, type_: PaintElementType, _dirty_rects: Option<&[Rect]>, info: Option<&cef::AcceleratedPaintInfo>) {
use cef::osr_texture_import::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()
}
@ -60,7 +69,7 @@ impl<H: CefEventHandler> Clone for RenderHandlerImpl<H> {
}
Self {
object: self.object,
event_handler: self.event_handler.clone(),
event_handler: self.event_handler.duplicate(),
}
}
}

View file

@ -3,13 +3,14 @@ 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 crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
use super::scheme_handler_factory::SchemeHandlerFactoryImpl;
use crate::cef::CefEventHandler;
pub(crate) struct RenderProcessAppImpl {
pub(crate) struct RenderProcessAppImpl<H: CefEventHandler> {
object: *mut RcImpl<_cef_app_t, Self>,
render_process_handler: RenderProcessHandler,
}
impl RenderProcessAppImpl {
impl<H: CefEventHandler> RenderProcessAppImpl<H> {
pub(crate) fn app() -> App {
App::new(Self {
object: std::ptr::null_mut(),
@ -18,9 +19,9 @@ impl RenderProcessAppImpl {
}
}
impl ImplApp for RenderProcessAppImpl {
impl<H: CefEventHandler> ImplApp for RenderProcessAppImpl<H> {
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
GraphiteSchemeHandlerFactory::register_schemes(registrar);
SchemeHandlerFactoryImpl::<H>::register_schemes(registrar);
}
fn render_process_handler(&self) -> Option<RenderProcessHandler> {
@ -32,7 +33,7 @@ impl ImplApp for RenderProcessAppImpl {
}
}
impl Clone for RenderProcessAppImpl {
impl<H: CefEventHandler> Clone for RenderProcessAppImpl<H> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
@ -44,7 +45,7 @@ impl Clone for RenderProcessAppImpl {
}
}
}
impl Rc for RenderProcessAppImpl {
impl<H: CefEventHandler> Rc for RenderProcessAppImpl<H> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
@ -52,7 +53,7 @@ impl Rc for RenderProcessAppImpl {
}
}
}
impl WrapApp for RenderProcessAppImpl {
impl<H: CefEventHandler> WrapApp for RenderProcessAppImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
self.object = object;
}

View file

@ -4,7 +4,7 @@ use cef::{CefString, ImplFrame, ImplRenderProcessHandler, ImplV8Context, ImplV8V
use crate::cef::ipc::{MessageType, UnpackMessage, UnpackedMessage};
use super::render_process_v8_handler::BrowserProcessV8HandlerImpl;
use super::render_process_v8_handler::RenderProcessV8HandlerImpl;
pub(crate) struct RenderProcessHandlerImpl {
object: *mut RcImpl<cef_render_process_handler_t, Self>,
@ -22,7 +22,7 @@ impl ImplRenderProcessHandler for RenderProcessHandlerImpl {
frame: Option<&mut cef::Frame>,
_source_process: cef::ProcessId,
message: Option<&mut cef::ProcessMessage>,
) -> ::std::os::raw::c_int {
) -> std::ffi::c_int {
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
match unpacked_message {
Some(UnpackedMessage {
@ -76,24 +76,30 @@ impl ImplRenderProcessHandler for RenderProcessHandlerImpl {
}
fn on_context_created(&self, _browser: Option<&mut cef::Browser>, _frame: Option<&mut cef::Frame>, context: Option<&mut cef::V8Context>) {
let function_name = "sendNativeMessage";
let register_js_function = |context: &mut cef::V8Context, name: &'static str| {
let mut v8_handler = V8Handler::new(RenderProcessV8HandlerImpl::new());
let Some(mut function) = v8_value_create_function(Some(&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 mut v8_handler = V8Handler::new(BrowserProcessV8HandlerImpl::new());
let Some(mut function) = v8_value_create_function(Some(&CefString::from(function_name)), Some(&mut v8_handler)) else {
tracing::error!("Failed to create V8 function {function_name}");
return;
};
let initialized_function_name = "initializeNativeCommunication";
let send_function_name = "sendNativeMessage";
let Some(global) = context.global() else {
tracing::error!("Global object is not available in V8 context");
return;
};
global.set_value_bykey(Some(&CefString::from(function_name)), Some(&mut function), V8Propertyattribute::default());
register_js_function(context, initialized_function_name);
register_js_function(context, send_function_name);
}
fn get_raw(&self) -> *mut _cef_render_process_handler_t {

View file

@ -2,17 +2,16 @@ use cef::{ImplV8Handler, ImplV8Value, V8Value, WrapV8Handler, rc::Rc, v8_context
use crate::cef::ipc::{MessageType, SendMessage};
pub struct BrowserProcessV8HandlerImpl {
pub struct RenderProcessV8HandlerImpl {
object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>,
}
impl BrowserProcessV8HandlerImpl {
impl RenderProcessV8HandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
}
impl ImplV8Handler for BrowserProcessV8HandlerImpl {
impl ImplV8Handler for RenderProcessV8HandlerImpl {
fn execute(
&self,
name: Option<&cef::CefString>,
@ -20,9 +19,12 @@ impl ImplV8Handler for BrowserProcessV8HandlerImpl {
arguments: Option<&[Option<V8Value>]>,
_retval: Option<&mut Option<V8Value>>,
_exception: Option<&mut cef::CefString>,
) -> ::std::os::raw::c_int {
if let Some(name) = name {
if name.to_string() == "sendNativeMessage" {
) -> std::ffi::c_int {
match name.map(|s| s.to_string()).unwrap_or_default().as_str() {
"initializeNativeCommunication" => {
v8_context_get_current_context().send_message(MessageType::Initialized, vec![0u8].as_slice());
}
"sendNativeMessage" => {
let Some(args) = arguments else {
tracing::error!("No arguments provided to sendNativeMessage");
return 0;
@ -48,6 +50,9 @@ impl ImplV8Handler for BrowserProcessV8HandlerImpl {
return 1;
}
name => {
tracing::error!("Unknown V8 function called: {}", name);
}
}
1
}
@ -57,7 +62,7 @@ impl ImplV8Handler for BrowserProcessV8HandlerImpl {
}
}
impl Clone for BrowserProcessV8HandlerImpl {
impl Clone for RenderProcessV8HandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
@ -66,8 +71,7 @@ impl Clone for BrowserProcessV8HandlerImpl {
Self { object: self.object }
}
}
impl Rc for BrowserProcessV8HandlerImpl {
impl Rc for RenderProcessV8HandlerImpl {
fn as_base(&self) -> &cef::sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
@ -75,8 +79,7 @@ impl Rc for BrowserProcessV8HandlerImpl {
}
}
}
impl WrapV8Handler for BrowserProcessV8HandlerImpl {
impl WrapV8Handler for RenderProcessV8HandlerImpl {
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,74 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_scheme_handler_factory_t, cef_base_ref_counted_t, cef_scheme_options_t};
use cef::{Browser, CefString, Frame, ImplRequest, ImplSchemeHandlerFactory, ImplSchemeRegistrar, Request, ResourceHandler, SchemeRegistrar, WrapSchemeHandlerFactory};
use super::resource_handler::ResourceHandlerImpl;
use crate::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(request) = request {
let url = CefString::from(&request.url()).to_string();
let path = url
.strip_prefix(&format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/"))
.expect("CEF should only call this for our custom scheme and domain that we registered this factory for");
let resource = self.event_handler.load_resource(path.to_string().into());
return Some(ResourceHandler::new(ResourceHandlerImpl::new(resource)));
}
None
}
fn get_raw(&self) -> *mut _cef_scheme_handler_factory_t {
self.object.cast()
}
}
impl<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.duplicate(),
}
}
}
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;
}
}

View file

@ -1,14 +1,17 @@
use cef::{CefString, Frame, ImplBinaryValue, ImplBrowser, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Context, ProcessId, V8Context, sys::cef_process_id_t};
use super::{Context, Initialized};
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(),
@ -24,6 +27,7 @@ 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(()),
@ -39,21 +43,6 @@ pub(crate) struct MessageInfo {
pub(crate) trait SendMessage {
fn send_message(&self, message_type: MessageType, message: &[u8]);
}
impl SendMessage for Context<Initialized> {
fn send_message(&self, message_type: MessageType, message: &[u8]) {
let Some(browser) = &self.browser else {
tracing::error!("Browser is not initialized, cannot send message");
return;
};
let Some(frame) = browser.main_frame() else {
tracing::error!("Main frame is not available, cannot send message");
return;
};
frame.send_message(message_type, message);
}
}
impl SendMessage for Option<V8Context> {
fn send_message(&self, message_type: MessageType, message: &[u8]) {
let Some(context) = self else {

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

@ -1,223 +0,0 @@
use std::cell::RefCell;
use std::ffi::c_int;
use std::ops::DerefMut;
use std::slice::Iter;
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_resource_handler_t, _cef_scheme_handler_factory_t, cef_base_ref_counted_t, cef_scheme_options_t};
use cef::{
Browser, Callback, CefString, Frame, ImplRequest, ImplResourceHandler, ImplResponse, ImplSchemeHandlerFactory, ImplSchemeRegistrar, Request, ResourceHandler, ResourceReadCallback, Response,
SchemeRegistrar, WrapResourceHandler, WrapSchemeHandlerFactory,
};
use include_dir::{Dir, include_dir};
pub(crate) const GRAPHITE_SCHEME: &str = "graphite-static";
pub(crate) const FRONTEND_DOMAIN: &str = "frontend";
pub(crate) struct GraphiteSchemeHandlerFactory {
object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>,
}
impl GraphiteSchemeHandlerFactory {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
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(GRAPHITE_SCHEME)), scheme_options);
}
}
}
impl ImplSchemeHandlerFactory for GraphiteSchemeHandlerFactory {
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() != GRAPHITE_SCHEME {
return None;
}
if let Some(request) = request {
let url = CefString::from(&request.url()).to_string();
let path = url.strip_prefix(&format!("{GRAPHITE_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 {
FRONTEND_DOMAIN => {
if path.is_empty() {
Some(ResourceHandler::new(GraphiteFrontendResourceHandler::new("index.html")))
} else {
Some(ResourceHandler::new(GraphiteFrontendResourceHandler::new(path)))
}
}
_ => None,
};
}
return None;
}
None
}
fn get_raw(&self) -> *mut _cef_scheme_handler_factory_t {
self.object.cast()
}
}
static FRONTEND: Dir = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist");
struct GraphiteFrontendResourceHandler<'a> {
object: *mut RcImpl<_cef_resource_handler_t, Self>,
data: Option<RefCell<Iter<'a, u8>>>,
mimetype: Option<String>,
}
impl<'a> GraphiteFrontendResourceHandler<'a> {
pub fn new(path: &str) -> Self {
let file = FRONTEND.get_file(path);
let data = if let Some(file) = file {
Some(RefCell::new(file.contents().iter()))
} else {
tracing::error!("Failed to find asset at path: {}", path);
None
};
let mimetype = if let Some(file) = file {
let ext = file.path().extension().and_then(|s| s.to_str()).unwrap_or("");
// We know what file types will be in the assets this should be fine
match ext {
"html" => Some("text/html".to_string()),
"css" => Some("text/css".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,
}
} else {
None
};
Self {
object: std::ptr::null_mut(),
data,
mimetype,
}
}
}
impl<'a> ImplResourceHandler for GraphiteFrontendResourceHandler<'a> {
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.data.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 mut read = 0;
let out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read as usize) };
if let Some(data) = &self.data {
let mut data = data.borrow_mut();
for (out, &data) in out.iter_mut().zip(data.deref_mut()) {
*out = data;
read += 1;
}
}
if let Some(bytes_read) = bytes_read {
*bytes_read = read;
}
if read > 0 {
1 // Indicating that data was read
} else {
0 // Indicating no data was read
}
}
fn get_raw(&self) -> *mut _cef_resource_handler_t {
self.object.cast()
}
}
impl WrapSchemeHandlerFactory for GraphiteSchemeHandlerFactory {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>) {
self.object = object;
}
}
impl<'a> WrapResourceHandler for GraphiteFrontendResourceHandler<'a> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_resource_handler_t, Self>) {
self.object = object;
}
}
impl Clone for GraphiteSchemeHandlerFactory {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl<'a> Clone for GraphiteFrontendResourceHandler<'a> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
data: self.data.clone(),
mimetype: self.mimetype.clone(),
}
}
}
impl Rc for GraphiteSchemeHandlerFactory {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<'a> Rc for GraphiteFrontendResourceHandler<'a> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}

9
desktop/src/cli.rs Normal file
View file

@ -0,0 +1,9 @@
#[derive(clap::Parser)]
#[clap(name = "graphite", version)]
pub struct Cli {
#[arg(help = "Files to open on startup")]
pub files: Vec<std::path::PathBuf>,
#[arg(long, action = clap::ArgAction::SetTrue, help = "Disable hardware accelerated UI rendering")]
pub disable_ui_acceleration: bool,
}

View file

@ -1,3 +1,13 @@
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) const APP_NAME: &str = "Graphite";
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub(crate) const APP_ID: &str = "art.graphite.Graphite";
pub(crate) const APP_DIRECTORY_NAME: &str = "graphite";
pub(crate) const APP_LOCK_FILE_NAME: &str = "instance.lock";
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron";
pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents";
// CEF configuration constants
pub(crate) const CEF_WINDOWLESS_FRAME_RATE: i32 = 60;
pub(crate) const CEF_MESSAGE_LOOP_MAX_ITERATIONS: usize = 10;

View file

@ -1,26 +0,0 @@
use std::path::PathBuf;
use rfd::AsyncFileDialog;
pub(crate) async fn dialog_open_graphite_file() -> Option<PathBuf> {
AsyncFileDialog::new()
.add_filter("Graphite", &["graphite"])
.set_title("Open Graphite Document")
.pick_file()
.await
.map(|f| f.path().to_path_buf())
}
pub(crate) async fn dialog_save_graphite_file(name: String) -> Option<PathBuf> {
AsyncFileDialog::new()
.add_filter("Graphite", &["graphite"])
.set_title("Save Graphite Document")
.set_file_name(name)
.save_file()
.await
.map(|f| f.path().to_path_buf())
}
pub(crate) async fn dialog_save_file(name: String) -> Option<PathBuf> {
AsyncFileDialog::new().set_title("Save File").set_file_name(name).save_file().await.map(|f| f.path().to_path_buf())
}

View file

@ -1,7 +1,7 @@
use std::fs::create_dir_all;
use std::path::PathBuf;
use crate::consts::APP_DIRECTORY_NAME;
use crate::consts::{APP_DIRECTORY_NAME, APP_DOCUMENTS_DIRECTORY_NAME};
pub(crate) fn ensure_dir_exists(path: &PathBuf) {
if !path.exists() {
@ -9,8 +9,14 @@ pub(crate) fn ensure_dir_exists(path: &PathBuf) {
}
}
pub(crate) fn graphite_data_dir() -> PathBuf {
pub(crate) fn app_data_dir() -> PathBuf {
let path = dirs::data_dir().expect("Failed to get data directory").join(APP_DIRECTORY_NAME);
ensure_dir_exists(&path);
path
}
pub(crate) fn app_autosave_documents_dir() -> PathBuf {
let path = app_data_dir().join(APP_DOCUMENTS_DIRECTORY_NAME);
ensure_dir_exists(&path);
path
}

39
desktop/src/event.rs Normal file
View file

@ -0,0 +1,39 @@
use crate::wrapper::NodeGraphExecutionResult;
use crate::wrapper::messages::DesktopWrapperMessage;
pub(crate) enum AppEvent {
UiUpdate(wgpu::Texture),
CursorChange(crate::window::Cursor),
ScheduleBrowserWork(std::time::Instant),
WebCommunicationInitialized,
DesktopWrapperMessage(DesktopWrapperMessage),
NodeGraphExecutionResult(NodeGraphExecutionResult),
CloseWindow,
#[cfg(target_os = "macos")]
MenuEvent {
id: String,
},
}
#[derive(Clone)]
pub(crate) struct AppEventScheduler {
pub(crate) proxy: winit::event_loop::EventLoopProxy,
pub(crate) sender: std::sync::mpsc::Sender<AppEvent>,
}
impl AppEventScheduler {
pub(crate) fn schedule(&self, event: AppEvent) {
let _ = self.sender.send(event);
self.proxy.wake_up();
}
}
pub(crate) trait CreateAppEventSchedulerEventLoopExt {
fn create_app_event_scheduler(&self, sender: std::sync::mpsc::Sender<AppEvent>) -> AppEventScheduler;
}
impl CreateAppEventSchedulerEventLoopExt for winit::event_loop::EventLoop {
fn create_app_event_scheduler(&self, sender: std::sync::mpsc::Sender<AppEvent>) -> AppEventScheduler {
AppEventScheduler { proxy: self.create_proxy(), sender }
}
}

View file

@ -0,0 +1,23 @@
use crate::wrapper::{WgpuContext, WgpuContextBuilder, WgpuFeatures};
pub(super) async fn create_wgpu_context() -> WgpuContext {
let wgpu_context_builder = WgpuContextBuilder::new().with_features(WgpuFeatures::PUSH_CONSTANTS);
// TODO: add a cli flag to list adapters and exit instead of always printing
println!("\nAvailable WGPU adapters:\n{}", wgpu_context_builder.available_adapters_fmt().await);
// TODO: make this configurable via cli flags instead
let wgpu_context = match std::env::var("GRAPHITE_WGPU_ADAPTER").ok().and_then(|s| s.parse().ok()) {
None => wgpu_context_builder.build().await,
Some(adapter_index) => {
tracing::info!("Overriding WGPU adapter selection with adapter index {adapter_index}");
wgpu_context_builder.build_with_adapter_selection(|_| Some(adapter_index)).await
}
}
.expect("Failed to create WGPU context");
// TODO: add a cli flag to list adapters and exit instead of always printing
println!("Using WGPU adapter: {:?}", wgpu_context.adapter.get_info());
wgpu_context
}

102
desktop/src/lib.rs Normal file
View file

@ -0,0 +1,102 @@
use clap::Parser;
use std::process::exit;
use tracing_subscriber::EnvFilter;
use winit::event_loop::EventLoop;
pub(crate) mod consts;
mod app;
mod cef;
mod cli;
mod dirs;
mod event;
mod persist;
mod render;
mod window;
mod gpu_context;
pub(crate) use graphite_desktop_wrapper as wrapper;
use app::App;
use cef::CefHandler;
use cli::Cli;
use event::CreateAppEventSchedulerEventLoopExt;
use crate::consts::APP_LOCK_FILE_NAME;
pub fn start() {
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::warn!("Cef subprocess failed with error: {error}");
return;
}
let mut lock = pidlock::Pidlock::new_validated(dirs::app_data_dir().join(APP_LOCK_FILE_NAME)).unwrap();
match lock.acquire() {
Ok(lock) => {
tracing::info!("Acquired application lock");
lock
}
Err(pidlock::PidlockError::LockExists) => {
tracing::error!("Another instance is already running, Exiting.");
exit(0);
}
Err(err) => {
tracing::error!("Failed to acquire application lock: {err}");
exit(1);
}
};
App::init();
let cli = Cli::parse();
let wgpu_context = futures::executor::block_on(gpu_context::create_wgpu_context());
let event_loop = EventLoop::new().unwrap();
let (app_event_sender, app_event_receiver) = std::sync::mpsc::channel();
let app_event_scheduler = event_loop.create_app_event_scheduler(app_event_sender);
let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel();
let cef_handler = cef::CefHandler::new(wgpu_context.clone(), app_event_scheduler.clone(), cef_view_info_receiver);
let cef_context = match cef_context_builder.initialize(cef_handler, cli.disable_ui_acceleration) {
Ok(c) => {
tracing::info!("CEF initialized successfully");
c
}
Err(cef::InitError::AlreadyRunning) => {
tracing::error!("Another instance is already running, Exiting.");
exit(1);
}
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);
}
Err(cef::InitError::RequestContextCreationFailed) => {
tracing::error!("Failed to create CEF request context");
exit(1);
}
};
let mut app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, cli.files);
event_loop.run_app(&mut app).unwrap();
}
pub fn start_helper() {
let cef_context_builder = cef::CefContextBuilder::<CefHandler>::new_helper();
assert!(cef_context_builder.is_sub_process());
cef_context_builder.execute_sub_process();
}

View file

@ -1,82 +1,3 @@
use std::process::exit;
use std::time::Instant;
use std::{fmt::Debug, time::Duration};
use graphite_editor::messages::prelude::Message;
use tracing_subscriber::EnvFilter;
use winit::event_loop::EventLoop;
pub(crate) mod consts;
mod cef;
use cef::{Setup, WindowSize};
mod render;
use render::WgpuContext;
mod app;
use app::WinitApp;
mod dirs;
mod dialogs;
#[derive(Debug)]
pub(crate) enum CustomEvent {
UiUpdate(wgpu::Texture),
ScheduleBrowserWork(Instant),
DispatchMessage(Message),
MessageReceived(Message),
NodeGraphRan(Option<wgpu::Texture>),
}
fn main() {
tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init();
let cef_context = match cef::Context::<Setup>::new() {
Ok(c) => c,
Err(cef::SetupError::Subprocess) => exit(0),
Err(cef::SetupError::SubprocessFailed(t)) => {
tracing::error!("Subprocess of type {t} failed");
exit(1);
}
};
let event_loop = EventLoop::<CustomEvent>::with_user_event().build().unwrap();
let (window_size_sender, window_size_receiver) = std::sync::mpsc::channel();
let wgpu_context = futures::executor::block_on(WgpuContext::new()).unwrap();
let cef_context = match cef_context.init(cef::CefHandler::new(window_size_receiver, event_loop.create_proxy(), wgpu_context.clone())) {
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);
}
};
tracing::info!("Cef initialized successfully");
let rendering_loop_proxy = event_loop.create_proxy();
let target_fps = 60;
std::thread::spawn(move || {
loop {
let last_render = Instant::now();
let (has_run, texture) = futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph());
if has_run {
let _ = rendering_loop_proxy.send_event(CustomEvent::NodeGraphRan(texture.map(|t| (*t.texture).clone())));
}
let frame_time = Duration::from_secs_f32((target_fps as f32).recip());
let sleep = last_render + frame_time - Instant::now();
std::thread::sleep(sleep);
}
});
let mut winit_app = WinitApp::new(cef_context, window_size_sender, wgpu_context, event_loop.create_proxy());
event_loop.run_app(&mut winit_app).unwrap();
graphite_desktop::start();
}

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

@ -0,0 +1,214 @@
use crate::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>>,
}
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);
}
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::app_data_dir();
path.push(crate::consts::APP_STATE_FILE_NAME);
path
}
fn preferences_file_path() -> std::path::PathBuf {
let mut path = crate::dirs::app_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::app_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,
}
}
}

View file

@ -1,5 +1,5 @@
mod frame_buffer_ref;
pub(crate) use frame_buffer_ref::FrameBufferRef;
mod graphics_state;
pub(crate) use graphics_state::{GraphicsState, WgpuContext};
mod state;
pub(crate) use state::{RenderError, RenderState};

View file

@ -8,15 +8,9 @@ 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
vec2f( -1.0, -1.0),
vec2f( 3.0, -1.0),
vec2f( -1.0, 3.0),
);
let xy = pos[vertex_index];
out.clip_position = vec4f(xy , 0.0, 1.0);
@ -29,6 +23,8 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
struct Constants {
viewport_scale: vec2<f32>,
viewport_offset: vec2<f32>,
ui_scale: vec2<f32>,
background_color: vec4<f32>,
};
var<push_constant> constants: Constants;
@ -44,34 +40,48 @@ var s_diffuse: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let ui = textureSample(t_ui, s_diffuse, in.tex_coords);
if (ui.a >= 0.999) {
return ui;
let ui_coordinate = in.tex_coords * constants.ui_scale;
if (ui_coordinate.x < 0.0 || ui_coordinate.x > 1.0 ||
ui_coordinate.y < 0.0 || ui_coordinate.y > 1.0) {
return srgb_to_linear(constants.background_color);
}
let ui_linear = srgb_to_linear(textureSample(t_ui, s_diffuse, ui_coordinate));
if (ui_linear.a >= 0.999) {
return ui_linear;
}
// UI texture is premultiplied, we need to unpremultiply before blending
let ui_srgb = linear_to_srgb(unpremultiply(ui_linear));
let viewport_coordinate = (in.tex_coords - constants.viewport_offset) * constants.viewport_scale;
// Vello renders its values to an `RgbaUnorm` texture, but if we try to use this in the main rendering pipeline
// which renders to an `Srgb` surface, gamma mapping is applied twice. This converts back to linear to compensate.
let overlay_raw = textureSample(t_overlays, s_diffuse, viewport_coordinate);
let overlay = vec4<f32>(srgb_to_linear(overlay_raw.rgb), overlay_raw.a);
let viewport_raw = textureSample(t_viewport, s_diffuse, viewport_coordinate);
let viewport = vec4<f32>(srgb_to_linear(viewport_raw.rgb), viewport_raw.a);
if (overlay.a < 0.001) {
return blend(ui, viewport);
if (viewport_coordinate.x < 0.0 || viewport_coordinate.x > 1.0 ||
viewport_coordinate.y < 0.0 || viewport_coordinate.y > 1.0) {
return srgb_to_linear(constants.background_color);
}
let composite = blend(overlay, viewport);
return blend(ui, composite);
}
let overlay_srgb = textureSample(t_overlays, s_diffuse, viewport_coordinate);
var viewport_srgb = textureSample(t_viewport, s_diffuse, viewport_coordinate);
fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
return select(
pow((srgb + 0.055) / 1.055, vec3<f32>(2.4)),
srgb / 12.92,
srgb <= vec3<f32>(0.04045)
);
if (viewport_srgb.a < 0.001) {
viewport_srgb = constants.background_color;
}
if (overlay_srgb.a < 0.001) {
if (ui_srgb.a < 0.001) {
return srgb_to_linear(viewport_srgb);
} else {
return srgb_to_linear(blend(ui_srgb, viewport_srgb));
}
}
let composite_linear = blend(srgb_to_linear(overlay_srgb), srgb_to_linear(viewport_srgb));
if (ui_srgb.a < 0.001) {
return composite_linear;
}
return srgb_to_linear(blend(ui_srgb, linear_to_srgb(composite_linear)));
}
fn blend(fg: vec4<f32>, bg: vec4<f32>) -> vec4<f32> {
@ -79,3 +89,25 @@ fn blend(fg: vec4<f32>, bg: vec4<f32>) -> vec4<f32> {
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

@ -1,13 +1,10 @@
use graphene_std::Color;
use std::sync::Arc;
use wgpu_executor::WgpuExecutor;
use winit::window::Window;
use crate::window::Window;
pub(crate) use wgpu_executor::Context as WgpuContext;
use crate::wrapper::{Color, WgpuContext, WgpuExecutor};
#[derive(derivative::Derivative)]
#[derivative(Debug)]
pub(crate) struct GraphicsState {
pub(crate) struct RenderState {
surface: wgpu::Surface<'static>,
context: WgpuContext,
executor: WgpuExecutor,
@ -15,6 +12,8 @@ pub(crate) struct GraphicsState {
render_pipeline: wgpu::RenderPipeline,
transparent_texture: wgpu::Texture,
sampler: wgpu::Sampler,
desired_width: u32,
desired_height: u32,
viewport_scale: [f32; 2],
viewport_offset: [f32; 2],
viewport_texture: Option<wgpu::Texture>,
@ -25,11 +24,10 @@ pub(crate) struct GraphicsState {
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();
impl RenderState {
pub(crate) fn new(window: &Window, context: WgpuContext) -> Self {
let size = window.surface_size();
let surface = window.create_surface(context.instance.clone());
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]);
@ -39,10 +37,13 @@ impl GraphicsState {
format: surface_format,
width: size.width,
height: size.height,
#[cfg(not(target_os = "macos"))]
present_mode: surface_caps.present_modes[0],
#[cfg(target_os = "macos")]
present_mode: wgpu::PresentMode::Immediate,
alpha_mode: surface_caps.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
desired_maximum_frame_latency: 1,
};
surface.configure(&context.device, &config);
@ -175,6 +176,8 @@ impl GraphicsState {
render_pipeline,
transparent_texture,
sampler,
desired_width: size.width,
desired_height: size.height,
viewport_scale: [1.0, 1.0],
viewport_offset: [0.0, 0.0],
viewport_texture: None,
@ -186,6 +189,13 @@ impl GraphicsState {
}
pub(crate) fn resize(&mut self, width: u32, height: u32) {
if width == self.desired_width && height == self.desired_height {
return;
}
self.desired_width = width;
self.desired_height = height;
if width > 0 && height > 0 && (self.config.width != width || self.config.height != height) {
self.config.width = width;
self.config.height = height;
@ -234,26 +244,36 @@ impl GraphicsState {
self.bind_overlays_texture(texture);
}
pub(crate) fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
pub(crate) fn render(&mut self, window: &Window) -> Result<(), RenderError> {
let ui_scale = if let Some(ui_texture) = &self.ui_texture
&& (self.desired_width != ui_texture.width() || self.desired_height != ui_texture.height())
{
Some([self.desired_width as f32 / ui_texture.width() as f32, self.desired_height as f32 / ui_texture.height() as f32])
} else {
None
};
if let Some(scene) = self.overlays_scene.take() {
self.render_overlays(scene);
}
let output = self.surface.get_current_texture()?;
let output = self.surface.get_current_texture().map_err(RenderError::SurfaceError)?;
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"),
label: Some("Graphite Composition Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.01, g: 0.01, b: 0.01, a: 1.0 }),
load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.01, g: 0.01, b: 0.01, a: 1. }),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
@ -267,18 +287,26 @@ impl GraphicsState {
bytemuck::bytes_of(&Constants {
viewport_scale: self.viewport_scale,
viewport_offset: self.viewport_offset,
ui_scale: ui_scale.unwrap_or([1., 1.]),
_pad: [0., 0.],
background_color: [0x22 as f32 / 0xff as f32, 0x22 as f32 / 0xff as f32, 0x22 as f32 / 0xff as f32, 1.], // #222222
}),
);
if let Some(bind_group) = &self.bind_group {
render_pass.set_bind_group(0, bind_group, &[]);
render_pass.draw(0..6, 0..1); // Draw 3 vertices for fullscreen triangle
render_pass.draw(0..3, 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();
if ui_scale.is_some() {
return Err(RenderError::OutdatedUITextureError);
}
Ok(())
}
@ -314,9 +342,17 @@ impl GraphicsState {
}
}
pub(crate) enum RenderError {
OutdatedUITextureError,
SurfaceError(wgpu::SurfaceError),
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Constants {
viewport_scale: [f32; 2],
viewport_offset: [f32; 2],
ui_scale: [f32; 2],
_pad: [f32; 2],
background_color: [f32; 4],
}

190
desktop/src/window.rs Normal file
View file

@ -0,0 +1,190 @@
use std::collections::HashMap;
use std::sync::Arc;
use winit::cursor::{CursorIcon, CustomCursor, CustomCursorSource};
use winit::event_loop::ActiveEventLoop;
use winit::window::{Window as WinitWindow, WindowAttributes};
use crate::consts::APP_NAME;
use crate::event::AppEventScheduler;
use crate::wrapper::messages::MenuItem;
pub(crate) trait NativeWindow {
fn init() {}
fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes;
fn new(window: &dyn WinitWindow, app_event_scheduler: AppEventScheduler) -> Self;
fn can_render(&self) -> bool {
true
}
fn update_menu(&self, _entries: Vec<MenuItem>) {}
fn hide(&self) {}
fn hide_others(&self) {}
fn show_all(&self) {}
}
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
use linux as native;
#[cfg(target_os = "macos")]
mod mac;
#[cfg(target_os = "macos")]
use mac as native;
#[cfg(target_os = "windows")]
mod win;
#[cfg(target_os = "windows")]
use win as native;
pub(crate) struct Window {
winit_window: Arc<dyn winit::window::Window>,
#[allow(dead_code)]
native_handle: native::NativeWindowImpl,
custom_cursors: HashMap<CustomCursorSource, CustomCursor>,
clipboard: window_clipboard::Clipboard,
}
impl Window {
pub(crate) fn init() {
native::NativeWindowImpl::init();
}
pub(crate) fn new(event_loop: &dyn ActiveEventLoop, app_event_scheduler: AppEventScheduler) -> Self {
let mut attributes = WindowAttributes::default()
.with_title(APP_NAME)
.with_min_surface_size(winit::dpi::LogicalSize::new(400, 300))
.with_surface_size(winit::dpi::LogicalSize::new(1200, 800))
.with_resizable(true)
.with_visible(false)
.with_theme(Some(winit::window::Theme::Dark));
attributes = native::NativeWindowImpl::configure(attributes, event_loop);
let winit_window = event_loop.create_window(attributes).unwrap();
let native_handle = native::NativeWindowImpl::new(winit_window.as_ref(), app_event_scheduler);
let clipboard = unsafe { window_clipboard::Clipboard::connect(&winit_window) }.expect("failed to create clipboard");
Self {
winit_window: winit_window.into(),
native_handle,
custom_cursors: HashMap::new(),
clipboard,
}
}
pub(crate) fn show(&self) {
self.winit_window.set_visible(true);
self.winit_window.focus_window();
}
pub(crate) fn request_redraw(&self) {
self.winit_window.request_redraw();
}
pub(crate) fn create_surface(&self, instance: Arc<wgpu::Instance>) -> wgpu::Surface<'static> {
instance.create_surface(self.winit_window.clone()).unwrap()
}
pub(crate) fn pre_present_notify(&self) {
self.winit_window.pre_present_notify();
}
pub(crate) fn can_render(&self) -> bool {
self.native_handle.can_render()
}
pub(crate) fn surface_size(&self) -> winit::dpi::PhysicalSize<u32> {
self.winit_window.surface_size()
}
pub(crate) fn scale_factor(&self) -> f64 {
self.winit_window.scale_factor()
}
pub(crate) fn minimize(&self) {
self.winit_window.set_minimized(true);
}
pub(crate) fn toggle_maximize(&self) {
self.winit_window.set_maximized(!self.winit_window.is_maximized());
}
pub(crate) fn is_maximized(&self) -> bool {
self.winit_window.is_maximized()
}
pub(crate) fn is_fullscreen(&self) -> bool {
self.winit_window.fullscreen().is_some()
}
pub(crate) fn start_drag(&self) {
let _ = self.winit_window.drag_window();
}
pub(crate) fn hide(&self) {
self.native_handle.hide();
}
pub(crate) fn hide_others(&self) {
self.native_handle.hide_others();
}
pub(crate) fn show_all(&self) {
self.native_handle.show_all();
}
pub(crate) fn set_cursor(&mut self, event_loop: &dyn ActiveEventLoop, cursor: Cursor) {
let cursor = match cursor {
Cursor::Icon(cursor_icon) => cursor_icon.into(),
Cursor::Custom(custom_cursor_source) => {
let custom_cursor = match self.custom_cursors.get(&custom_cursor_source).cloned() {
Some(cursor) => cursor,
None => {
let Ok(custom_cursor) = event_loop.create_custom_cursor(custom_cursor_source.clone()) else {
tracing::error!("Failed to create custom cursor");
return;
};
self.custom_cursors.insert(custom_cursor_source, custom_cursor.clone());
custom_cursor
}
};
custom_cursor.into()
}
};
self.winit_window.set_cursor(cursor);
}
pub(crate) fn update_menu(&self, entries: Vec<MenuItem>) {
self.native_handle.update_menu(entries);
}
pub(crate) fn clipboard_read(&self) -> Option<String> {
match self.clipboard.read() {
Ok(data) => Some(data),
Err(e) => {
tracing::error!("Failed to read from clipboard: {e}");
None
}
}
}
pub(crate) fn clipboard_write(&mut self, data: String) {
if let Err(e) = self.clipboard.write(data) {
tracing::error!("Failed to write to clipboard: {e}")
}
}
}
pub(crate) enum Cursor {
Icon(CursorIcon),
Custom(CustomCursorSource),
}
impl From<CursorIcon> for Cursor {
fn from(icon: CursorIcon) -> Self {
Cursor::Icon(icon)
}
}
impl From<CustomCursorSource> for Cursor {
fn from(custom: CustomCursorSource) -> Self {
Cursor::Custom(custom)
}
}

View file

@ -0,0 +1,26 @@
use winit::event_loop::ActiveEventLoop;
use winit::platform::wayland::ActiveEventLoopExtWayland;
use winit::platform::wayland::WindowAttributesWayland;
use winit::platform::x11::WindowAttributesX11;
use winit::window::{Window, WindowAttributes};
use crate::consts::{APP_ID, APP_NAME};
use crate::event::AppEventScheduler;
pub(super) struct NativeWindowImpl {}
impl super::NativeWindow for NativeWindowImpl {
fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes {
if event_loop.is_wayland() {
let wayland_attributes = WindowAttributesWayland::default().with_name(APP_ID, "").with_prefer_csd(true);
attributes.with_platform_attributes(Box::new(wayland_attributes))
} else {
let x11_attributes = WindowAttributesX11::default().with_name(APP_ID, APP_NAME);
attributes.with_platform_attributes(Box::new(x11_attributes))
}
}
fn new(_window: &dyn Window, _app_event_scheduler: AppEventScheduler) -> Self {
NativeWindowImpl {}
}
}

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