mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
Merge branch 'master' into merge_segments
This commit is contained in:
commit
f732d3b123
969 changed files with 47431 additions and 44197 deletions
2
.branding
Normal file
2
.branding
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
https://github.com/Keavon/graphite-branded-assets/archive/f8b02e68c92f5bbd27626bdd7a51102303b70a40.tar.gz
|
||||
d06fd7b79fa9b7509c23072fa56745415fdc6eb98575d15214b0acc47ea4dd42
|
||||
|
|
@ -10,3 +10,6 @@ rustflags = [
|
|||
"link-arg=--max-memory=4294967296",
|
||||
"--cfg=web_sys_unstable_apis",
|
||||
]
|
||||
|
||||
[env]
|
||||
CARGO_WORKSPACE_DIR = { value = "", relative = true }
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@
|
|||
"streetsidesoftware.code-spell-checker",
|
||||
// Helpful
|
||||
"mhutchie.git-graph",
|
||||
"waderyan.gitblame",
|
||||
"qezhu.gitlink",
|
||||
"wmaurer.change-case"
|
||||
]
|
||||
|
|
|
|||
13
.github/workflows/build-dev-and-ci.yml
vendored
13
.github/workflows/build-dev-and-ci.yml
vendored
|
|
@ -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
17
.github/workflows/build-nix-package.yml
vendored
Normal 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
|
||||
6
.github/workflows/build-production.yml
vendored
6
.github/workflows/build-production.yml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
|
|
@ -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: |
|
||||
|
|
|
|||
190
.github/workflows/comment-profiling-changes.yaml
vendored
190
.github/workflows/comment-profiling-changes.yaml
vendored
|
|
@ -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)}%)`);
|
||||
|
|
|
|||
9
.github/workflows/library-rawkit.yml
vendored
9
.github/workflows/library-rawkit.yml
vendored
|
|
@ -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"
|
||||
|
|
|
|||
53
.github/workflows/website.yml
vendored
53
.github/workflows/website.yml
vendored
|
|
@ -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
4
.gitignore
vendored
|
|
@ -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
26
.nix/deps/cef.nix
Normal 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
5
.nix/deps/crane.nix
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{ pkgs, inputs, ... }:
|
||||
|
||||
{
|
||||
lib = inputs.crane.mkLib pkgs;
|
||||
}
|
||||
58
.nix/deps/rust-gpu.nix
Normal file
58
.nix/deps/rust-gpu.nix
Normal 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
22
.nix/dev.nix
Normal 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
28
.nix/flake.lock
generated
|
|
@ -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": {
|
||||
|
|
|
|||
193
.nix/flake.nix
193
.nix/flake.nix
|
|
@ -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
136
.nix/pkgs/graphite.nix
Normal 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}"
|
||||
'';
|
||||
}
|
||||
)
|
||||
36
.nix/pkgs/raster-nodes-shaders.nix
Normal file
36
.nix/pkgs/raster-nodes-shaders.nix
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
|
|
@ -13,7 +13,6 @@
|
|||
"streetsidesoftware.code-spell-checker",
|
||||
// Helpful
|
||||
"mhutchie.git-graph",
|
||||
"waderyan.gitblame",
|
||||
"qezhu.gitlink",
|
||||
"wmaurer.change-case"
|
||||
]
|
||||
|
|
|
|||
17
.vscode/settings.json
vendored
17
.vscode/settings.json
vendored
|
|
@ -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
3160
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
226
Cargo.toml
226
Cargo.toml
|
|
@ -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" }
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -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).*
|
||||
|
|
|
|||
36
about.toml
36
about.toml
|
|
@ -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
|
||||
|
|
|
|||
2
demo-artwork/changing-seasons.graphite
generated
2
demo-artwork/changing-seasons.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/isometric-fountain.graphite
generated
2
demo-artwork/isometric-fountain.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/marbled-mandelbrot.graphite
generated
2
demo-artwork/marbled-mandelbrot.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/painted-dreams.graphite
generated
2
demo-artwork/painted-dreams.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/parametric-dunescape.graphite
generated
2
demo-artwork/parametric-dunescape.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/procedural-string-lights.graphite
generated
2
demo-artwork/procedural-string-lights.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/red-dress.graphite
generated
2
demo-artwork/red-dress.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/valley-of-spires.graphite
generated
2
demo-artwork/valley-of-spires.graphite
generated
File diff suppressed because one or more lines are too long
40
deny.toml
40
deny.toml
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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
16
desktop/bundle/Cargo.toml
Normal 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
10
desktop/bundle/build.rs
Normal 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}");
|
||||
}
|
||||
71
desktop/bundle/src/common.rs
Normal file
71
desktop/bundle/src/common.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
21
desktop/bundle/src/linux.rs
Normal file
21
desktop/bundle/src/linux.rs
Normal 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
127
desktop/bundle/src/mac.rs
Normal 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,
|
||||
}
|
||||
17
desktop/bundle/src/main.rs
Normal file
17
desktop/bundle/src/main.rs
Normal 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
34
desktop/bundle/src/win.rs
Normal 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
|
||||
}
|
||||
15
desktop/embedded-resources/Cargo.toml
Normal file
15
desktop/embedded-resources/Cargo.toml
Normal 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)'] }
|
||||
32
desktop/embedded-resources/build.rs
Normal file
32
desktop/embedded-resources/build.rs
Normal 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?");
|
||||
}
|
||||
}
|
||||
10
desktop/embedded-resources/src/lib.rs
Normal file
10
desktop/embedded-resources/src/lib.rs
Normal 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;
|
||||
16
desktop/platform/linux/Cargo.toml
Normal file
16
desktop/platform/linux/Cargo.toml
Normal 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 = "../.." }
|
||||
3
desktop/platform/linux/src/main.rs
Normal file
3
desktop/platform/linux/src/main.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
graphite_desktop::start();
|
||||
}
|
||||
20
desktop/platform/mac/Cargo.toml
Normal file
20
desktop/platform/mac/Cargo.toml
Normal 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 = "../.." }
|
||||
3
desktop/platform/mac/src/helper.rs
Normal file
3
desktop/platform/mac/src/helper.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
graphite_desktop::start_helper();
|
||||
}
|
||||
3
desktop/platform/mac/src/main.rs
Normal file
3
desktop/platform/mac/src/main.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
graphite_desktop::start();
|
||||
}
|
||||
19
desktop/platform/win/Cargo.toml
Normal file
19
desktop/platform/win/Cargo.toml
Normal 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"
|
||||
31
desktop/platform/win/build.rs
Normal file
31
desktop/platform/win/build.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
4
desktop/platform/win/src/main.rs
Normal file
4
desktop/platform/win/src/main.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#![windows_subsystem = "windows"]
|
||||
fn main() {
|
||||
graphite_desktop::start();
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
22
desktop/src/cef/consts.rs
Normal 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;
|
||||
|
|
@ -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>);
|
||||
}
|
||||
|
|
|
|||
230
desktop/src/cef/context/builder.rs
Normal file
230
desktop/src/cef/context/builder.rs
Normal 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,
|
||||
}
|
||||
67
desktop/src/cef/context/multithreaded.rs
Normal file
67
desktop/src/cef/context/multithreaded.rs
Normal 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));
|
||||
}
|
||||
71
desktop/src/cef/context/singlethreaded.rs
Normal file
71
desktop/src/cef/context/singlethreaded.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, ' '),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
256
desktop/src/cef/input/state.rs
Normal file
256
desktop/src/cef/input/state.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
66
desktop/src/cef/internal/context_menu_handler.rs
Normal file
66
desktop/src/cef/internal/context_menu_handler.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_context_menu_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{ImplContextMenuHandler, WrapContextMenuHandler};
|
||||
|
||||
pub(crate) struct ContextMenuHandlerImpl {
|
||||
object: *mut RcImpl<_cef_context_menu_handler_t, Self>,
|
||||
}
|
||||
impl ContextMenuHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplContextMenuHandler for ContextMenuHandlerImpl {
|
||||
fn run_context_menu(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
_frame: Option<&mut cef::Frame>,
|
||||
_params: Option<&mut cef::ContextMenuParams>,
|
||||
_model: Option<&mut cef::MenuModel>,
|
||||
_callback: Option<&mut cef::RunContextMenuCallback>,
|
||||
) -> std::ffi::c_int {
|
||||
// Prevent context menu
|
||||
1
|
||||
}
|
||||
|
||||
fn run_quick_menu(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
_frame: Option<&mut cef::Frame>,
|
||||
_location: Option<&cef::Point>,
|
||||
_size: Option<&cef::Size>,
|
||||
_edit_state_flags: cef::QuickMenuEditStateFlags,
|
||||
_callback: Option<&mut cef::RunQuickMenuCallback>,
|
||||
) -> std::ffi::c_int {
|
||||
// Prevent quick menu
|
||||
1
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_context_menu_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ContextMenuHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for ContextMenuHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapContextMenuHandler for ContextMenuHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_context_menu_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
147
desktop/src/cef/internal/display_handler.rs
Normal file
147
desktop/src/cef/internal/display_handler.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
60
desktop/src/cef/internal/load_handler.rs
Normal file
60
desktop/src/cef/internal/load_handler.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_load_handler_t, cef_base_ref_counted_t, cef_load_handler_t};
|
||||
use cef::{ImplBrowser, ImplBrowserHost, ImplLoadHandler, WrapLoadHandler};
|
||||
|
||||
use crate::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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
108
desktop/src/cef/internal/resource_handler.rs
Normal file
108
desktop/src/cef/internal/resource_handler.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
74
desktop/src/cef/internal/scheme_handler_factory.rs
Normal file
74
desktop/src/cef/internal/scheme_handler_factory.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_scheme_handler_factory_t, cef_base_ref_counted_t, cef_scheme_options_t};
|
||||
use cef::{Browser, CefString, Frame, ImplRequest, ImplSchemeHandlerFactory, ImplSchemeRegistrar, Request, ResourceHandler, SchemeRegistrar, WrapSchemeHandlerFactory};
|
||||
|
||||
use super::resource_handler::ResourceHandlerImpl;
|
||||
use crate::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;
|
||||
}
|
||||
}
|
||||
61
desktop/src/cef/internal/task.rs
Normal file
61
desktop/src/cef/internal/task.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_task_t, cef_base_ref_counted_t};
|
||||
use cef::{ImplTask, WrapTask};
|
||||
use std::cell::RefCell;
|
||||
|
||||
// Closure-based task wrapper following CEF patterns
|
||||
pub struct ClosureTask<F> {
|
||||
pub(crate) object: *mut RcImpl<_cef_task_t, Self>,
|
||||
pub(crate) closure: RefCell<Option<F>>,
|
||||
}
|
||||
|
||||
impl<F: FnOnce() + Send + 'static> ClosureTask<F> {
|
||||
pub fn new(closure: F) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
closure: RefCell::new(Some(closure)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnOnce() + Send + 'static> ImplTask for ClosureTask<F> {
|
||||
fn execute(&self) {
|
||||
if let Some(closure) = self.closure.borrow_mut().take() {
|
||||
closure();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_task_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnOnce() + Send + 'static> Clone for ClosureTask<F> {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
if !self.object.is_null() {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
closure: RefCell::new(None), // Closure can only be executed once
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnOnce() + Send + 'static> Rc for ClosureTask<F> {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnOnce() + Send + 'static> WrapTask for ClosureTask<F> {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_task_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
59
desktop/src/cef/platform.rs
Normal file
59
desktop/src/cef/platform.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
9
desktop/src/cli.rs
Normal 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,
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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
39
desktop/src/event.rs
Normal 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 }
|
||||
}
|
||||
}
|
||||
23
desktop/src/gpu_context.rs
Normal file
23
desktop/src/gpu_context.rs
Normal 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
102
desktop/src/lib.rs
Normal 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();
|
||||
}
|
||||
|
|
@ -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
214
desktop/src/persist.rs
Normal 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(¤t_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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
190
desktop/src/window.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
26
desktop/src/window/linux.rs
Normal file
26
desktop/src/window/linux.rs
Normal 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
Loading…
Add table
Add a link
Reference in a new issue