mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 10:58:12 +08:00
Merge branch 'master' into merge_segments
This commit is contained in:
@@ -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",
|
"link-arg=--max-memory=4294967296",
|
||||||
"--cfg=web_sys_unstable_apis",
|
"--cfg=web_sys_unstable_apis",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[env]
|
||||||
|
CARGO_WORKSPACE_DIR = { value = "", relative = true }
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
"streetsidesoftware.code-spell-checker",
|
"streetsidesoftware.code-spell-checker",
|
||||||
// Helpful
|
// Helpful
|
||||||
"mhutchie.git-graph",
|
"mhutchie.git-graph",
|
||||||
"waderyan.gitblame",
|
|
||||||
"qezhu.gitlink",
|
"qezhu.gitlink",
|
||||||
"wmaurer.change-case"
|
"wmaurer.change-case"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
pull_request: {}
|
pull_request: {}
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
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:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -34,10 +34,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: "latest"
|
node-version: "latest"
|
||||||
|
|
||||||
- name: 🚧 Install Node dependencies
|
- name: 🚧 Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
cd frontend
|
cd frontend
|
||||||
npm ci
|
npm run setup
|
||||||
|
|
||||||
- name: 🦀 Install the latest Rust
|
- name: 🦀 Install the latest Rust
|
||||||
run: |
|
run: |
|
||||||
@@ -47,6 +47,11 @@ jobs:
|
|||||||
echo "Latest updated version:"
|
echo "Latest updated version:"
|
||||||
rustc --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
|
- name: ✂ Replace template in <head> of index.html
|
||||||
run: |
|
run: |
|
||||||
# Remove the INDEX_HTML_HEAD_REPLACEMENT environment variable for build links (not master deploys)
|
# Remove the INDEX_HTML_HEAD_REPLACEMENT environment variable for build links (not master deploys)
|
||||||
@@ -103,7 +108,7 @@ jobs:
|
|||||||
|
|
||||||
- name: 🧪 Run Rust tests
|
- name: 🧪 Run Rust tests
|
||||||
run: |
|
run: |
|
||||||
mold -run cargo test --all-features --workspace
|
mold -run cargo test --all-features
|
||||||
|
|
||||||
- name: 📃 Generate code documentation info for website
|
- name: 📃 Generate code documentation info for website
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
RUSTC_WRAPPER: /usr/bin/sccache
|
RUSTC_WRAPPER: /usr/bin/sccache
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
SCCACHE_DIR: /var/lib/github-actions/.cache
|
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:
|
steps:
|
||||||
- name: 📥 Clone and checkout repository
|
- name: 📥 Clone and checkout repository
|
||||||
@@ -32,10 +32,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: "latest"
|
node-version: "latest"
|
||||||
|
|
||||||
- name: 🚧 Install Node dependencies
|
- name: 🚧 Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
cd frontend
|
cd frontend
|
||||||
npm ci
|
npm run setup
|
||||||
|
|
||||||
- name: 🦀 Install the latest Rust
|
- name: 🦀 Install the latest Rust
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: "latest"
|
node-version: "latest"
|
||||||
|
|
||||||
- name: 🚧 Install Node dependencies
|
- name: 🚧 Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
cd frontend
|
cd frontend
|
||||||
npm ci
|
npm run setup
|
||||||
|
|
||||||
- name: 🦀 Install the latest Rust
|
- name: 🦀 Install the latest Rust
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ name: Profiling Changes
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'node-graph/**'
|
||||||
|
- 'Cargo.toml'
|
||||||
|
- 'Cargo.lock'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
@@ -9,6 +13,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
profile:
|
profile:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -33,12 +38,12 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/bin/iai-callgrind-runner
|
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
|
- name: Install iai-callgrind
|
||||||
if: steps.cache-iai.outputs.cache-hit != 'true'
|
if: steps.cache-iai.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
cargo install iai-callgrind-runner@0.12.3
|
cargo install iai-callgrind-runner@0.16.1
|
||||||
|
|
||||||
- name: Checkout master branch
|
- name: Checkout master branch
|
||||||
run: |
|
run: |
|
||||||
@@ -49,21 +54,30 @@ jobs:
|
|||||||
id: master-sha
|
id: master-sha
|
||||||
run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
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
|
- name: Cache benchmark baselines
|
||||||
id: cache-benchmark-baselines
|
id: cache-benchmark-baselines
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: target/iai
|
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: |
|
restore-keys: |
|
||||||
${{ runner.os }}-benchmark-baselines-master-
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.cpu-info.outputs.cpu-hash }}-benchmark-baselines-master-
|
||||||
|
|
||||||
- name: Run baseline benchmarks
|
- name: Run baseline benchmarks
|
||||||
if: steps.cache-benchmark-baselines.outputs.cache-hit != 'true'
|
if: steps.cache-benchmark-baselines.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
# Compile benchmarks
|
# Compile benchmarks
|
||||||
cargo bench --bench compile_demo_art_iai -- --save-baseline=master
|
cargo bench --bench compile_demo_art_iai -- --save-baseline=master
|
||||||
|
|
||||||
# Runtime benchmarks
|
# Runtime benchmarks
|
||||||
cargo bench --bench update_executor_iai -- --save-baseline=master
|
cargo bench --bench update_executor_iai -- --save-baseline=master
|
||||||
cargo bench --bench run_once_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 }}
|
git checkout ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Run PR benchmarks
|
- name: Run PR benchmarks
|
||||||
id: benchmark
|
|
||||||
run: |
|
run: |
|
||||||
# Compile benchmarks
|
# Compile benchmarks
|
||||||
COMPILE_OUTPUT=$(cargo bench --bench compile_demo_art_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g')
|
cargo bench --bench compile_demo_art_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/compile_output.json
|
||||||
|
|
||||||
# Runtime benchmarks
|
# Runtime benchmarks
|
||||||
UPDATE_OUTPUT=$(cargo bench --bench update_executor_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g')
|
cargo bench --bench update_executor_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/update_output.json
|
||||||
RUN_ONCE_OUTPUT=$(cargo bench --bench run_once_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g')
|
cargo bench --bench run_once_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/run_once_output.json
|
||||||
RUN_CACHED_OUTPUT=$(cargo bench --bench run_cached_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g')
|
cargo bench --bench run_cached_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/run_cached_output.json
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
- name: Make old comments collapsed by default
|
- 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
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
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
|
- name: Comment PR
|
||||||
|
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
script: |
|
script: |
|
||||||
const compileOutput = JSON.parse(`${{ steps.benchmark.outputs.COMPILE_OUTPUT }}`);
|
const fs = require('fs');
|
||||||
const updateOutput = JSON.parse(`${{ steps.benchmark.outputs.UPDATE_OUTPUT }}`);
|
|
||||||
const runOnceOutput = JSON.parse(`${{ steps.benchmark.outputs.RUN_ONCE_OUTPUT }}`);
|
const compileOutput = JSON.parse(fs.readFileSync('/tmp/compile_output.json', 'utf8'));
|
||||||
const runCachedOutput = JSON.parse(`${{ steps.benchmark.outputs.RUN_CACHED_OUTPUT }}`);
|
const updateOutput = JSON.parse(fs.readFileSync('/tmp/update_output.json', 'utf8'));
|
||||||
|
const runOnceOutput = JSON.parse(fs.readFileSync('/tmp/run_once_output.json', 'utf8'));
|
||||||
let significantChanges = false;
|
const runCachedOutput = JSON.parse(fs.readFileSync('/tmp/run_cached_output.json', 'utf8'));
|
||||||
|
|
||||||
|
const hasSignificantChanges = '${{ steps.analyze.outputs.has-significant-changes }}' === 'true';
|
||||||
let commentBody = "";
|
let commentBody = "";
|
||||||
|
|
||||||
function formatNumber(num) {
|
function formatNumber(num) {
|
||||||
@@ -160,13 +220,31 @@ jobs:
|
|||||||
let sectionBody = "";
|
let sectionBody = "";
|
||||||
let hasResults = false;
|
let hasResults = false;
|
||||||
let hasSignificantChanges = 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) {
|
for (const benchmark of benchmarkOutput) {
|
||||||
if (benchmark.callgrind_summary && benchmark.callgrind_summary.summaries) {
|
if (benchmark.profiles && benchmark.profiles.length > 0) {
|
||||||
const summary = benchmark.callgrind_summary.summaries[0];
|
const profile = benchmark.profiles[0];
|
||||||
const irDiff = summary.events.Ir;
|
if (profile.summaries && profile.summaries.parts && profile.summaries.parts.length > 0) {
|
||||||
|
const part = profile.summaries.parts[0];
|
||||||
if (irDiff.diff_pct !== null) {
|
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;
|
hasResults = true;
|
||||||
const changePercentage = formatPercentage(irDiff.diff_pct);
|
const changePercentage = formatPercentage(irDiff.diff_pct);
|
||||||
const color = irDiff.diff_pct > 0 ? "red" : "lime";
|
const color = irDiff.diff_pct > 0 ? "red" : "lime";
|
||||||
@@ -178,19 +256,23 @@ jobs:
|
|||||||
sectionBody += "<details>\n<summary>Detailed metrics</summary>\n\n```\n";
|
sectionBody += "<details>\n<summary>Detailed metrics</summary>\n\n```\n";
|
||||||
sectionBody += `Baselines: master| HEAD\n`;
|
sectionBody += `Baselines: master| HEAD\n`;
|
||||||
|
|
||||||
for (const [eventKind, costsDiff] of Object.entries(summary.events)) {
|
for (const [metricName, metricData] of Object.entries(part.metrics_summary.Callgrind)) {
|
||||||
if (costsDiff.diff_pct !== null) {
|
if (metricData.diffs && metricData.diffs.diff_pct !== null) {
|
||||||
const changePercentage = formatPercentage(costsDiff.diff_pct);
|
const changePercentage = formatPercentage(parseFloat(metricData.diffs.diff_pct));
|
||||||
const line = `${padRight(eventKind, 20)} ${padLeft(formatNumber(costsDiff.old), 11)}|${padLeft(formatNumber(costsDiff.new), 11)} ${padLeft(changePercentage, 15)}`;
|
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 += `${line}\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sectionBody += "```\n</details>\n\n";
|
sectionBody += "```\n</details>\n\n";
|
||||||
|
|
||||||
if (Math.abs(irDiff.diff_pct) > 5) {
|
if (isSignificantChange(irDiff.diff_pct, Math.abs(irDiff.new - irDiff.old), sectionTitle)) {
|
||||||
significantChanges = true;
|
significantChanges = true;
|
||||||
hasSignificantChanges = true;
|
hasSignificantChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,7 +318,7 @@ jobs:
|
|||||||
if (commentBody.length > 0) {
|
if (commentBody.length > 0) {
|
||||||
const output = `<details open>\n<summary>Performance Benchmark Results</summary>\n\n${commentBody}\n</details>`;
|
const output = `<details open>\n<summary>Performance Benchmark Results</summary>\n\n${commentBody}\n</details>`;
|
||||||
|
|
||||||
if (significantChanges) {
|
if (hasSignificantChanges) {
|
||||||
github.rest.issues.createComment({
|
github.rest.issues.createComment({
|
||||||
issue_number: context.issue.number,
|
issue_number: context.issue.number,
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
@@ -250,3 +332,13 @@ jobs:
|
|||||||
} else {
|
} else {
|
||||||
console.log("No benchmark results to display.");
|
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)}%)`);
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ jobs:
|
|||||||
|
|
||||||
- name: 📦 Run sccache-cache
|
- name: 📦 Run sccache-cache
|
||||||
uses: mozilla-actions/sccache-action@v0.0.6
|
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
|
- name: 🔬 Check Rust formatting
|
||||||
run: |
|
run: |
|
||||||
@@ -56,4 +63,4 @@ jobs:
|
|||||||
|
|
||||||
- name: 📈 Run sccache stat for check
|
- name: 📈 Run sccache stat for check
|
||||||
shell: bash
|
shell: bash
|
||||||
run: sccache --show-stats
|
run: sccache --show-stats || echo "sccache stats unavailable"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ on:
|
|||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
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:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -26,6 +26,14 @@ jobs:
|
|||||||
- name: 📥 Clone and checkout repository
|
- name: 📥 Clone and checkout repository
|
||||||
uses: actions/checkout@v3
|
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
|
- name: 🕸 Install Zola
|
||||||
uses: taiki-e/install-action@v2
|
uses: taiki-e/install-action@v2
|
||||||
with:
|
with:
|
||||||
@@ -63,55 +71,24 @@ jobs:
|
|||||||
mkdir artifacts
|
mkdir artifacts
|
||||||
mv hierarchical_message_system_tree.txt artifacts/hierarchical_message_system_tree.txt
|
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: |
|
run: |
|
||||||
mv artifacts/* website/other/editor-structure
|
mv artifacts/* .
|
||||||
|
|
||||||
- name: 🔧 Build auto-generated code docs artifacts into HTML
|
- name: 🔧 Build auto-generated code docs artifacts into HTML
|
||||||
run: |
|
run: |
|
||||||
cd website/other/editor-structure
|
cd website
|
||||||
node generate.js hierarchical_message_system_tree.txt replacement.html
|
npm run generate-editor-structure
|
||||||
|
|
||||||
- name: 🌐 Build Graphite website with Zola
|
- name: 🌐 Build Graphite website with Zola
|
||||||
env:
|
env:
|
||||||
MODE: prod
|
MODE: prod
|
||||||
run: |
|
run: |
|
||||||
cd website
|
cd website
|
||||||
npm run install-fonts
|
npm ci
|
||||||
|
npm run lint
|
||||||
zola --config config.toml build --minify
|
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
|
- name: 📤 Publish to Cloudflare Pages
|
||||||
id: cloudflare
|
id: cloudflare
|
||||||
uses: cloudflare/pages-action@1
|
uses: cloudflare/pages-action@1
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
branding/
|
||||||
target/
|
target/
|
||||||
|
result/
|
||||||
*.spv
|
*.spv
|
||||||
*.exrc
|
*.exrc
|
||||||
perf.data*
|
perf.data*
|
||||||
@@ -7,4 +9,6 @@ profile.json.gz
|
|||||||
flamegraph.svg
|
flamegraph.svg
|
||||||
.idea/
|
.idea/
|
||||||
.direnv
|
.direnv
|
||||||
|
.DS_Store
|
||||||
hierarchical_message_system_tree.txt
|
hierarchical_message_system_tree.txt
|
||||||
|
hierarchical_message_system_tree.html
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{ pkgs, inputs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
lib = inputs.crane.mkLib pkgs;
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
Generated
+22
-6
@@ -1,5 +1,20 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"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": {
|
"flake-compat": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1733328505,
|
"lastModified": 1733328505,
|
||||||
@@ -34,11 +49,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1754214453,
|
"lastModified": 1764242076,
|
||||||
"narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=",
|
"narHash": "sha256-sKoIWfnijJ0+9e4wRvIgm/HgE27bzwQxcEmo2J/gNpI=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376",
|
"rev": "2fad6eac6077f03fe109c4d4eb171cf96791faa4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -50,6 +65,7 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
"flake-compat": "flake-compat",
|
"flake-compat": "flake-compat",
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
@@ -63,11 +79,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1753238793,
|
"lastModified": 1764297505,
|
||||||
"narHash": "sha256-jmQeEpgX+++MEgrcikcwoSiI7vDZWLP0gci7XiWb9uQ=",
|
"narHash": "sha256-qrLpVu2/hA9Cu6IovMEsgh9YRyvmmWS+bSx7C1JGChA=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "0ad7ab4ca8e83febf147197e65c006dff60623ab",
|
"rev": "9623580f8ce09ec444b9aca107566ec5db110e62",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
+115
-78
@@ -12,8 +12,6 @@
|
|||||||
# - Development shell: `nix develop .nix` from the project root
|
# - Development shell: `nix develop .nix` from the project root
|
||||||
# - Run in dev shell with direnv: add `use flake` to .envrc
|
# - Run in dev shell with direnv: add `use flake` to .envrc
|
||||||
{
|
{
|
||||||
description = "Development environment and build configuration";
|
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
rust-overlay = {
|
rust-overlay = {
|
||||||
@@ -21,102 +19,141 @@
|
|||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
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
|
# 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";
|
flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { nixpkgs, rust-overlay, flake-utils, ... }:
|
outputs =
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
inputs:
|
||||||
|
inputs.flake-utils.lib.eachDefaultSystem (
|
||||||
|
system:
|
||||||
let
|
let
|
||||||
overlays = [ (import rust-overlay) ];
|
info = {
|
||||||
pkgs = import nixpkgs {
|
pname = "graphite";
|
||||||
inherit system overlays;
|
version = "unstable";
|
||||||
};
|
src = ./..;
|
||||||
|
|
||||||
rustc-wasm = pkgs.rust-bin.stable.latest.default.override {
|
|
||||||
targets = [ "wasm32-unknown-unknown" ];
|
|
||||||
extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
libcef = pkgs.libcef.overrideAttrs (finalAttrs: previousAttrs: {
|
pkgs = import inputs.nixpkgs {
|
||||||
version = "138.0.26";
|
inherit system;
|
||||||
gitRevision = "84f2d27";
|
overlays = [ (import inputs.rust-overlay) ];
|
||||||
chromiumVersion = "138.0.7204.158";
|
};
|
||||||
srcHash = "sha256-d9jQJX7rgdoHfROD3zmOdMSesRdKE3slB5ZV+U2wlbQ=";
|
|
||||||
|
|
||||||
__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 = ''
|
libs = rec {
|
||||||
strip $out/lib/*
|
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" {} ''
|
tools = rec {
|
||||||
mkdir -p $out
|
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
|
pkgs.git
|
||||||
find ${libcef}/lib -type f -name "*" -exec ln -s {} $out/ \;
|
|
||||||
find ${libcef}/libexec -type f -name "*" -exec ln -s {} $out/ \;
|
|
||||||
cp -r ${libcef}/share/cef/* $out/
|
|
||||||
|
|
||||||
echo '${builtins.toJSON {
|
pkgs.cargo-watch
|
||||||
type = "minimal";
|
pkgs.cargo-nextest
|
||||||
name = builtins.baseNameOf libcef.src.url;
|
pkgs.cargo-expand
|
||||||
sha1 = "";
|
|
||||||
}}' > $out/archive.json
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Shared build inputs - system libraries that need to be in LD_LIBRARY_PATH
|
# Linker
|
||||||
buildInputs = with pkgs; [
|
pkgs.mold
|
||||||
# System libraries
|
|
||||||
wayland
|
|
||||||
openssl
|
|
||||||
vulkan-loader
|
|
||||||
libraw
|
|
||||||
libGL
|
|
||||||
];
|
|
||||||
|
|
||||||
# Development tools that don't need to be in LD_LIBRARY_PATH
|
# Profiling tools
|
||||||
buildTools = [
|
pkgs.gnuplot
|
||||||
rustc-wasm
|
pkgs.samply
|
||||||
pkgs.nodejs
|
pkgs.cargo-flamegraph
|
||||||
pkgs.nodePackages.npm
|
|
||||||
pkgs.binaryen
|
|
||||||
pkgs.wasm-bindgen-cli
|
|
||||||
pkgs.wasm-pack
|
|
||||||
pkgs.pkg-config
|
|
||||||
pkgs.git
|
|
||||||
pkgs.cargo-about
|
|
||||||
|
|
||||||
# Linker
|
# Plotting tools
|
||||||
pkgs.mold
|
pkgs.graphviz
|
||||||
];
|
];
|
||||||
# Development tools that don't need to be in LD_LIBRARY_PATH
|
all = desktop ++ frontend ++ dev;
|
||||||
devTools = with pkgs; [
|
};
|
||||||
cargo-watch
|
|
||||||
cargo-nextest
|
|
||||||
cargo-expand
|
|
||||||
|
|
||||||
# Profiling tools
|
|
||||||
gnuplot
|
|
||||||
samply
|
|
||||||
cargo-flamegraph
|
|
||||||
];
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Development shell configuration
|
packages = rec {
|
||||||
devShells.default = pkgs.mkShell {
|
graphiteWithArgs =
|
||||||
packages = buildInputs ++ buildTools ++ devTools;
|
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}";
|
default = graphite;
|
||||||
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'
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
devShells.default = import ./dev.nix {
|
||||||
|
inherit
|
||||||
|
pkgs
|
||||||
|
deps
|
||||||
|
libs
|
||||||
|
tools
|
||||||
|
;
|
||||||
|
};
|
||||||
|
|
||||||
|
formatter = pkgs.nixfmt-tree;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
+10
-13
@@ -16,16 +16,13 @@
|
|||||||
# > nix-shell .nix --command "npm start"
|
# > nix-shell .nix --command "npm start"
|
||||||
|
|
||||||
# Uses flake compat to provide a development shell that is identical to the one defined in the flake
|
# Uses flake compat to provide a development shell that is identical to the one defined in the flake
|
||||||
(import
|
(import (
|
||||||
(
|
let
|
||||||
let
|
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
nodeName = lock.nodes.root.inputs.flake-compat;
|
||||||
nodeName = lock.nodes.root.inputs.flake-compat;
|
in
|
||||||
in
|
fetchTarball {
|
||||||
fetchTarball {
|
url = lock.nodes.${nodeName}.locked.url;
|
||||||
url = lock.nodes.${nodeName}.locked.url;
|
sha256 = lock.nodes.${nodeName}.locked.narHash;
|
||||||
sha256 = lock.nodes.${nodeName}.locked.narHash;
|
}
|
||||||
}
|
) { src = ./.; }).shellNix
|
||||||
)
|
|
||||||
{ src = ./.; }
|
|
||||||
).shellNix
|
|
||||||
|
|||||||
Vendored
-1
@@ -13,7 +13,6 @@
|
|||||||
"streetsidesoftware.code-spell-checker",
|
"streetsidesoftware.code-spell-checker",
|
||||||
// Helpful
|
// Helpful
|
||||||
"mhutchie.git-graph",
|
"mhutchie.git-graph",
|
||||||
"waderyan.gitblame",
|
|
||||||
"qezhu.gitlink",
|
"qezhu.gitlink",
|
||||||
"wmaurer.change-case"
|
"wmaurer.change-case"
|
||||||
]
|
]
|
||||||
|
|||||||
Vendored
+10
-7
@@ -33,18 +33,21 @@
|
|||||||
},
|
},
|
||||||
// Rust Analyzer config
|
// Rust Analyzer config
|
||||||
"rust-analyzer.cargo.allTargets": false,
|
"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 config
|
||||||
"eslint.format.enable": true,
|
"eslint.format.enable": true,
|
||||||
"eslint.workingDirectories": ["./frontend", "./website/other/bezier-rs-demos", "./website"],
|
"eslint.workingDirectories": ["./frontend", "./website"],
|
||||||
"eslint.validate": ["javascript", "typescript", "svelte"],
|
"eslint.validate": ["javascript", "typescript", "svelte"],
|
||||||
// Svelte config
|
// Svelte config
|
||||||
"svelte.plugin.svelte.compilerWarnings": {
|
"svelte.plugin.svelte.compilerWarnings": {
|
||||||
// NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
"css-unused-selector": "ignore", // 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", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||||
"vite-plugin-svelte-css-no-scopable-elements": "ignore",
|
"a11y-no-static-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||||
"a11y-no-static-element-interactions": "ignore",
|
"a11y-no-noninteractive-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||||
"a11y-no-noninteractive-element-interactions": "ignore",
|
"a11y-click-events-have-key-events": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||||
"a11y-click-events-have-key-events": "ignore"
|
|
||||||
},
|
},
|
||||||
// VS Code config
|
// VS Code config
|
||||||
"html.format.wrapLineLength": 200,
|
"html.format.wrapLineLength": 200,
|
||||||
|
|||||||
Generated
+2107
-1053
File diff suppressed because it is too large
Load Diff
+153
-73
@@ -1,72 +1,132 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"editor",
|
|
||||||
"desktop",
|
"desktop",
|
||||||
"proc-macros",
|
"desktop/wrapper",
|
||||||
|
"desktop/embedded-resources",
|
||||||
|
"desktop/bundle",
|
||||||
|
"desktop/platform/linux",
|
||||||
|
"desktop/platform/mac",
|
||||||
|
"desktop/platform/win",
|
||||||
|
"editor",
|
||||||
"frontend/wasm",
|
"frontend/wasm",
|
||||||
"node-graph/gapplication-io",
|
"libraries/dyn-any",
|
||||||
"node-graph/gbrush",
|
"libraries/path-bool",
|
||||||
"node-graph/gcore",
|
"libraries/math-parser",
|
||||||
"node-graph/gcore-shaders",
|
"node-graph/libraries/application-io",
|
||||||
"node-graph/gstd",
|
"node-graph/libraries/core-types",
|
||||||
"node-graph/gmath-nodes",
|
"node-graph/libraries/no-std-types",
|
||||||
"node-graph/gpath-bool",
|
"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/graph-craft",
|
||||||
"node-graph/graphene-cli",
|
"node-graph/graphene-cli",
|
||||||
"node-graph/graster-nodes",
|
"node-graph/nodes/gstd",
|
||||||
"node-graph/gsvg-renderer",
|
|
||||||
"node-graph/interpreted-executor",
|
"node-graph/interpreted-executor",
|
||||||
"node-graph/node-macro",
|
"node-graph/node-macro",
|
||||||
"node-graph/preprocessor",
|
"node-graph/preprocessor",
|
||||||
"libraries/dyn-any",
|
"proc-macros",
|
||||||
"libraries/path-bool",
|
"tools/crate-hierarchy-viz"
|
||||||
"libraries/bezier-rs",
|
|
||||||
"libraries/math-parser",
|
|
||||||
"website/other/bezier-rs-demos/wasm",
|
|
||||||
]
|
]
|
||||||
default-members = [
|
default-members = [
|
||||||
"editor",
|
"editor",
|
||||||
"frontend/wasm",
|
"frontend/wasm",
|
||||||
"node-graph/gbrush",
|
"libraries/dyn-any",
|
||||||
"node-graph/gcore",
|
"libraries/path-bool",
|
||||||
"node-graph/gcore-shaders",
|
"libraries/math-parser",
|
||||||
"node-graph/gstd",
|
"node-graph/libraries/application-io",
|
||||||
"node-graph/gmath-nodes",
|
"node-graph/libraries/core-types",
|
||||||
"node-graph/gpath-bool",
|
"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/graph-craft",
|
||||||
"node-graph/graphene-cli",
|
"node-graph/graphene-cli",
|
||||||
"node-graph/graster-nodes",
|
"node-graph/nodes/gstd",
|
||||||
"node-graph/gsvg-renderer",
|
|
||||||
"node-graph/interpreted-executor",
|
"node-graph/interpreted-executor",
|
||||||
"node-graph/node-macro",
|
"node-graph/node-macro",
|
||||||
|
"node-graph/preprocessor",
|
||||||
|
# blocked by https://github.com/rust-lang/cargo/issues/15890
|
||||||
|
# "proc-macros",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
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]
|
[workspace.dependencies]
|
||||||
# Local dependencies
|
# Local dependencies
|
||||||
bezier-rs = { path = "libraries/bezier-rs", features = ["dyn-any", "serde"] }
|
dyn-any = { path = "libraries/dyn-any", features = [
|
||||||
dyn-any = { path = "libraries/dyn-any", features = ["derive", "glam", "reqwest", "log-bad-types", "rc"] }
|
"derive",
|
||||||
preprocessor = { path = "node-graph/preprocessor"}
|
"glam",
|
||||||
|
"reqwest",
|
||||||
|
"log-bad-types",
|
||||||
|
"rc",
|
||||||
|
] }
|
||||||
|
preprocessor = { path = "node-graph/preprocessor" }
|
||||||
math-parser = { path = "libraries/math-parser" }
|
math-parser = { path = "libraries/math-parser" }
|
||||||
path-bool = { path = "libraries/path-bool" }
|
path-bool = { path = "libraries/path-bool" }
|
||||||
graphene-application-io = { path = "node-graph/gapplication-io" }
|
graphene-application-io = { path = "node-graph/libraries/application-io" }
|
||||||
graphene-brush = { path = "node-graph/gbrush" }
|
core-types = { path = "node-graph/libraries/core-types" }
|
||||||
graphene-core = { path = "node-graph/gcore" }
|
no-std-types = { path = "node-graph/libraries/no-std-types" }
|
||||||
graphene-core-shaders = { path = "node-graph/gcore-shaders" }
|
raster-types = { path = "node-graph/libraries/raster-types" }
|
||||||
graphene-math-nodes = { path = "node-graph/gmath-nodes" }
|
vector-types = { path = "node-graph/libraries/vector-types" }
|
||||||
graphene-path-bool = { path = "node-graph/gpath-bool" }
|
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" }
|
graph-craft = { path = "node-graph/graph-craft" }
|
||||||
graphene-raster-nodes = { path = "node-graph/graster-nodes" }
|
raster-nodes = { path = "node-graph/nodes/raster" }
|
||||||
graphene-std = { path = "node-graph/gstd" }
|
graphene-std = { path = "node-graph/nodes/gstd" }
|
||||||
graphene-svg-renderer = { path = "node-graph/gsvg-renderer" }
|
|
||||||
interpreted-executor = { path = "node-graph/interpreted-executor" }
|
interpreted-executor = { path = "node-graph/interpreted-executor" }
|
||||||
node-macro = { path = "node-graph/node-macro" }
|
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" }
|
graphite-proc-macros = { path = "proc-macros" }
|
||||||
|
|
||||||
# Workspace dependencies
|
# Workspace dependencies
|
||||||
rustc-hash = "2.0"
|
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 = { version = "1.0", features = ["derive", "rc"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde-wasm-bindgen = "0.6"
|
serde-wasm-bindgen = "0.6"
|
||||||
@@ -76,17 +136,16 @@ env_logger = "0.11"
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
bitflags = { version = "2.4", features = ["serde"] }
|
bitflags = { version = "2.4", features = ["serde"] }
|
||||||
ctor = "0.2"
|
ctor = "0.2"
|
||||||
convert_case = "0.7"
|
convert_case = "0.8"
|
||||||
derivative = "2.2"
|
derivative = "2.2"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
proc-macro2 = { version = "1", features = [ "span-locations" ] }
|
proc-macro2 = { version = "1", features = ["span-locations"] }
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
axum = "0.8"
|
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
ron = "0.8"
|
ron = "0.11"
|
||||||
fastnoise-lite = "1.1"
|
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
|
# 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",
|
"fragile-send-sync-non-atomic-wasm",
|
||||||
"spirv",
|
"spirv",
|
||||||
@@ -114,24 +173,34 @@ web-sys = { version = "=0.3.77", features = [
|
|||||||
"HtmlImageElement",
|
"HtmlImageElement",
|
||||||
"ImageBitmapRenderingContext",
|
"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"
|
url = "2.5"
|
||||||
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt"] }
|
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
|
vello = { git = "https://github.com/linebender/vello" }
|
||||||
resvg = "0.44"
|
vello_encoding = { git = "https://github.com/linebender/vello" }
|
||||||
usvg = "0.44"
|
resvg = "0.45"
|
||||||
|
usvg = "0.45"
|
||||||
rand = { version = "0.9", default-features = false, features = ["std_rng"] }
|
rand = { version = "0.9", default-features = false, features = ["std_rng"] }
|
||||||
rand_chacha = "0.9"
|
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"
|
base64 = "0.22"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp"] }
|
image = { version = "0.25", default-features = false, features = [
|
||||||
parley = "0.5.0"
|
"png",
|
||||||
skrifa = "0.32.0"
|
"jpeg",
|
||||||
pretty_assertions = "1.4.1"
|
"bmp",
|
||||||
|
] }
|
||||||
|
parley = "0.6"
|
||||||
|
skrifa = "0.36"
|
||||||
|
pretty_assertions = "1.4"
|
||||||
fern = { version = "0.7", features = ["colored"] }
|
fern = { version = "0.7", features = ["colored"] }
|
||||||
num_enum = "0.7"
|
num_enum = { version = "0.7", default-features = false }
|
||||||
num-derive = "0.4"
|
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 = [
|
specta = { version = "2.0.0-rc.22", features = [
|
||||||
"glam",
|
"glam",
|
||||||
"derive",
|
"derive",
|
||||||
@@ -148,32 +217,38 @@ syn = { version = "2.0", default-features = false, features = [
|
|||||||
"extra-traits",
|
"extra-traits",
|
||||||
"proc-macro",
|
"proc-macro",
|
||||||
] }
|
] }
|
||||||
kurbo = { version = "0.11.0", features = ["serde"] }
|
kurbo = { version = "0.12", features = ["serde"] }
|
||||||
petgraph = { version = "0.7.1", default-features = false, features = [
|
lyon_geom = "1.0"
|
||||||
"graphmap",
|
petgraph = { version = "0.7", default-features = false, features = ["graphmap"] }
|
||||||
] }
|
half = { version = "2.4", default-features = false, features = ["bytemuck"] }
|
||||||
half = { version = "2.4.1", default-features = false, features = ["bytemuck"] }
|
|
||||||
tinyvec = { version = "1", features = ["std"] }
|
tinyvec = { version = "1", features = ["std"] }
|
||||||
criterion = { version = "0.5", features = ["html_reports"] }
|
criterion = { version = "0.7", features = ["html_reports"] }
|
||||||
iai-callgrind = { version = "0.12.3" }
|
iai-callgrind = { version = "0.16" }
|
||||||
ndarray = "0.16.1"
|
ndarray = "0.16"
|
||||||
strum = { version = "0.26.3", features = ["derive"] }
|
strum = { version = "0.27", features = ["derive"] }
|
||||||
dirs = "6.0"
|
dirs = "6.0"
|
||||||
cef = "138.5.0"
|
cef = "142"
|
||||||
include_dir = "0.7.4"
|
cef-dll-sys = "142"
|
||||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
include_dir = "0.7"
|
||||||
tracing = "0.1.41"
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
rfd = "0.15.4"
|
tracing = "0.1"
|
||||||
open = "5.3.2"
|
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]
|
[profile.dev]
|
||||||
opt-level = 1
|
opt-level = 1
|
||||||
|
|
||||||
[profile.dev.package]
|
[profile.dev.package]
|
||||||
graphite-editor = { opt-level = 1 }
|
no-std-types = { opt-level = 1 }
|
||||||
graphene-core-shaders = { opt-level = 1 }
|
core-types= { opt-level = 1 }
|
||||||
graphene-core = { opt-level = 1 }
|
|
||||||
graphene-std = { opt-level = 1 }
|
|
||||||
interpreted-executor = { opt-level = 1 } # This is a mitigation for https://github.com/rustwasm/wasm-pack/issues/981 which is needed because the node_registry function is too large
|
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 }
|
graphite-proc-macros = { opt-level = 1 }
|
||||||
image = { opt-level = 2 }
|
image = { opt-level = 2 }
|
||||||
@@ -181,6 +256,7 @@ rustc-hash = { opt-level = 3 }
|
|||||||
serde_derive = { opt-level = 1 }
|
serde_derive = { opt-level = 1 }
|
||||||
specta-macros = { opt-level = 1 }
|
specta-macros = { opt-level = 1 }
|
||||||
syn = { opt-level = 1 }
|
syn = { opt-level = 1 }
|
||||||
|
node-macro = { opt-level = 2 }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
@@ -189,3 +265,7 @@ debug = true
|
|||||||
[profile.profiling]
|
[profile.profiling]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
debug = true
|
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" }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
<a href="https://graphite.rs/">
|
<a href="https://graphite.art/">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/9366c148-4405-484f-909a-9a3526eb9209">
|
<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">
|
<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
|
# 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 />
|
<br />
|
||||||
<a href="https://discord.graphite.rs/">
|
<a href="https://discord.graphite.art/">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/ad185fac-3b48-446d-863c-2bcb0724abee">
|
<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">
|
<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 ❤️
|
## 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.
|
> 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
|
## 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).*
|
*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).*
|
||||||
|
|||||||
+18
-18
@@ -1,22 +1,22 @@
|
|||||||
# Keep this list in sync with those in `/deny.toml` and `/frontend/vite.config.ts`.
|
|
||||||
accepted = [
|
accepted = [
|
||||||
"Apache-2.0 WITH LLVM-exception",
|
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/deny.toml`
|
||||||
"Apache-2.0",
|
"Apache-2.0", # Keep this list in sync with those in `/deny.toml`
|
||||||
"BSD-2-Clause",
|
"BSD-2-Clause", # Keep this list in sync with those in `/deny.toml`
|
||||||
"BSD-3-Clause",
|
"BSD-3-Clause", # Keep this list in sync with those in `/deny.toml`
|
||||||
"BSL-1.0",
|
"BSL-1.0", # Keep this list in sync with those in `/deny.toml`
|
||||||
"CC0-1.0",
|
"CC0-1.0", # Keep this list in sync with those in `/deny.toml`
|
||||||
"CDLA-Permissive-2.0",
|
"CDLA-Permissive-2.0", # Keep this list in sync with those in `/deny.toml`
|
||||||
"ISC",
|
"ISC", # Keep this list in sync with those in `/deny.toml`
|
||||||
"MIT-0",
|
"MIT-0", # Keep this list in sync with those in `/deny.toml`
|
||||||
"MIT",
|
"MIT", # Keep this list in sync with those in `/deny.toml`
|
||||||
"MPL-2.0",
|
"MPL-2.0", # Keep this list in sync with those in `/deny.toml`
|
||||||
"OpenSSL",
|
"OpenSSL", # Keep this list in sync with those in `/deny.toml`
|
||||||
"Unicode-3.0",
|
"Unicode-3.0", # Keep this list in sync with those in `/deny.toml`
|
||||||
"Unicode-DFS-2016",
|
"Unicode-DFS-2016", # Keep this list in sync with those in `/deny.toml`
|
||||||
"Zlib",
|
"Zlib", # Keep this list in sync with those in `/deny.toml`
|
||||||
"NCSA",
|
"NCSA", # Keep this list in sync with those in `/deny.toml`
|
||||||
"bzip2-1.0.6",
|
"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"]
|
workarounds = ["ring"]
|
||||||
ignore-build-dependencies = true
|
ignore-build-dependencies = true
|
||||||
|
|||||||
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
@@ -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
|
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||||
# output a note when they are encountered.
|
# output a note when they are encountered.
|
||||||
ignore = [
|
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-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
|
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
||||||
# lower than the range specified will be ignored. Note that ignored advisories
|
# 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
|
# See https://spdx.org/licenses/ for list of possible licenses
|
||||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
# [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 = [
|
allow = [
|
||||||
"Apache-2.0 WITH LLVM-exception",
|
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/about.toml`
|
||||||
"Apache-2.0",
|
"Apache-2.0", # Keep this list in sync with those in `/about.toml`
|
||||||
"BSD-2-Clause",
|
"BSD-2-Clause", # Keep this list in sync with those in `/about.toml`
|
||||||
"BSD-3-Clause",
|
"BSD-3-Clause", # Keep this list in sync with those in `/about.toml`
|
||||||
"BSL-1.0",
|
"BSL-1.0", # Keep this list in sync with those in `/about.toml`
|
||||||
"CC0-1.0",
|
"CC0-1.0", # Keep this list in sync with those in `/about.toml`
|
||||||
"CDLA-Permissive-2.0",
|
"CDLA-Permissive-2.0", # Keep this list in sync with those in `/about.toml`
|
||||||
"ISC",
|
"ISC", # Keep this list in sync with those in `/about.toml`
|
||||||
"MIT-0",
|
"MIT-0", # Keep this list in sync with those in `/about.toml`
|
||||||
"MIT",
|
"MIT", # Keep this list in sync with those in `/about.toml`
|
||||||
"MPL-2.0",
|
"MPL-2.0", # Keep this list in sync with those in `/about.toml`
|
||||||
"OpenSSL",
|
"OpenSSL", # Keep this list in sync with those in `/about.toml`
|
||||||
"Unicode-3.0",
|
"Unicode-3.0", # Keep this list in sync with those in `/about.toml`
|
||||||
"Unicode-DFS-2016",
|
"Unicode-DFS-2016", # Keep this list in sync with those in `/about.toml`
|
||||||
"Zlib",
|
"Zlib", # Keep this list in sync with those in `/about.toml`
|
||||||
"NCSA",
|
"NCSA", # Keep this list in sync with those in `/about.toml`
|
||||||
"bzip2-1.0.6",
|
"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 confidence threshold for detecting a license from license text.
|
||||||
# The higher the value, the more closely the license text must be to the
|
# The higher the value, the more closely the license text must be to the
|
||||||
|
|||||||
+43
-15
@@ -2,40 +2,68 @@
|
|||||||
name = "graphite-desktop"
|
name = "graphite-desktop"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Graphite Desktop"
|
description = "Graphite Desktop"
|
||||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
repository = ""
|
repository = ""
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.87"
|
rust-version = "1.87"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "graphite"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["gpu"]
|
default = ["recommended", "embedded_resources"]
|
||||||
gpu = ["graphite-editor/gpu"]
|
recommended = ["gpu", "accelerated_paint"]
|
||||||
|
embedded_resources = ["dep:graphite-desktop-embedded-resources"]
|
||||||
|
gpu = ["graphite-desktop-wrapper/gpu"]
|
||||||
|
accelerated_paint = ["cef/accelerated_osr"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# # Local dependencies
|
# Local dependencies
|
||||||
graphite-editor = { path = "../editor", features = [
|
graphite-desktop-wrapper = { path = "wrapper" }
|
||||||
"gpu",
|
graphite-desktop-embedded-resources = { path = "embedded-resources", optional = true }
|
||||||
"ron",
|
|
||||||
"vello",
|
|
||||||
] }
|
|
||||||
graphene-std = { workspace = true }
|
|
||||||
graph-craft = { workspace = true }
|
|
||||||
wgpu-executor = { workspace = true }
|
|
||||||
|
|
||||||
wgpu = { workspace = true }
|
wgpu = { workspace = true }
|
||||||
winit = { workspace = true, features = ["serde"] }
|
winit = { workspace = true, features = [ "wayland-csd-adwaita-notitlebar", "serde" ] }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
cef = { workspace = true }
|
cef = { workspace = true }
|
||||||
include_dir = { workspace = true }
|
cef-dll-sys = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
ron = { workspace = true}
|
ron = { workspace = true }
|
||||||
bytemuck = { workspace = true }
|
bytemuck = { workspace = true }
|
||||||
glam = { workspace = true }
|
glam = { workspace = true }
|
||||||
vello = { workspace = true }
|
vello = { workspace = true }
|
||||||
derivative = { workspace = true }
|
derivative = { workspace = true }
|
||||||
rfd = { workspace = true }
|
rfd = { workspace = true }
|
||||||
open = { 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 }
|
||||||
|
|||||||
+3
-3
@@ -2,10 +2,10 @@
|
|||||||
Name=Graphite
|
Name=Graphite
|
||||||
GenericName=Vector & Raster Graphics Editor
|
GenericName=Vector & Raster Graphics Editor
|
||||||
Comment=Open-source vector & raster graphics editor. Featuring node based procedural nondestructive editing workflow.
|
Comment=Open-source vector & raster graphics editor. Featuring node based procedural nondestructive editing workflow.
|
||||||
Exec=graphite-editor
|
Exec=graphite
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Icon=graphite-icon-color
|
Icon=graphite
|
||||||
Categories=Graphics;VectorGraphics;RasterGraphics;
|
Categories=Graphics;VectorGraphics;RasterGraphics;
|
||||||
Keywords=graphite;editor;vector;raster;procedural;design;
|
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 |
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "graphite-desktop-bundle"
|
||||||
|
version = "0.0.0"
|
||||||
|
description = "Graphite Desktop Bundle"
|
||||||
|
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||||
|
license = "Apache-2.0"
|
||||||
|
repository = ""
|
||||||
|
edition = "2024"
|
||||||
|
rust-version = "1.87"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cef-dll-sys = { workspace = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
|
plist = { version = "*" }
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-env-changed=CARGO_PROFILE");
|
||||||
|
println!("cargo:rerun-if-env-changed=PROFILE");
|
||||||
|
let profile = std::env::var("CARGO_PROFILE").or_else(|_| std::env::var("PROFILE")).unwrap();
|
||||||
|
println!("cargo:rustc-env=CARGO_PROFILE={profile}");
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-env-changed=DEP_CEF_DLL_WRAPPER_CEF_DIR");
|
||||||
|
let cef_dir = std::env::var("DEP_CEF_DLL_WRAPPER_CEF_DIR").unwrap();
|
||||||
|
println!("cargo:rustc-env=CEF_PATH={cef_dir}");
|
||||||
|
}
|
||||||
@@ -0,0 +1,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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
mod common;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
mod linux;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod mac;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
mod win;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
linux::main().unwrap();
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mac::main().unwrap();
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
win::main().unwrap();
|
||||||
|
}
|
||||||
@@ -0,0 +1,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
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "graphite-desktop-embedded-resources"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Graphite Desktop Embedded Resources"
|
||||||
|
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||||
|
license = "Apache-2.0"
|
||||||
|
repository = ""
|
||||||
|
edition = "2024"
|
||||||
|
rust-version = "1.87"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
include_dir = { workspace = true }
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(embedded_resources)'] }
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
const EMBEDDED_RESOURCES_ENV: &str = "EMBEDDED_RESOURCES";
|
||||||
|
const DEFAULT_RESOURCES_DIR: &str = "../../frontend/dist";
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut embedded_resources: Option<String> = None;
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-env-changed={EMBEDDED_RESOURCES_ENV}");
|
||||||
|
if let Ok(embedded_resources_env) = std::env::var(EMBEDDED_RESOURCES_ENV)
|
||||||
|
&& std::path::PathBuf::from(&embedded_resources_env).exists()
|
||||||
|
{
|
||||||
|
embedded_resources = Some(embedded_resources_env);
|
||||||
|
}
|
||||||
|
|
||||||
|
if embedded_resources.is_none() {
|
||||||
|
// Check if the directory `DEFAULT_RESOURCES_DIR` exists and sets the embedded_resources cfg accordingly
|
||||||
|
// Absolute path of `DEFAULT_RESOURCES_DIR` available via the `EMBEDDED_RESOURCES` environment variable
|
||||||
|
let crate_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
println!("cargo:rerun-if-changed={DEFAULT_RESOURCES_DIR}");
|
||||||
|
if let Ok(resources) = crate_dir.join(DEFAULT_RESOURCES_DIR).canonicalize()
|
||||||
|
&& resources.exists()
|
||||||
|
{
|
||||||
|
embedded_resources = Some(resources.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(embedded_resources) = embedded_resources {
|
||||||
|
println!("cargo:rustc-cfg=embedded_resources");
|
||||||
|
println!("cargo:rustc-env={EMBEDDED_RESOURCES_ENV}={embedded_resources}");
|
||||||
|
} else {
|
||||||
|
println!("cargo:warning=Resource directory does not exist. Resources will not be embedded. Did you forget to build the frontend?");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//! This crate provides `EMBEDDED_RESOURCES` that can be included in the desktop application binary.
|
||||||
|
//! It is intended to be used by the `embedded_resources` feature of the `graphite-desktop` crate.
|
||||||
|
//! The build script checks if the specified resources directory exists and sets the `embedded_resources` cfg flag accordingly.
|
||||||
|
//! If the resources directory does not exist, resources will not be embedded and a warning will be reported during compilation.
|
||||||
|
|
||||||
|
#[cfg(embedded_resources)]
|
||||||
|
pub static EMBEDDED_RESOURCES: Option<include_dir::Dir> = Some(include_dir::include_dir!("$EMBEDDED_RESOURCES"));
|
||||||
|
|
||||||
|
#[cfg(not(embedded_resources))]
|
||||||
|
pub static EMBEDDED_RESOURCES: Option<include_dir::Dir> = None;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "graphite-desktop-platform-linux"
|
||||||
|
version = "0.0.0"
|
||||||
|
description = "Graphite Desktop Platform Linux"
|
||||||
|
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||||
|
license = "Apache-2.0"
|
||||||
|
repository = ""
|
||||||
|
edition = "2024"
|
||||||
|
rust-version = "1.87"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "graphite"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
graphite-desktop = { path = "../.." }
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
graphite_desktop::start();
|
||||||
|
}
|
||||||
@@ -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 = "../.." }
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
graphite_desktop::start_helper();
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
graphite_desktop::start();
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "graphite-desktop-platform-win"
|
||||||
|
version = "0.0.0"
|
||||||
|
description = "Graphite Desktop Platform Windows"
|
||||||
|
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||||
|
license = "Apache-2.0"
|
||||||
|
repository = ""
|
||||||
|
edition = "2024"
|
||||||
|
rust-version = "1.87"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "graphite"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
graphite-desktop = { path = "../.." }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.build-dependencies]
|
||||||
|
winres = "0.1"
|
||||||
@@ -0,0 +1,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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#![windows_subsystem = "windows"]
|
||||||
|
fn main() {
|
||||||
|
graphite_desktop::start();
|
||||||
|
}
|
||||||
+520
-232
@@ -1,291 +1,559 @@
|
|||||||
use crate::CustomEvent;
|
use rfd::AsyncFileDialog;
|
||||||
use crate::WindowSize;
|
use std::fs;
|
||||||
use crate::consts::APP_NAME;
|
use std::path::PathBuf;
|
||||||
use crate::dialogs::dialog_open_graphite_file;
|
use std::sync::mpsc::{Receiver, Sender, SyncSender};
|
||||||
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 std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
use std::time::Instant;
|
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
use winit::dpi::PhysicalSize;
|
use winit::dpi::PhysicalSize;
|
||||||
use winit::event::StartCause;
|
use winit::event::{ButtonSource, ElementState, MouseButton, WindowEvent};
|
||||||
use winit::event::WindowEvent;
|
use winit::event_loop::{ActiveEventLoop, ControlFlow};
|
||||||
use winit::event_loop::ActiveEventLoop;
|
|
||||||
use winit::event_loop::ControlFlow;
|
|
||||||
use winit::event_loop::EventLoopProxy;
|
|
||||||
use winit::window::Window;
|
|
||||||
use winit::window::WindowId;
|
use winit::window::WindowId;
|
||||||
|
|
||||||
use crate::cef;
|
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 {
|
pub(crate) struct App {
|
||||||
cef_context: cef::Context<cef::Initialized>,
|
render_state: Option<RenderState>,
|
||||||
window: Option<Arc<Window>>,
|
|
||||||
cef_schedule: Option<Instant>,
|
|
||||||
window_size_sender: Sender<WindowSize>,
|
|
||||||
graphics_state: Option<GraphicsState>,
|
|
||||||
wgpu_context: WgpuContext,
|
wgpu_context: WgpuContext,
|
||||||
event_loop_proxy: EventLoopProxy<CustomEvent>,
|
window: Option<Window>,
|
||||||
editor: Editor,
|
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 {
|
impl App {
|
||||||
pub(crate) fn new(cef_context: cef::Context<cef::Initialized>, window_size_sender: Sender<WindowSize>, wgpu_context: WgpuContext, event_loop_proxy: EventLoopProxy<CustomEvent>) -> Self {
|
pub(crate) fn init() {
|
||||||
Self {
|
Window::init();
|
||||||
cef_context,
|
|
||||||
window: None,
|
|
||||||
cef_schedule: Some(Instant::now()),
|
|
||||||
graphics_state: None,
|
|
||||||
window_size_sender,
|
|
||||||
wgpu_context,
|
|
||||||
event_loop_proxy,
|
|
||||||
editor: Editor::new(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_message(&mut self, message: Message) {
|
pub(crate) fn new(
|
||||||
let responses = self.editor.handle_message(message);
|
cef_context: Box<dyn cef::CefContext>,
|
||||||
self.send_messages_to_editor(responses);
|
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>) {
|
let rendering_app_event_scheduler = app_event_scheduler.clone();
|
||||||
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::RenderOverlays(_))) {
|
let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1);
|
||||||
let FrontendMessage::RenderOverlays(overlay_context) = message else { unreachable!() };
|
std::thread::spawn(move || {
|
||||||
if let Some(graphics_state) = &mut self.graphics_state {
|
loop {
|
||||||
let scene = overlay_context.take_scene();
|
let result = futures::executor::block_on(DesktopWrapper::execute_node_graph());
|
||||||
graphics_state.set_overlays_scene(scene);
|
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 fullscreen = window.is_fullscreen();
|
||||||
let event_loop_proxy = self.event_loop_proxy.clone();
|
if fullscreen != self.window_fullscreen {
|
||||||
let _ = thread::spawn(move || {
|
self.window_fullscreen = fullscreen;
|
||||||
let path = futures::executor::block_on(dialog_open_graphite_file());
|
self.app_event_scheduler
|
||||||
if let Some(path) = path {
|
.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::UpdateFullscreen { fullscreen }));
|
||||||
let content = std::fs::read_to_string(&path).unwrap_or_else(|_| {
|
}
|
||||||
tracing::error!("Failed to read file: {}", path.display());
|
|
||||||
String::new()
|
let size = window.surface_size();
|
||||||
});
|
let scale = window.scale_factor() * self.ui_scale;
|
||||||
let message = PortfolioMessage::OpenDocumentFile {
|
let is_new_size = size != self.window_size;
|
||||||
document_name: path.file_name().and_then(|s| s.to_str()).unwrap_or("unknown").to_string(),
|
let is_new_scale = scale != self.window_scale;
|
||||||
document_serialized_content: content,
|
|
||||||
};
|
if !is_new_size && !is_new_scale {
|
||||||
let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into()));
|
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 { .. })) {
|
self.cef_context.notify_view_info_changed();
|
||||||
let FrontendMessage::TriggerSaveDocument { document_id, name, path, content } = message else {
|
|
||||||
unreachable!()
|
if let Some(render_state) = &mut self.render_state {
|
||||||
};
|
render_state.resize(size.width, size.height);
|
||||||
if let Some(path) = path {
|
}
|
||||||
let _ = std::fs::write(&path, content);
|
|
||||||
} else {
|
window.request_redraw();
|
||||||
let event_loop_proxy = self.event_loop_proxy.clone();
|
|
||||||
|
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 _ = thread::spawn(move || {
|
||||||
let path = futures::executor::block_on(dialog_save_graphite_file(name));
|
let mut dialog = AsyncFileDialog::new().set_title(title);
|
||||||
if let Some(path) = path {
|
for filter in filters {
|
||||||
if let Err(e) = std::fs::write(&path, content) {
|
dialog = dialog.add_filter(filter.name, &filter.extensions);
|
||||||
tracing::error!("Failed to save file: {}: {}", path.display(), e);
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
let message = Message::Portfolio(PortfolioMessage::DocumentPassMessage {
|
tracing::error!("Failed to read file: {}", path.display());
|
||||||
document_id,
|
|
||||||
message: DocumentMessage::SavedDocument { path: Some(path) },
|
|
||||||
});
|
|
||||||
let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
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 { .. })) {
|
fn handle_desktop_frontend_messages(&mut self, messages: Vec<DesktopFrontendMessage>) {
|
||||||
let FrontendMessage::TriggerSaveFile { name, content } = message else { unreachable!() };
|
let mut responses = Vec::new();
|
||||||
let _ = thread::spawn(move || {
|
for message in messages {
|
||||||
let path = futures::executor::block_on(dialog_save_file(name));
|
self.handle_desktop_frontend_message(message, &mut responses);
|
||||||
if let Some(path) = path {
|
}
|
||||||
if let Err(e) = std::fs::write(&path, content) {
|
for message in responses {
|
||||||
tracing::error!("Failed to save file: {}: {}", path.display(), e);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
NodeGraphExecutionResult::NotRun => {}
|
||||||
}
|
},
|
||||||
|
AppEvent::UiUpdate(texture) => {
|
||||||
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerVisitLink { .. })) {
|
if let Some(render_state) = self.render_state.as_mut() {
|
||||||
let _ = thread::spawn(move || {
|
render_state.bind_ui_texture(texture);
|
||||||
let FrontendMessage::TriggerVisitLink { url } = message else { unreachable!() };
|
let elapsed = self.last_ui_update.elapsed().as_secs_f32();
|
||||||
if let Err(e) = open::that(&url) {
|
self.last_ui_update = Instant::now();
|
||||||
tracing::error!("Failed to open URL: {}: {}", url, e);
|
if elapsed < 0.5 {
|
||||||
}
|
self.avg_frame_time = (self.avg_frame_time * 3. + elapsed) / 4.;
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
if let Some(window) = &self.window {
|
if let Some(window) = &self.window {
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CustomEvent::ScheduleBrowserWork(instant) => {
|
AppEvent::ScheduleBrowserWork(instant) => {
|
||||||
if instant <= Instant::now() {
|
if instant <= Instant::now() {
|
||||||
self.cef_context.work();
|
self.cef_context.work();
|
||||||
} else {
|
} else {
|
||||||
self.cef_schedule = Some(instant);
|
self.cef_schedule = Some(instant);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CustomEvent::DispatchMessage(message) => {
|
AppEvent::CursorChange(cursor) => {
|
||||||
self.dispatch_message(message);
|
if let Some(window) = &mut self.window {
|
||||||
|
window.set_cursor(event_loop, cursor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
CustomEvent::MessageReceived(message) => {
|
AppEvent::CloseWindow => {
|
||||||
if let Message::InputPreprocessor(_) = &message {
|
// TODO: Implement graceful shutdown
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.dispatch_message(message);
|
tracing::info!("Exiting main event loop");
|
||||||
|
event_loop.exit();
|
||||||
}
|
}
|
||||||
CustomEvent::NodeGraphRan(texture) => {
|
#[cfg(target_os = "macos")]
|
||||||
if let Some(texture) = texture
|
AppEvent::MenuEvent { id } => {
|
||||||
&& let Some(graphics_state) = &mut self.graphics_state
|
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id });
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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 render_state = RenderState::new(self.window.as_ref().unwrap(), self.wgpu_context.clone());
|
||||||
let Some(event) = self.cef_context.handle_window_event(event) else { return };
|
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 {
|
match event {
|
||||||
WindowEvent::CloseRequested => {
|
WindowEvent::CloseRequested => {
|
||||||
tracing::info!("The close button was pressed; stopping");
|
self.app_event_scheduler.schedule(AppEvent::CloseWindow);
|
||||||
event_loop.exit();
|
|
||||||
}
|
}
|
||||||
WindowEvent::Resized(PhysicalSize { width, height }) => {
|
WindowEvent::SurfaceResized(_) | WindowEvent::ScaleFactorChanged { .. } => {
|
||||||
let _ = self.window_size_sender.send(WindowSize::new(width as usize, height as usize));
|
self.resize();
|
||||||
self.cef_context.notify_of_resize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowEvent::RedrawRequested => {
|
WindowEvent::RedrawRequested => {
|
||||||
let Some(ref mut graphics_state) = self.graphics_state else { return };
|
#[cfg(target_os = "macos")]
|
||||||
// Only rerender once we have a new ui texture to display
|
self.resize();
|
||||||
|
|
||||||
match graphics_state.render() {
|
let Some(render_state) = &mut self.render_state else { return };
|
||||||
Ok(_) => {}
|
if let Some(window) = &self.window {
|
||||||
Err(wgpu::SurfaceError::Lost) => {
|
if !window.can_render() {
|
||||||
tracing::warn!("lost surface");
|
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
|
// Notify cef of possible input events
|
||||||
self.cef_context.work();
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+218
-54
@@ -1,80 +1,152 @@
|
|||||||
use crate::{CustomEvent, WgpuContext, render::FrameBufferRef};
|
//! CEF (Chromium Embedded Framework) integration for Graphite Desktop
|
||||||
use std::{
|
//!
|
||||||
sync::{Arc, Mutex, mpsc::Receiver},
|
//! This module provides CEF browser integration with hardware-accelerated texture sharing.
|
||||||
time::Instant,
|
//!
|
||||||
};
|
//! # 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 context;
|
||||||
mod dirs;
|
mod dirs;
|
||||||
mod input;
|
mod input;
|
||||||
mod internal;
|
mod internal;
|
||||||
mod ipc;
|
mod ipc;
|
||||||
mod scheme_handler;
|
mod platform;
|
||||||
mod utility;
|
mod utility;
|
||||||
|
|
||||||
pub(crate) use context::{Context, InitError, Initialized, Setup, SetupError};
|
#[cfg(feature = "accelerated_paint")]
|
||||||
use winit::event_loop::EventLoopProxy;
|
use cef::osr_texture_import::SharedTextureHandle;
|
||||||
|
|
||||||
pub(crate) trait CefEventHandler: Clone {
|
pub(crate) use context::{CefContext, CefContextBuilder, InitError};
|
||||||
fn window_size(&self) -> WindowSize;
|
|
||||||
|
pub(crate) trait CefEventHandler: Send + Sync + 'static {
|
||||||
|
fn view_info(&self) -> ViewInfo;
|
||||||
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>);
|
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>);
|
||||||
/// Scheudule the main event loop to run the cef event loop after the timeout
|
#[cfg(feature = "accelerated_paint")]
|
||||||
/// [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation.
|
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 schedule_cef_message_loop_work(&self, scheduled_time: Instant);
|
||||||
|
fn initialized_web_communication(&self);
|
||||||
fn receive_web_message(&self, message: &[u8]);
|
fn receive_web_message(&self, message: &[u8]);
|
||||||
|
fn duplicate(&self) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub(crate) struct WindowSize {
|
pub(crate) struct ViewInfo {
|
||||||
pub(crate) width: usize,
|
width: u32,
|
||||||
pub(crate) height: usize,
|
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) enum ViewInfoUpdate {
|
||||||
pub(crate) fn new(width: usize, height: usize) -> Self {
|
Size { width: u32, height: u32 },
|
||||||
Self { width, height }
|
Scale(f64),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct CefHandler {
|
pub(crate) struct Resource {
|
||||||
window_size_receiver: Arc<Mutex<WindowSizeReceiver>>,
|
pub(crate) reader: ResourceReader,
|
||||||
event_loop_proxy: EventLoopProxy<CustomEvent>,
|
pub(crate) mimetype: Option<String>,
|
||||||
wgpu_context: WgpuContext,
|
|
||||||
}
|
}
|
||||||
struct WindowSizeReceiver {
|
|
||||||
receiver: Receiver<WindowSize>,
|
#[expect(dead_code)]
|
||||||
window_size: WindowSize,
|
#[derive(Clone)]
|
||||||
|
pub(crate) enum ResourceReader {
|
||||||
|
Embedded(io::Cursor<&'static [u8]>),
|
||||||
|
File(Arc<File>),
|
||||||
}
|
}
|
||||||
impl WindowSizeReceiver {
|
impl Read for ResourceReader {
|
||||||
fn new(window_size_receiver: Receiver<WindowSize>) -> Self {
|
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
Self {
|
match self {
|
||||||
window_size: WindowSize { width: 1, height: 1 },
|
ResourceReader::Embedded(cursor) => cursor.read(buf),
|
||||||
receiver: window_size_receiver,
|
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 {
|
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 {
|
Self {
|
||||||
window_size_receiver: Arc::new(Mutex::new(WindowSizeReceiver::new(window_size_receiver))),
|
|
||||||
event_loop_proxy,
|
|
||||||
wgpu_context,
|
wgpu_context,
|
||||||
|
app_event_scheduler,
|
||||||
|
view_info_receiver: Arc::new(Mutex::new(ViewInfoReceiver::new(view_info_receiver))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CefEventHandler for CefHandler {
|
impl CefEventHandler for CefHandler {
|
||||||
fn window_size(&self) -> WindowSize {
|
fn view_info(&self) -> ViewInfo {
|
||||||
let Ok(mut guard) = self.window_size_receiver.lock() else {
|
let Ok(mut guard) = self.view_info_receiver.lock() else {
|
||||||
tracing::error!("Failed to lock window_size_receiver");
|
tracing::error!("Failed to lock view_info_receiver");
|
||||||
return WindowSize::new(1, 1);
|
return ViewInfo::new();
|
||||||
};
|
};
|
||||||
let WindowSizeReceiver { receiver, window_size } = &mut *guard;
|
let ViewInfoReceiver { receiver, view_info } = &mut *guard;
|
||||||
for new_window_size in receiver.try_iter() {
|
for update in receiver.try_iter() {
|
||||||
*window_size = new_window_size;
|
view_info.apply_update(update);
|
||||||
}
|
}
|
||||||
*window_size
|
*view_info
|
||||||
}
|
}
|
||||||
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>) {
|
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>) {
|
||||||
let width = frame_buffer.width() as u32;
|
let width = frame_buffer.width() as u32;
|
||||||
@@ -89,7 +161,7 @@ impl CefEventHandler for CefHandler {
|
|||||||
mip_level_count: 1,
|
mip_level_count: 1,
|
||||||
sample_count: 1,
|
sample_count: 1,
|
||||||
dimension: wgpu::TextureDimension::D2,
|
dimension: wgpu::TextureDimension::D2,
|
||||||
format: wgpu::TextureFormat::Bgra8UnormSrgb,
|
format: wgpu::TextureFormat::Bgra8Unorm,
|
||||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||||
view_formats: &[],
|
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) {
|
#[cfg(feature = "accelerated_paint")]
|
||||||
let _ = self.event_loop_proxy.send_event(CustomEvent::ScheduleBrowserWork(scheduled_time));
|
fn draw_gpu(&self, shared_texture: SharedTextureHandle) {
|
||||||
}
|
match shared_texture.import_texture(&self.wgpu_context.device) {
|
||||||
|
Ok(texture) => {
|
||||||
fn receive_web_message(&self, message: &[u8]) {
|
self.app_event_scheduler.schedule(AppEvent::UiUpdate(texture));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
+10
-155
@@ -1,161 +1,16 @@
|
|||||||
use cef::sys::{CEF_API_VERSION_LAST, cef_resultcode_t};
|
#[cfg(not(target_os = "macos"))]
|
||||||
use cef::{App, BrowserSettings, Client, DictionaryValue, ImplBrowser, ImplBrowserHost, ImplCommandLine, RenderHandler, RequestContext, WindowInfo, browser_host_create_browser_sync, initialize};
|
mod multithreaded;
|
||||||
use cef::{Browser, CefString, Settings, api_hash, args::Args, execute_process};
|
mod singlethreaded;
|
||||||
use thiserror::Error;
|
|
||||||
use winit::event::WindowEvent;
|
|
||||||
|
|
||||||
use crate::cef::dirs::{cef_cache_dir, cef_data_dir};
|
mod builder;
|
||||||
|
pub(crate) use builder::{CefContextBuilder, InitError};
|
||||||
|
|
||||||
use super::input::InputState;
|
pub(crate) trait CefContext {
|
||||||
use super::ipc::{MessageType, SendMessage};
|
fn work(&mut self);
|
||||||
use super::scheme_handler::{FRONTEND_DOMAIN, GRAPHITE_SCHEME};
|
|
||||||
use super::{CefEventHandler, input};
|
|
||||||
|
|
||||||
use super::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderHandlerImpl, RenderProcessAppImpl};
|
fn handle_window_event(&mut self, event: &winit::event::WindowEvent);
|
||||||
|
|
||||||
pub(crate) struct Setup {}
|
fn notify_view_info_changed(&self);
|
||||||
pub(crate) struct Initialized {}
|
|
||||||
pub(crate) trait ContextState {}
|
|
||||||
impl ContextState for Setup {}
|
|
||||||
impl ContextState for Initialized {}
|
|
||||||
|
|
||||||
pub(crate) struct Context<S: ContextState> {
|
fn send_web_message(&self, message: Vec<u8>);
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-7
@@ -1,17 +1,24 @@
|
|||||||
use std::path::PathBuf;
|
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";
|
static CEF_DIR_NAME: &str = "browser";
|
||||||
|
|
||||||
pub(crate) fn cef_data_dir() -> PathBuf {
|
pub(crate) fn delete_instance_dirs() {
|
||||||
let path = graphite_data_dir().join(CEF_DIR_NAME);
|
let cef_dir = app_data_dir().join(CEF_DIR_NAME);
|
||||||
ensure_dir_exists(&path);
|
if let Ok(entries) = std::fs::read_dir(&cef_dir) {
|
||||||
path
|
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 {
|
pub(crate) fn create_instance_dir() -> PathBuf {
|
||||||
let path = cef_data_dir().join("cache");
|
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);
|
ensure_dir_exists(&path);
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|||||||
+153
-253
@@ -1,277 +1,177 @@
|
|||||||
use cef::sys::{cef_event_flags_t, cef_key_event_type_t, cef_mouse_button_type_t};
|
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 cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, MouseEvent};
|
||||||
use winit::dpi::PhysicalPosition;
|
use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
||||||
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
use winit::keyboard::Key;
|
||||||
|
|
||||||
use super::context::{Context, Initialized};
|
|
||||||
|
|
||||||
mod keymap;
|
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 {
|
match event {
|
||||||
WindowEvent::CursorMoved { position, .. } => {
|
WindowEvent::PointerMoved { position, .. } | WindowEvent::PointerEntered { position, .. } => {
|
||||||
if let Some(browser) = &context.browser {
|
if !input_state.cursor_move(position) {
|
||||||
if let Some(host) = browser.host() {
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let Some(host) = browser.host() else { return };
|
||||||
|
host.send_mouse_move_event(Some(&input_state.into()), 0);
|
||||||
}
|
}
|
||||||
WindowEvent::MouseInput { state, button, .. } => {
|
WindowEvent::PointerLeft { position, .. } => {
|
||||||
if let Some(browser) = &context.browser {
|
if let Some(position) = position {
|
||||||
if let Some(host) = browser.host() {
|
let _ = input_state.cursor_move(position);
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: _, .. } => {
|
WindowEvent::MouseWheel { delta, phase: _, device_id: _, .. } => {
|
||||||
if let Some(browser) = &context.browser {
|
let mouse_event = input_state.into();
|
||||||
if let Some(host) = browser.host() {
|
let (mut delta_x, mut delta_y) = match delta {
|
||||||
let mouse_event = (&context.input_state).into();
|
MouseScrollDelta::LineDelta(x, y) => (x * SCROLL_LINE_WIDTH as f32, y * SCROLL_LINE_HEIGHT as f32),
|
||||||
let line_width = 40; //feels about right, TODO: replace with correct value
|
MouseScrollDelta::PixelDelta(physical_position) => (physical_position.x as f32, physical_position.y as f32),
|
||||||
let line_height = 30; //feels about right, TODO: replace with correct value
|
};
|
||||||
let (delta_x, delta_y) = match delta {
|
delta_x *= SCROLL_SPEED_X;
|
||||||
MouseScrollDelta::LineDelta(x, y) => (x * line_width as f32, y * line_height as f32),
|
delta_y *= SCROLL_SPEED_Y;
|
||||||
MouseScrollDelta::PixelDelta(physical_position) => (physical_position.x as f32, physical_position.y as f32),
|
|
||||||
};
|
let Some(host) = browser.host() else { return };
|
||||||
host.send_mouse_wheel_event(Some(&mouse_event), delta_x as i32, delta_y as i32);
|
host.send_mouse_wheel_event(Some(&mouse_event), delta_x as i32, delta_y as i32);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
WindowEvent::ModifiersChanged(modifiers) => {
|
WindowEvent::ModifiersChanged(modifiers) => {
|
||||||
context.input_state.update_modifiers(&modifiers.state());
|
input_state.modifiers_changed(&modifiers.state());
|
||||||
}
|
}
|
||||||
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
|
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
|
||||||
if let Some(browser) = &context.browser {
|
let Some(host) = browser.host() else { return };
|
||||||
if let Some(host) = browser.host() {
|
|
||||||
host.set_focus(1);
|
|
||||||
|
|
||||||
let (named_key, character) = match &event.logical_key {
|
let mut key_event = KeyEvent {
|
||||||
winit::keyboard::Key::Named(named_key) => (
|
type_: match (event.state, &event.logical_key) {
|
||||||
Some(named_key),
|
(ElementState::Pressed, winit::keyboard::Key::Character(_)) => cef_key_event_type_t::KEYEVENT_CHAR,
|
||||||
match named_key {
|
(ElementState::Pressed, _) => cef_key_event_type_t::KEYEVENT_RAWKEYDOWN,
|
||||||
winit::keyboard::NamedKey::Space => Some(' '),
|
(ElementState::Released, _) => cef_key_event_type_t::KEYEVENT_KEYUP,
|
||||||
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));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
.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),
|
WindowEvent::PinchGesture { delta, .. } => {
|
||||||
}
|
if !delta.is_normal() {
|
||||||
None
|
return;
|
||||||
}
|
}
|
||||||
|
let Some(host) = browser.host() else { return };
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
let mouse_event = MouseEvent {
|
||||||
pub(crate) struct MouseState {
|
modifiers: CefModifiers::PINCH_MODIFIERS.into(),
|
||||||
left: bool,
|
..input_state.into()
|
||||||
right: bool,
|
};
|
||||||
middle: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
let delta = (delta * PINCH_ZOOM_SPEED).round() as i32;
|
||||||
pub(crate) struct MousePosition {
|
|
||||||
x: usize,
|
|
||||||
y: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&PhysicalPosition<f64>> for MousePosition {
|
host.send_mouse_wheel_event(Some(&mouse_event), 0, delta);
|
||||||
fn from(position: &PhysicalPosition<f64>) -> Self {
|
|
||||||
Self {
|
|
||||||
x: position.x as usize,
|
|
||||||
y: position.y as usize,
|
|
||||||
}
|
}
|
||||||
}
|
_ => {}
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
pub(crate) struct InputState {
|
|
||||||
modifiers: winit::keyboard::ModifiersState,
|
|
||||||
mouse_position: MousePosition,
|
|
||||||
mouse_state: MouseState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InputState {
|
|
||||||
fn update_modifiers(&mut self, modifiers: &winit::keyboard::ModifiersState) {
|
|
||||||
self.modifiers = *modifiers;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_mouse_position(&mut self, position: &PhysicalPosition<f64>) {
|
|
||||||
self.mouse_position = position.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_mouse_state(&mut self, state: MouseState) {
|
|
||||||
self.mouse_state = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cef_modifiers(&self, location: &winit::keyboard::KeyLocation, is_repeat: bool) -> CefModifiers {
|
|
||||||
CefModifiers::new(self, location, is_repeat)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cef_modifiers_mouse_event(&self) -> CefModifiers {
|
|
||||||
self.cef_modifiers(&winit::keyboard::KeyLocation::Standard, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<InputState> for CefModifiers {
|
|
||||||
fn from(val: InputState) -> Self {
|
|
||||||
CefModifiers::new(&val, &winit::keyboard::KeyLocation::Standard, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&InputState> for MouseEvent {
|
|
||||||
fn from(val: &InputState) -> Self {
|
|
||||||
MouseEvent {
|
|
||||||
x: val.mouse_position.x as i32,
|
|
||||||
y: val.mouse_position.y as i32,
|
|
||||||
modifiers: val.cef_modifiers_mouse_event().raw(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-231
@@ -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 {
|
macro_rules! map_enum {
|
||||||
($target:expr, $enum:ident, $( ($code:expr, $variant:ident), )+ ) => {
|
($target:expr, $enum:ident, $( ($code:expr, $variant:ident), )+ ) => {
|
||||||
match $target {
|
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 {
|
impl ToVKBits for winit::keyboard::NamedKey {
|
||||||
fn to_vk_bits(&self) -> i32 {
|
fn to_vk_bits(&self) -> i32 {
|
||||||
use winit::keyboard::NamedKey;
|
|
||||||
map_enum!(
|
map_enum!(
|
||||||
self,
|
self,
|
||||||
NamedKey,
|
NamedKey,
|
||||||
@@ -39,14 +66,12 @@ impl ToVKBits for winit::keyboard::NamedKey {
|
|||||||
(0x91, ScrollLock),
|
(0x91, ScrollLock),
|
||||||
(0x10, Shift),
|
(0x10, Shift),
|
||||||
(0x5B, Meta),
|
(0x5B, Meta),
|
||||||
(0x5C, Super),
|
|
||||||
(0x0D, Enter),
|
(0x0D, Enter),
|
||||||
(0x09, Tab),
|
(0x09, Tab),
|
||||||
(0x20, Space),
|
|
||||||
(0x28, ArrowDown),
|
|
||||||
(0x25, ArrowLeft),
|
(0x25, ArrowLeft),
|
||||||
(0x27, ArrowRight),
|
|
||||||
(0x26, ArrowUp),
|
(0x26, ArrowUp),
|
||||||
|
(0x27, ArrowRight),
|
||||||
|
(0x28, ArrowDown),
|
||||||
(0x23, End),
|
(0x23, End),
|
||||||
(0x24, Home),
|
(0x24, Home),
|
||||||
(0x22, PageDown),
|
(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 {
|
impl ToVKBits for char {
|
||||||
fn to_vk_bits(&self) -> i32 {
|
fn to_vk_bits(&self) -> i32 {
|
||||||
map!(
|
map!(
|
||||||
@@ -234,215 +269,7 @@ impl ToVKBits for char {
|
|||||||
(0xDE, '"'),
|
(0xDE, '"'),
|
||||||
(0xBF, '/'),
|
(0xBF, '/'),
|
||||||
(0xBF, '?'),
|
(0xBF, '?'),
|
||||||
)
|
(0x20, ' '),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, '?'),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_app;
|
||||||
mod browser_process_client;
|
mod browser_process_client;
|
||||||
mod browser_process_handler;
|
mod browser_process_handler;
|
||||||
mod browser_process_life_span_handler;
|
|
||||||
mod render_handler;
|
|
||||||
mod render_process_app;
|
mod render_process_app;
|
||||||
mod render_process_handler;
|
mod render_process_handler;
|
||||||
mod render_process_v8_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_app::BrowserProcessAppImpl;
|
||||||
pub(super) use browser_process_client::BrowserProcessClientImpl;
|
pub(super) use browser_process_client::BrowserProcessClientImpl;
|
||||||
pub(super) use render_handler::RenderHandlerImpl;
|
|
||||||
pub(super) use render_process_app::RenderProcessAppImpl;
|
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 std::env;
|
||||||
|
|
||||||
use cef::rc::{Rc, RcImpl};
|
use cef::rc::{Rc, RcImpl};
|
||||||
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
|
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
|
||||||
use cef::{BrowserProcessHandler, CefString, ImplApp, ImplCommandLine, SchemeRegistrar, WrapApp};
|
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::browser_process_handler::BrowserProcessHandlerImpl;
|
||||||
|
use super::scheme_handler_factory::SchemeHandlerFactoryImpl;
|
||||||
|
use crate::cef::CefEventHandler;
|
||||||
|
|
||||||
pub(crate) struct BrowserProcessAppImpl<H: CefEventHandler> {
|
pub(crate) struct BrowserProcessAppImpl<H: CefEventHandler> {
|
||||||
object: *mut RcImpl<_cef_app_t, Self>,
|
object: *mut RcImpl<_cef_app_t, Self>,
|
||||||
event_handler: H,
|
event_handler: H,
|
||||||
}
|
}
|
||||||
impl<H: CefEventHandler + Clone> BrowserProcessAppImpl<H> {
|
impl<H: CefEventHandler> BrowserProcessAppImpl<H> {
|
||||||
pub(crate) fn new(event_handler: H) -> Self {
|
pub(crate) fn new(event_handler: H) -> Self {
|
||||||
Self {
|
Self {
|
||||||
object: std::ptr::null_mut(),
|
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> {
|
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>) {
|
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>) {
|
fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) {
|
||||||
if let Some(cmd) = command_line {
|
if let Some(cmd) = command_line {
|
||||||
// Disable GPU acceleration, because it is not supported for Offscreen Rendering and can cause crashes.
|
cmd.append_switch_with_value(Some(&CefString::from("renderer-process-limit")), Some(&CefString::from("1")));
|
||||||
cmd.append_switch(Some(&CefString::from("disable-gpu")));
|
cmd.append_switch_with_value(Some(&CefString::from("disk-cache-size")), Some(&CefString::from("0")));
|
||||||
cmd.append_switch(Some(&CefString::from("disable-gpu-compositing")));
|
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
|
// 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")
|
let use_wayland = env::var("WAYLAND_DISPLAY")
|
||||||
.ok()
|
.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")));
|
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 {
|
fn clone(&self) -> Self {
|
||||||
unsafe {
|
unsafe {
|
||||||
let rc_impl = &mut *self.object;
|
let rc_impl = &mut *self.object;
|
||||||
@@ -67,7 +106,7 @@ impl<H: CefEventHandler + Clone> Clone for BrowserProcessAppImpl<H> {
|
|||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
object: self.object,
|
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>) {
|
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
|
||||||
self.object = object;
|
self.object = object;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,31 @@
|
|||||||
use cef::rc::{Rc, RcImpl};
|
use cef::rc::{Rc, RcImpl};
|
||||||
use cef::sys::{_cef_client_t, cef_base_ref_counted_t};
|
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::CefEventHandler;
|
||||||
use crate::cef::ipc::{MessageType, UnpackMessage, UnpackedMessage};
|
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> {
|
pub(crate) struct BrowserProcessClientImpl<H: CefEventHandler> {
|
||||||
object: *mut RcImpl<_cef_client_t, Self>,
|
object: *mut RcImpl<_cef_client_t, Self>,
|
||||||
render_handler: RenderHandler,
|
|
||||||
event_handler: H,
|
event_handler: H,
|
||||||
|
load_handler: LoadHandler,
|
||||||
|
render_handler: RenderHandler,
|
||||||
|
display_handler: DisplayHandler,
|
||||||
}
|
}
|
||||||
impl<H: CefEventHandler> BrowserProcessClientImpl<H> {
|
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 {
|
Self {
|
||||||
object: std::ptr::null_mut(),
|
object: std::ptr::null_mut(),
|
||||||
render_handler,
|
event_handler: event_handler.duplicate(),
|
||||||
event_handler,
|
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>,
|
_frame: Option<&mut cef::Frame>,
|
||||||
_source_process: cef::ProcessId,
|
_source_process: cef::ProcessId,
|
||||||
message: Option<&mut cef::ProcessMessage>,
|
message: Option<&mut cef::ProcessMessage>,
|
||||||
) -> ::std::os::raw::c_int {
|
) -> std::ffi::c_int {
|
||||||
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
|
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
|
||||||
match unpacked_message {
|
match unpacked_message {
|
||||||
|
Some(UnpackedMessage {
|
||||||
|
message_type: MessageType::Initialized,
|
||||||
|
data: _,
|
||||||
|
}) => self.event_handler.initialized_web_communication(),
|
||||||
Some(UnpackedMessage {
|
Some(UnpackedMessage {
|
||||||
message_type: MessageType::SendToNative,
|
message_type: MessageType::SendToNative,
|
||||||
data,
|
data,
|
||||||
@@ -45,12 +57,24 @@ impl<H: CefEventHandler> ImplClient for BrowserProcessClientImpl<H> {
|
|||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_handler(&self) -> Option<cef::LoadHandler> {
|
||||||
|
Some(self.load_handler.clone())
|
||||||
|
}
|
||||||
|
|
||||||
fn render_handler(&self) -> Option<RenderHandler> {
|
fn render_handler(&self) -> Option<RenderHandler> {
|
||||||
Some(self.render_handler.clone())
|
Some(self.render_handler.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn life_span_handler(&self) -> Option<cef::LifeSpanHandler> {
|
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 {
|
fn get_raw(&self) -> *mut _cef_client_t {
|
||||||
@@ -66,8 +90,10 @@ impl<H: CefEventHandler> Clone for BrowserProcessClientImpl<H> {
|
|||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
object: self.object,
|
object: self.object,
|
||||||
|
event_handler: self.event_handler.duplicate(),
|
||||||
|
load_handler: self.load_handler.clone(),
|
||||||
render_handler: self.render_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::rc::{Rc, RcImpl};
|
||||||
use cef::sys::{_cef_browser_process_handler_t, cef_base_ref_counted_t, cef_browser_process_handler_t};
|
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::CefEventHandler;
|
||||||
use crate::cef::scheme_handler::{GRAPHITE_SCHEME, GraphiteSchemeHandlerFactory};
|
|
||||||
|
|
||||||
pub(crate) struct BrowserProcessHandlerImpl<H: CefEventHandler> {
|
pub(crate) struct BrowserProcessHandlerImpl<H: CefEventHandler> {
|
||||||
object: *mut RcImpl<cef_browser_process_handler_t, Self>,
|
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> {
|
impl<H: CefEventHandler> 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())));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_schedule_message_pump_work(&self, delay_ms: i64) {
|
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));
|
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
|
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 {
|
fn clone(&self) -> Self {
|
||||||
unsafe {
|
unsafe {
|
||||||
let rc_impl = &mut *self.object;
|
let rc_impl = &mut *self.object;
|
||||||
@@ -46,7 +41,7 @@ impl<H: CefEventHandler + Clone> Clone for BrowserProcessHandlerImpl<H> {
|
|||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
object: self.object,
|
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>) {
|
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_browser_process_handler_t, Self>) {
|
||||||
self.object = object;
|
self.object = object;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
use cef::rc::{Rc, RcImpl};
|
||||||
|
use cef::sys::{_cef_context_menu_handler_t, cef_base_ref_counted_t};
|
||||||
|
use cef::{ImplContextMenuHandler, WrapContextMenuHandler};
|
||||||
|
|
||||||
|
pub(crate) struct ContextMenuHandlerImpl {
|
||||||
|
object: *mut RcImpl<_cef_context_menu_handler_t, Self>,
|
||||||
|
}
|
||||||
|
impl ContextMenuHandlerImpl {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self { object: std::ptr::null_mut() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImplContextMenuHandler for ContextMenuHandlerImpl {
|
||||||
|
fn run_context_menu(
|
||||||
|
&self,
|
||||||
|
_browser: Option<&mut cef::Browser>,
|
||||||
|
_frame: Option<&mut cef::Frame>,
|
||||||
|
_params: Option<&mut cef::ContextMenuParams>,
|
||||||
|
_model: Option<&mut cef::MenuModel>,
|
||||||
|
_callback: Option<&mut cef::RunContextMenuCallback>,
|
||||||
|
) -> std::ffi::c_int {
|
||||||
|
// Prevent context menu
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_quick_menu(
|
||||||
|
&self,
|
||||||
|
_browser: Option<&mut cef::Browser>,
|
||||||
|
_frame: Option<&mut cef::Frame>,
|
||||||
|
_location: Option<&cef::Point>,
|
||||||
|
_size: Option<&cef::Size>,
|
||||||
|
_edit_state_flags: cef::QuickMenuEditStateFlags,
|
||||||
|
_callback: Option<&mut cef::RunQuickMenuCallback>,
|
||||||
|
) -> std::ffi::c_int {
|
||||||
|
// Prevent quick menu
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_raw(&self) -> *mut _cef_context_menu_handler_t {
|
||||||
|
self.object.cast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for ContextMenuHandlerImpl {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
unsafe {
|
||||||
|
let rc_impl = &mut *self.object;
|
||||||
|
rc_impl.interface.add_ref();
|
||||||
|
}
|
||||||
|
Self { object: self.object }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Rc for ContextMenuHandlerImpl {
|
||||||
|
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||||
|
unsafe {
|
||||||
|
let base = &*self.object;
|
||||||
|
std::mem::transmute(&base.cef_object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl WrapContextMenuHandler for ContextMenuHandlerImpl {
|
||||||
|
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_context_menu_handler_t, Self>) {
|
||||||
|
self.object = object;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-11
@@ -2,32 +2,32 @@ use cef::rc::{Rc, RcImpl};
|
|||||||
use cef::sys::{_cef_life_span_handler_t, cef_base_ref_counted_t};
|
use cef::sys::{_cef_life_span_handler_t, cef_base_ref_counted_t};
|
||||||
use cef::{ImplLifeSpanHandler, WrapLifeSpanHandler};
|
use cef::{ImplLifeSpanHandler, WrapLifeSpanHandler};
|
||||||
|
|
||||||
pub(crate) struct BrowserProcessLifeSpanHandlerImpl {
|
pub(crate) struct LifeSpanHandlerImpl {
|
||||||
object: *mut RcImpl<_cef_life_span_handler_t, Self>,
|
object: *mut RcImpl<_cef_life_span_handler_t, Self>,
|
||||||
}
|
}
|
||||||
impl BrowserProcessLifeSpanHandlerImpl {
|
impl LifeSpanHandlerImpl {
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
Self { object: std::ptr::null_mut() }
|
Self { object: std::ptr::null_mut() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImplLifeSpanHandler for BrowserProcessLifeSpanHandlerImpl {
|
impl ImplLifeSpanHandler for LifeSpanHandlerImpl {
|
||||||
fn on_before_popup(
|
fn on_before_popup(
|
||||||
&self,
|
&self,
|
||||||
_browser: Option<&mut cef::Browser>,
|
_browser: Option<&mut cef::Browser>,
|
||||||
_frame: Option<&mut cef::Frame>,
|
_frame: Option<&mut cef::Frame>,
|
||||||
_popup_id: ::std::os::raw::c_int,
|
_popup_id: std::ffi::c_int,
|
||||||
target_url: Option<&cef::CefString>,
|
target_url: Option<&cef::CefString>,
|
||||||
_target_frame_name: Option<&cef::CefString>,
|
_target_frame_name: Option<&cef::CefString>,
|
||||||
_target_disposition: cef::WindowOpenDisposition,
|
_target_disposition: cef::WindowOpenDisposition,
|
||||||
_user_gesture: ::std::os::raw::c_int,
|
_user_gesture: std::ffi::c_int,
|
||||||
_popup_features: Option<&cef::PopupFeatures>,
|
_popup_features: Option<&cef::PopupFeatures>,
|
||||||
_window_info: Option<&mut cef::WindowInfo>,
|
_window_info: Option<&mut cef::WindowInfo>,
|
||||||
_client: Option<&mut Option<impl cef::ImplClient>>,
|
_client: Option<&mut Option<cef::Client>>,
|
||||||
_settings: Option<&mut cef::BrowserSettings>,
|
_settings: Option<&mut cef::BrowserSettings>,
|
||||||
_extra_info: Option<&mut Option<cef::DictionaryValue>>,
|
_extra_info: Option<&mut Option<cef::DictionaryValue>>,
|
||||||
_no_javascript_access: Option<&mut ::std::os::raw::c_int>,
|
_no_javascript_access: Option<&mut std::ffi::c_int>,
|
||||||
) -> ::std::os::raw::c_int {
|
) -> std::ffi::c_int {
|
||||||
let target = target_url.map(|url| url.to_string()).unwrap_or("unknown".to_string());
|
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);
|
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 {
|
fn clone(&self) -> Self {
|
||||||
unsafe {
|
unsafe {
|
||||||
let rc_impl = &mut *self.object;
|
let rc_impl = &mut *self.object;
|
||||||
@@ -49,7 +49,7 @@ impl Clone for BrowserProcessLifeSpanHandlerImpl {
|
|||||||
Self { object: self.object }
|
Self { object: self.object }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Rc for BrowserProcessLifeSpanHandlerImpl {
|
impl Rc for LifeSpanHandlerImpl {
|
||||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||||
unsafe {
|
unsafe {
|
||||||
let base = &*self.object;
|
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>) {
|
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_life_span_handler_t, Self>) {
|
||||||
self.object = object;
|
self.object = object;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
use cef::rc::{Rc, RcImpl};
|
||||||
|
use cef::sys::{_cef_load_handler_t, cef_base_ref_counted_t, cef_load_handler_t};
|
||||||
|
use cef::{ImplBrowser, ImplBrowserHost, ImplLoadHandler, WrapLoadHandler};
|
||||||
|
|
||||||
|
use crate::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> {
|
impl<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
|
||||||
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
|
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
|
||||||
if let Some(rect) = rect {
|
if let Some(rect) = rect {
|
||||||
let view = self.event_handler.window_size();
|
let view_info = self.event_handler.view_info();
|
||||||
*rect = Rect {
|
*rect = Rect {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: view.width as i32,
|
width: view_info.width() as i32,
|
||||||
height: view.height as i32,
|
height: view_info.height() as i32,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_paint(
|
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) {
|
||||||
&self,
|
|
||||||
_browser: Option<&mut Browser>,
|
|
||||||
_type_: PaintElementType,
|
|
||||||
_dirty_rect_count: usize,
|
|
||||||
_dirty_rects: Option<&Rect>,
|
|
||||||
buffer: *const u8,
|
|
||||||
width: ::std::os::raw::c_int,
|
|
||||||
height: ::std::os::raw::c_int,
|
|
||||||
) {
|
|
||||||
let buffer_size = (width * height * 4) as usize;
|
let buffer_size = (width * height * 4) as usize;
|
||||||
let buffer_slice = unsafe { std::slice::from_raw_parts(buffer, buffer_size) };
|
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");
|
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)
|
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 {
|
fn get_raw(&self) -> *mut _cef_render_handler_t {
|
||||||
self.object.cast()
|
self.object.cast()
|
||||||
}
|
}
|
||||||
@@ -60,7 +69,7 @@ impl<H: CefEventHandler> Clone for RenderHandlerImpl<H> {
|
|||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
object: self.object,
|
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 cef::{App, ImplApp, RenderProcessHandler, SchemeRegistrar, WrapApp};
|
||||||
|
|
||||||
use super::render_process_handler::RenderProcessHandlerImpl;
|
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>,
|
object: *mut RcImpl<_cef_app_t, Self>,
|
||||||
render_process_handler: RenderProcessHandler,
|
render_process_handler: RenderProcessHandler,
|
||||||
}
|
}
|
||||||
impl RenderProcessAppImpl {
|
impl<H: CefEventHandler> RenderProcessAppImpl<H> {
|
||||||
pub(crate) fn app() -> App {
|
pub(crate) fn app() -> App {
|
||||||
App::new(Self {
|
App::new(Self {
|
||||||
object: std::ptr::null_mut(),
|
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>) {
|
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> {
|
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 {
|
fn clone(&self) -> Self {
|
||||||
unsafe {
|
unsafe {
|
||||||
let rc_impl = &mut *self.object;
|
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 {
|
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||||
unsafe {
|
unsafe {
|
||||||
let base = &*self.object;
|
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>) {
|
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
|
||||||
self.object = object;
|
self.object = object;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use cef::{CefString, ImplFrame, ImplRenderProcessHandler, ImplV8Context, ImplV8V
|
|||||||
|
|
||||||
use crate::cef::ipc::{MessageType, UnpackMessage, UnpackedMessage};
|
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 {
|
pub(crate) struct RenderProcessHandlerImpl {
|
||||||
object: *mut RcImpl<cef_render_process_handler_t, Self>,
|
object: *mut RcImpl<cef_render_process_handler_t, Self>,
|
||||||
@@ -22,7 +22,7 @@ impl ImplRenderProcessHandler for RenderProcessHandlerImpl {
|
|||||||
frame: Option<&mut cef::Frame>,
|
frame: Option<&mut cef::Frame>,
|
||||||
_source_process: cef::ProcessId,
|
_source_process: cef::ProcessId,
|
||||||
message: Option<&mut cef::ProcessMessage>,
|
message: Option<&mut cef::ProcessMessage>,
|
||||||
) -> ::std::os::raw::c_int {
|
) -> std::ffi::c_int {
|
||||||
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
|
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
|
||||||
match unpacked_message {
|
match unpacked_message {
|
||||||
Some(UnpackedMessage {
|
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>) {
|
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 {
|
let Some(context) = context else {
|
||||||
tracing::error!("V8 context is not available");
|
tracing::error!("V8 context is not available");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut v8_handler = V8Handler::new(BrowserProcessV8HandlerImpl::new());
|
let initialized_function_name = "initializeNativeCommunication";
|
||||||
let Some(mut function) = v8_value_create_function(Some(&CefString::from(function_name)), Some(&mut v8_handler)) else {
|
let send_function_name = "sendNativeMessage";
|
||||||
tracing::error!("Failed to create V8 function {function_name}");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(global) = context.global() else {
|
register_js_function(context, initialized_function_name);
|
||||||
tracing::error!("Global object is not available in V8 context");
|
register_js_function(context, send_function_name);
|
||||||
return;
|
|
||||||
};
|
|
||||||
global.set_value_bykey(Some(&CefString::from(function_name)), Some(&mut function), V8Propertyattribute::default());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_raw(&self) -> *mut _cef_render_process_handler_t {
|
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};
|
use crate::cef::ipc::{MessageType, SendMessage};
|
||||||
|
|
||||||
pub struct BrowserProcessV8HandlerImpl {
|
pub struct RenderProcessV8HandlerImpl {
|
||||||
object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>,
|
object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>,
|
||||||
}
|
}
|
||||||
|
impl RenderProcessV8HandlerImpl {
|
||||||
impl BrowserProcessV8HandlerImpl {
|
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
Self { object: std::ptr::null_mut() }
|
Self { object: std::ptr::null_mut() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImplV8Handler for BrowserProcessV8HandlerImpl {
|
impl ImplV8Handler for RenderProcessV8HandlerImpl {
|
||||||
fn execute(
|
fn execute(
|
||||||
&self,
|
&self,
|
||||||
name: Option<&cef::CefString>,
|
name: Option<&cef::CefString>,
|
||||||
@@ -20,9 +19,12 @@ impl ImplV8Handler for BrowserProcessV8HandlerImpl {
|
|||||||
arguments: Option<&[Option<V8Value>]>,
|
arguments: Option<&[Option<V8Value>]>,
|
||||||
_retval: Option<&mut Option<V8Value>>,
|
_retval: Option<&mut Option<V8Value>>,
|
||||||
_exception: Option<&mut cef::CefString>,
|
_exception: Option<&mut cef::CefString>,
|
||||||
) -> ::std::os::raw::c_int {
|
) -> std::ffi::c_int {
|
||||||
if let Some(name) = name {
|
match name.map(|s| s.to_string()).unwrap_or_default().as_str() {
|
||||||
if name.to_string() == "sendNativeMessage" {
|
"initializeNativeCommunication" => {
|
||||||
|
v8_context_get_current_context().send_message(MessageType::Initialized, vec![0u8].as_slice());
|
||||||
|
}
|
||||||
|
"sendNativeMessage" => {
|
||||||
let Some(args) = arguments else {
|
let Some(args) = arguments else {
|
||||||
tracing::error!("No arguments provided to sendNativeMessage");
|
tracing::error!("No arguments provided to sendNativeMessage");
|
||||||
return 0;
|
return 0;
|
||||||
@@ -48,6 +50,9 @@ impl ImplV8Handler for BrowserProcessV8HandlerImpl {
|
|||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
name => {
|
||||||
|
tracing::error!("Unknown V8 function called: {}", name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
@@ -57,7 +62,7 @@ impl ImplV8Handler for BrowserProcessV8HandlerImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for BrowserProcessV8HandlerImpl {
|
impl Clone for RenderProcessV8HandlerImpl {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
unsafe {
|
unsafe {
|
||||||
let rc_impl = &mut *self.object;
|
let rc_impl = &mut *self.object;
|
||||||
@@ -66,8 +71,7 @@ impl Clone for BrowserProcessV8HandlerImpl {
|
|||||||
Self { object: self.object }
|
Self { object: self.object }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl Rc for RenderProcessV8HandlerImpl {
|
||||||
impl Rc for BrowserProcessV8HandlerImpl {
|
|
||||||
fn as_base(&self) -> &cef::sys::cef_base_ref_counted_t {
|
fn as_base(&self) -> &cef::sys::cef_base_ref_counted_t {
|
||||||
unsafe {
|
unsafe {
|
||||||
let base = &*self.object;
|
let base = &*self.object;
|
||||||
@@ -75,8 +79,7 @@ impl Rc for BrowserProcessV8HandlerImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl WrapV8Handler for RenderProcessV8HandlerImpl {
|
||||||
impl WrapV8Handler for BrowserProcessV8HandlerImpl {
|
|
||||||
fn wrap_rc(&mut self, object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>) {
|
fn wrap_rc(&mut self, object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>) {
|
||||||
self.object = object;
|
self.object = object;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-18
@@ -1,14 +1,17 @@
|
|||||||
use cef::{CefString, Frame, ImplBinaryValue, ImplBrowser, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Context, ProcessId, V8Context, sys::cef_process_id_t};
|
use cef::{CefString, Frame, ImplBinaryValue, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Context, ProcessId, V8Context, sys::cef_process_id_t};
|
||||||
|
|
||||||
use super::{Context, Initialized};
|
|
||||||
|
|
||||||
pub(crate) enum MessageType {
|
pub(crate) enum MessageType {
|
||||||
|
Initialized,
|
||||||
SendToJS,
|
SendToJS,
|
||||||
SendToNative,
|
SendToNative,
|
||||||
}
|
}
|
||||||
impl From<MessageType> for MessageInfo {
|
impl From<MessageType> for MessageInfo {
|
||||||
fn from(val: MessageType) -> Self {
|
fn from(val: MessageType) -> Self {
|
||||||
match val {
|
match val {
|
||||||
|
MessageType::Initialized => MessageInfo {
|
||||||
|
name: "initialized".to_string(),
|
||||||
|
target: cef_process_id_t::PID_BROWSER.into(),
|
||||||
|
},
|
||||||
MessageType::SendToJS => MessageInfo {
|
MessageType::SendToJS => MessageInfo {
|
||||||
name: "send_to_js".to_string(),
|
name: "send_to_js".to_string(),
|
||||||
target: cef_process_id_t::PID_RENDERER.into(),
|
target: cef_process_id_t::PID_RENDERER.into(),
|
||||||
@@ -24,6 +27,7 @@ impl TryFrom<String> for MessageType {
|
|||||||
type Error = ();
|
type Error = ();
|
||||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
match value.as_str() {
|
match value.as_str() {
|
||||||
|
"initialized" => Ok(MessageType::Initialized),
|
||||||
"send_to_js" => Ok(MessageType::SendToJS),
|
"send_to_js" => Ok(MessageType::SendToJS),
|
||||||
"send_to_native" => Ok(MessageType::SendToNative),
|
"send_to_native" => Ok(MessageType::SendToNative),
|
||||||
_ => Err(()),
|
_ => Err(()),
|
||||||
@@ -39,21 +43,6 @@ pub(crate) struct MessageInfo {
|
|||||||
pub(crate) trait SendMessage {
|
pub(crate) trait SendMessage {
|
||||||
fn send_message(&self, message_type: MessageType, message: &[u8]);
|
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> {
|
impl SendMessage for Option<V8Context> {
|
||||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||||
let Some(context) = self else {
|
let Some(context) = self else {
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
+13
-3
@@ -1,3 +1,13 @@
|
|||||||
pub(crate) static APP_NAME: &str = "Graphite";
|
pub(crate) const APP_NAME: &str = "Graphite";
|
||||||
pub(crate) static APP_ID: &str = "rs.graphite.GraphiteEditor";
|
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||||
pub(crate) static APP_DIRECTORY_NAME: &str = "graphite-editor";
|
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())
|
|
||||||
}
|
|
||||||
+8
-2
@@ -1,7 +1,7 @@
|
|||||||
use std::fs::create_dir_all;
|
use std::fs::create_dir_all;
|
||||||
use std::path::PathBuf;
|
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) {
|
pub(crate) fn ensure_dir_exists(path: &PathBuf) {
|
||||||
if !path.exists() {
|
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);
|
let path = dirs::data_dir().expect("Failed to get data directory").join(APP_DIRECTORY_NAME);
|
||||||
ensure_dir_exists(&path);
|
ensure_dir_exists(&path);
|
||||||
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
-80
@@ -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() {
|
fn main() {
|
||||||
tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init();
|
graphite_desktop::start();
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
mod frame_buffer_ref;
|
||||||
pub(crate) use frame_buffer_ref::FrameBufferRef;
|
pub(crate) use frame_buffer_ref::FrameBufferRef;
|
||||||
|
|
||||||
mod graphics_state;
|
mod state;
|
||||||
pub(crate) use graphics_state::{GraphicsState, WgpuContext};
|
pub(crate) use state::{RenderError, RenderState};
|
||||||
|
|||||||
@@ -8,15 +8,9 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
|||||||
var out: VertexOutput;
|
var out: VertexOutput;
|
||||||
|
|
||||||
let pos = array(
|
let pos = array(
|
||||||
// 1st triangle
|
vec2f( -1.0, -1.0),
|
||||||
vec2f( -1.0, -1.0), // center
|
vec2f( 3.0, -1.0),
|
||||||
vec2f( 1.0, -1.0), // right, center
|
vec2f( -1.0, 3.0),
|
||||||
vec2f( -1.0, 1.0), // center, top
|
|
||||||
|
|
||||||
// 2nd triangle
|
|
||||||
vec2f( -1.0, 1.0), // center, top
|
|
||||||
vec2f( 1.0, -1.0), // right, center
|
|
||||||
vec2f( 1.0, 1.0), // right, top
|
|
||||||
);
|
);
|
||||||
let xy = pos[vertex_index];
|
let xy = pos[vertex_index];
|
||||||
out.clip_position = vec4f(xy , 0.0, 1.0);
|
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 {
|
struct Constants {
|
||||||
viewport_scale: vec2<f32>,
|
viewport_scale: vec2<f32>,
|
||||||
viewport_offset: vec2<f32>,
|
viewport_offset: vec2<f32>,
|
||||||
|
ui_scale: vec2<f32>,
|
||||||
|
background_color: vec4<f32>,
|
||||||
};
|
};
|
||||||
|
|
||||||
var<push_constant> constants: Constants;
|
var<push_constant> constants: Constants;
|
||||||
@@ -44,34 +40,48 @@ var s_diffuse: sampler;
|
|||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
let ui = textureSample(t_ui, s_diffuse, in.tex_coords);
|
let ui_coordinate = in.tex_coords * constants.ui_scale;
|
||||||
if (ui.a >= 0.999) {
|
if (ui_coordinate.x < 0.0 || ui_coordinate.x > 1.0 ||
|
||||||
return ui;
|
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;
|
let viewport_coordinate = (in.tex_coords - constants.viewport_offset) * constants.viewport_scale;
|
||||||
|
if (viewport_coordinate.x < 0.0 || viewport_coordinate.x > 1.0 ||
|
||||||
// Vello renders its values to an `RgbaUnorm` texture, but if we try to use this in the main rendering pipeline
|
viewport_coordinate.y < 0.0 || viewport_coordinate.y > 1.0) {
|
||||||
// which renders to an `Srgb` surface, gamma mapping is applied twice. This converts back to linear to compensate.
|
return srgb_to_linear(constants.background_color);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let composite = blend(overlay, viewport);
|
let overlay_srgb = textureSample(t_overlays, s_diffuse, viewport_coordinate);
|
||||||
return blend(ui, composite);
|
var viewport_srgb = textureSample(t_viewport, s_diffuse, viewport_coordinate);
|
||||||
}
|
|
||||||
|
|
||||||
fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
|
if (viewport_srgb.a < 0.001) {
|
||||||
return select(
|
viewport_srgb = constants.background_color;
|
||||||
pow((srgb + 0.055) / 1.055, vec3<f32>(2.4)),
|
}
|
||||||
srgb / 12.92,
|
|
||||||
srgb <= vec3<f32>(0.04045)
|
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> {
|
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);
|
let rgb = fg.rgb * fg.a + bg.rgb * bg.a * (1.0 - fg.a);
|
||||||
return vec4<f32>(rgb, 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 crate::window::Window;
|
||||||
use std::sync::Arc;
|
|
||||||
use wgpu_executor::WgpuExecutor;
|
|
||||||
use winit::window::Window;
|
|
||||||
|
|
||||||
pub(crate) use wgpu_executor::Context as WgpuContext;
|
use crate::wrapper::{Color, WgpuContext, WgpuExecutor};
|
||||||
|
|
||||||
#[derive(derivative::Derivative)]
|
#[derive(derivative::Derivative)]
|
||||||
#[derivative(Debug)]
|
#[derivative(Debug)]
|
||||||
pub(crate) struct GraphicsState {
|
pub(crate) struct RenderState {
|
||||||
surface: wgpu::Surface<'static>,
|
surface: wgpu::Surface<'static>,
|
||||||
context: WgpuContext,
|
context: WgpuContext,
|
||||||
executor: WgpuExecutor,
|
executor: WgpuExecutor,
|
||||||
@@ -15,6 +12,8 @@ pub(crate) struct GraphicsState {
|
|||||||
render_pipeline: wgpu::RenderPipeline,
|
render_pipeline: wgpu::RenderPipeline,
|
||||||
transparent_texture: wgpu::Texture,
|
transparent_texture: wgpu::Texture,
|
||||||
sampler: wgpu::Sampler,
|
sampler: wgpu::Sampler,
|
||||||
|
desired_width: u32,
|
||||||
|
desired_height: u32,
|
||||||
viewport_scale: [f32; 2],
|
viewport_scale: [f32; 2],
|
||||||
viewport_offset: [f32; 2],
|
viewport_offset: [f32; 2],
|
||||||
viewport_texture: Option<wgpu::Texture>,
|
viewport_texture: Option<wgpu::Texture>,
|
||||||
@@ -25,11 +24,10 @@ pub(crate) struct GraphicsState {
|
|||||||
overlays_scene: Option<vello::Scene>,
|
overlays_scene: Option<vello::Scene>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GraphicsState {
|
impl RenderState {
|
||||||
pub(crate) fn new(window: Arc<Window>, context: WgpuContext) -> Self {
|
pub(crate) fn new(window: &Window, context: WgpuContext) -> Self {
|
||||||
let size = window.inner_size();
|
let size = window.surface_size();
|
||||||
|
let surface = window.create_surface(context.instance.clone());
|
||||||
let surface = context.instance.create_surface(window).unwrap();
|
|
||||||
|
|
||||||
let surface_caps = surface.get_capabilities(&context.adapter);
|
let surface_caps = surface.get_capabilities(&context.adapter);
|
||||||
let surface_format = surface_caps.formats.iter().find(|f| f.is_srgb()).copied().unwrap_or(surface_caps.formats[0]);
|
let 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,
|
format: surface_format,
|
||||||
width: size.width,
|
width: size.width,
|
||||||
height: size.height,
|
height: size.height,
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
present_mode: surface_caps.present_modes[0],
|
present_mode: surface_caps.present_modes[0],
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
present_mode: wgpu::PresentMode::Immediate,
|
||||||
alpha_mode: surface_caps.alpha_modes[0],
|
alpha_mode: surface_caps.alpha_modes[0],
|
||||||
view_formats: vec![],
|
view_formats: vec![],
|
||||||
desired_maximum_frame_latency: 2,
|
desired_maximum_frame_latency: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
surface.configure(&context.device, &config);
|
surface.configure(&context.device, &config);
|
||||||
@@ -175,6 +176,8 @@ impl GraphicsState {
|
|||||||
render_pipeline,
|
render_pipeline,
|
||||||
transparent_texture,
|
transparent_texture,
|
||||||
sampler,
|
sampler,
|
||||||
|
desired_width: size.width,
|
||||||
|
desired_height: size.height,
|
||||||
viewport_scale: [1.0, 1.0],
|
viewport_scale: [1.0, 1.0],
|
||||||
viewport_offset: [0.0, 0.0],
|
viewport_offset: [0.0, 0.0],
|
||||||
viewport_texture: None,
|
viewport_texture: None,
|
||||||
@@ -186,6 +189,13 @@ impl GraphicsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resize(&mut self, width: u32, height: u32) {
|
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) {
|
if width > 0 && height > 0 && (self.config.width != width || self.config.height != height) {
|
||||||
self.config.width = width;
|
self.config.width = width;
|
||||||
self.config.height = height;
|
self.config.height = height;
|
||||||
@@ -234,26 +244,36 @@ impl GraphicsState {
|
|||||||
self.bind_overlays_texture(texture);
|
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() {
|
if let Some(scene) = self.overlays_scene.take() {
|
||||||
self.render_overlays(scene);
|
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 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 encoder = self.context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder") });
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
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 {
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
view: &view,
|
view: &view,
|
||||||
resolve_target: None,
|
resolve_target: None,
|
||||||
ops: wgpu::Operations {
|
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,
|
store: wgpu::StoreOp::Store,
|
||||||
},
|
},
|
||||||
|
depth_slice: None,
|
||||||
})],
|
})],
|
||||||
depth_stencil_attachment: None,
|
depth_stencil_attachment: None,
|
||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
@@ -267,18 +287,26 @@ impl GraphicsState {
|
|||||||
bytemuck::bytes_of(&Constants {
|
bytemuck::bytes_of(&Constants {
|
||||||
viewport_scale: self.viewport_scale,
|
viewport_scale: self.viewport_scale,
|
||||||
viewport_offset: self.viewport_offset,
|
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 {
|
if let Some(bind_group) = &self.bind_group {
|
||||||
render_pass.set_bind_group(0, 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 {
|
} else {
|
||||||
tracing::warn!("No bind group available - showing clear color only");
|
tracing::warn!("No bind group available - showing clear color only");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.context.queue.submit(std::iter::once(encoder.finish()));
|
self.context.queue.submit(std::iter::once(encoder.finish()));
|
||||||
|
window.pre_present_notify();
|
||||||
output.present();
|
output.present();
|
||||||
|
|
||||||
|
if ui_scale.is_some() {
|
||||||
|
return Err(RenderError::OutdatedUITextureError);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,9 +342,17 @@ impl GraphicsState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) enum RenderError {
|
||||||
|
OutdatedUITextureError,
|
||||||
|
SurfaceError(wgpu::SurfaceError),
|
||||||
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
struct Constants {
|
struct Constants {
|
||||||
viewport_scale: [f32; 2],
|
viewport_scale: [f32; 2],
|
||||||
viewport_offset: [f32; 2],
|
viewport_offset: [f32; 2],
|
||||||
|
ui_scale: [f32; 2],
|
||||||
|
_pad: [f32; 2],
|
||||||
|
background_color: [f32; 4],
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
Reference in New Issue
Block a user