mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 23:08:05 +08:00
Merge remote-tracking branch 'origin/master' into circular-repeat-gizmos
This commit is contained in:
2
.branding
Normal file
2
.branding
Normal file
@@ -0,0 +1,2 @@
|
||||
https://github.com/Keavon/graphite-branded-assets/archive/f44aa2f362ae4fed8d634878b817a1d3948a7dcb.tar.gz
|
||||
dffe2b483e491979ef57c320d61446ada5400ef73ff26582976631d9c36efefc
|
||||
@@ -10,3 +10,6 @@ rustflags = [
|
||||
"link-arg=--max-memory=4294967296",
|
||||
"--cfg=web_sys_unstable_apis",
|
||||
]
|
||||
|
||||
[env]
|
||||
CARGO_WORKSPACE_DIR = { value = "", relative = true }
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
// Helpful
|
||||
"mhutchie.git-graph",
|
||||
"waderyan.gitblame",
|
||||
"qezhu.gitlink",
|
||||
"wmaurer.change-case"
|
||||
]
|
||||
|
||||
14
.github/pull_request_template.md
vendored
14
.github/pull_request_template.md
vendored
@@ -1,3 +1,13 @@
|
||||
<!-- Please reference any relevant issue number below, optionally with a "Closes"/"Resolves"/"Fixes" prefix -->
|
||||
<!--
|
||||
Graphite has ZERO-TOLERANCE for contributing undisclosed AI-generated content.
|
||||
If your PR involves AI, you must read our AI contribution policy (it's short):
|
||||
https://graphite.art/volunteer/guide/starting-a-task/ai-contribution-policy
|
||||
|
||||
Closes #
|
||||
REMEMBER:
|
||||
- You are responsible for thoroughly testing the successful implementation of your changes and ensuring no obvious regressions occur.
|
||||
- Egregiously dysfunctional PRs may be assumed to be undisclosed AI slop. If in doubt, ask on Discord before attempting a PR.
|
||||
- You are highly recommended to include a video showing the before-and-after behavior of your changes and screenshots of any new or modified UI.
|
||||
- Remember that Graphite maintains high standards for quality and the project is not a classroom for inexperienced developers to gain industry experience.
|
||||
- In this PR description, reference any relevant tasks by writing "Closes", "Resolves", or "Fixes" with the issue # or the URL of a Discord message documenting the task.
|
||||
- To acknowledge that you've read this, you must delete these rules and fill in the (strictly human-written) PR description in its place.
|
||||
-->
|
||||
|
||||
20
.github/workflows/build-dev-and-ci.yml
vendored
20
.github/workflows/build-dev-and-ci.yml
vendored
@@ -5,9 +5,10 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request: {}
|
||||
merge_group: {}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="dev.graphite.rs" data-api="https://graphite.rs/visit/event" src="https://graphite.rs/visit/script.hash.js"></script>
|
||||
INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="dev.graphite.art" data-api="https://graphite.art/visit/event" src="https://graphite.art/visit/script.hash.js"></script>
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -34,10 +35,10 @@ jobs:
|
||||
with:
|
||||
node-version: "latest"
|
||||
|
||||
- name: 🚧 Install Node dependencies
|
||||
- name: 🚧 Install build dependencies
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci
|
||||
npm run setup
|
||||
|
||||
- name: 🦀 Install the latest Rust
|
||||
run: |
|
||||
@@ -47,6 +48,11 @@ jobs:
|
||||
echo "Latest updated version:"
|
||||
rustc --version
|
||||
|
||||
- name: 🦀 Fetch Rust dependencies
|
||||
run: |
|
||||
echo "If it fails here, the committed Cargo.lock may be out of date"
|
||||
cargo fetch --locked
|
||||
|
||||
- name: ✂ Replace template in <head> of index.html
|
||||
run: |
|
||||
# Remove the INDEX_HTML_HEAD_REPLACEMENT environment variable for build links (not master deploys)
|
||||
@@ -103,14 +109,16 @@ jobs:
|
||||
|
||||
- name: 🧪 Run Rust tests
|
||||
run: |
|
||||
mold -run cargo test --all-features --workspace
|
||||
mold -run cargo test --all-features
|
||||
|
||||
- name: 📃 Generate code documentation info for website
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
cargo test --package graphite-editor --lib -- messages::message::test::generate_message_tree
|
||||
cd tools/editor-message-tree
|
||||
cargo run
|
||||
cd ../..
|
||||
mkdir -p artifacts-generated
|
||||
mv hierarchical_message_system_tree.txt artifacts-generated/hierarchical_message_system_tree.txt
|
||||
mv website/generated/hierarchical_message_system_tree.txt artifacts-generated/hierarchical_message_system_tree.txt
|
||||
|
||||
- name: 💿 Obtain cache of auto-generated code docs artifacts, to check if they've changed
|
||||
if: github.ref == 'refs/heads/master'
|
||||
|
||||
59
.github/workflows/build-linux-bundle.yml
vendored
Normal file
59
.github/workflows/build-linux-bundle.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: Build Linux Bundle
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
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: Free disk space
|
||||
run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache
|
||||
|
||||
- name: Build Linux Bundle
|
||||
run: nix build .nix#graphite-bundle.tar.xz && cp ./result ./graphite-linux-bundle.tar.xz
|
||||
|
||||
- name: Upload Linux Bundle
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: graphite-linux-bundle
|
||||
path: graphite-linux-bundle.tar.xz
|
||||
compression-level: 0
|
||||
|
||||
- name: Setup Flatpak Tooling
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y flatpak flatpak-builder
|
||||
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
|
||||
- name: Build Flatpak
|
||||
run: |
|
||||
nix build .nix#graphite-flatpak-manifest
|
||||
|
||||
rm -rf .flatpak
|
||||
mkdir -p .flatpak
|
||||
|
||||
cp ./result .flatpak/manifest.json
|
||||
|
||||
cd .flatpak
|
||||
mkdir -p repo
|
||||
|
||||
flatpak-builder --user --force-clean --install-deps-from=flathub --repo=repo build ./manifest.json
|
||||
|
||||
flatpak build-bundle repo graphite.flatpak art.graphite.Graphite --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
|
||||
|
||||
- name: Upload Flatpak
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: graphite-flatpak
|
||||
path: .flatpak/graphite.flatpak
|
||||
compression-level: 0
|
||||
153
.github/workflows/build-mac-bundle.yml
vendored
Normal file
153
.github/workflows/build-mac-bundle.yml
vendored
Normal file
@@ -0,0 +1,153 @@
|
||||
name: Build Mac Bundle
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
|
||||
env:
|
||||
WASM_BINDGEN_CLI_VERSION: "0.2.100"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
rustflags: ""
|
||||
target: wasm32-unknown-unknown
|
||||
|
||||
- name: Cache Cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
frontend/package-lock.json
|
||||
|
||||
- name: Install Native Dependencies
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
BINSTALL_DISABLE_TELEMETRY: "true"
|
||||
run: |
|
||||
brew update
|
||||
brew install \
|
||||
pkg-config \
|
||||
openssl@3 \
|
||||
binaryen \
|
||||
llvm \
|
||||
cargo-binstall
|
||||
|
||||
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
|
||||
echo "$(brew --prefix llvm)/bin" >> $GITHUB_PATH
|
||||
|
||||
cargo binstall --no-confirm --force wasm-pack
|
||||
cargo binstall --no-confirm --force cargo-about
|
||||
cargo binstall --no-confirm --force "wasm-bindgen-cli@${WASM_BINDGEN_CLI_VERSION}"
|
||||
|
||||
- name: Build Mac Bundle
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
run: npm run build-desktop
|
||||
|
||||
- name: Stage Artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
rm -rf target/artifacts
|
||||
mkdir -p target/artifacts
|
||||
cp -R target/release/Graphite.app target/artifacts/Graphite.app
|
||||
|
||||
- name: Upload Mac Bundle
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: graphite-mac-bundle
|
||||
path: target/artifacts
|
||||
|
||||
- name: Sign and Notarize Mac Bundle Preparation
|
||||
env:
|
||||
APPLE_CERT_BASE64: ${{ secrets.APPLE_CERT_BASE64 }}
|
||||
APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }}
|
||||
run: |
|
||||
mkdir -p .sign
|
||||
echo "$APPLE_CERT_BASE64" | base64 --decode > .sign/certificate.p12
|
||||
|
||||
security create-keychain -p "" .sign/main.keychain
|
||||
security default-keychain -s .sign/main.keychain
|
||||
security unlock-keychain -p "" .sign/main.keychain
|
||||
security set-keychain-settings -t 3600 -u .sign/main.keychain
|
||||
|
||||
security import .sign/certificate.p12 -k .sign/main.keychain -P "$APPLE_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productsign
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k "" .sign/main.keychain
|
||||
|
||||
cat > .sign/entitlements.plist <<'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
- name: Sign and Notarize Mac Bundle
|
||||
env:
|
||||
APPLE_EMAIL: ${{ secrets.APPLE_EMAIL }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_CERT_NAME: ${{ secrets.APPLE_CERT_NAME }}
|
||||
run: |
|
||||
CERTIFICATE="$APPLE_CERT_NAME"
|
||||
ENTITLEMENTS=".sign/entitlements.plist"
|
||||
APP_PATH="target/artifacts/Graphite.app"
|
||||
ZIP_PATH=".sign/Graphite.zip"
|
||||
|
||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper.app"
|
||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper (GPU).app"
|
||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper (Renderer).app"
|
||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework"
|
||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libcef_sandbox.dylib"
|
||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libEGL.dylib"
|
||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libGLESv2.dylib"
|
||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libvk_swiftshader.dylib"
|
||||
codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH" --deep
|
||||
|
||||
codesign --verify --deep --strict --verbose=4 "$APP_PATH"
|
||||
|
||||
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
|
||||
xcrun notarytool submit "$ZIP_PATH" --wait --apple-id "$APPLE_EMAIL" --team-id "$APPLE_TEAM_ID" --password "$APPLE_PASSWORD"
|
||||
rm "$ZIP_PATH"
|
||||
|
||||
xcrun stapler staple -v "$APP_PATH"
|
||||
|
||||
spctl -a -vv "$APP_PATH"
|
||||
|
||||
- name: Upload Mac Bundle Signed
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: graphite-mac-bundle-signed
|
||||
path: target/artifacts
|
||||
17
.github/workflows/build-nix-package.yml
vendored
Normal file
17
.github/workflows/build-nix-package.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Build Nix Package
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
|
||||
- name: Build Nix Package Dev
|
||||
run: nix build .nix#graphite-dev --print-build-logs
|
||||
6
.github/workflows/build-production.yml
vendored
6
.github/workflows/build-production.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
RUSTC_WRAPPER: /usr/bin/sccache
|
||||
CARGO_INCREMENTAL: 0
|
||||
SCCACHE_DIR: /var/lib/github-actions/.cache
|
||||
INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="editor.graphite.rs" data-api="https://graphite.rs/visit/event" src="https://graphite.rs/visit/script.hash.js"></script>
|
||||
INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="editor.graphite.art" data-api="https://graphite.art/visit/event" src="https://graphite.art/visit/script.hash.js"></script>
|
||||
|
||||
steps:
|
||||
- name: 📥 Clone and checkout repository
|
||||
@@ -32,10 +32,10 @@ jobs:
|
||||
with:
|
||||
node-version: "latest"
|
||||
|
||||
- name: 🚧 Install Node dependencies
|
||||
- name: 🚧 Install build dependencies
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci
|
||||
npm run setup
|
||||
|
||||
- name: 🦀 Install the latest Rust
|
||||
run: |
|
||||
|
||||
167
.github/workflows/build-win-bundle.yml
vendored
Normal file
167
.github/workflows/build-win-bundle.yml
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
name: Build Windows Bundle
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
||||
env:
|
||||
WASM_BINDGEN_CLI_VERSION: "0.2.100"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
rustflags: ""
|
||||
target: wasm32-unknown-unknown
|
||||
|
||||
- name: Cache Cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ env.USERPROFILE }}\.cargo\registry
|
||||
${{ env.USERPROFILE }}\.cargo\git
|
||||
target
|
||||
key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
frontend/package-lock.json
|
||||
|
||||
- name: Setup Cargo Binstall
|
||||
uses: cargo-bins/cargo-binstall@main
|
||||
|
||||
- name: Install Native Dependencies
|
||||
shell: pwsh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
BINSTALL_DISABLE_TELEMETRY: "true"
|
||||
run: |
|
||||
winget install --id LLVM.LLVM -e --accept-package-agreements --accept-source-agreements
|
||||
winget install --id Kitware.CMake -e --accept-package-agreements --accept-source-agreements
|
||||
winget install --id OpenSSL.OpenSSL -e --accept-package-agreements --accept-source-agreements
|
||||
winget install --id WebAssembly.Binaryen -e --accept-package-agreements --accept-source-agreements
|
||||
winget install --id GnuWin32.PkgConfig -e --accept-package-agreements --accept-source-agreements
|
||||
|
||||
"OPENSSL_DIR=C:\Program Files\OpenSSL-Win64" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
"PKG_CONFIG_PATH=C:\Program Files\OpenSSL-Win64\lib\pkgconfig" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
|
||||
cargo binstall --no-confirm --force wasm-pack
|
||||
cargo binstall --no-confirm --force cargo-about
|
||||
cargo binstall --no-confirm --force "wasm-bindgen-cli@$env:WASM_BINDGEN_CLI_VERSION"
|
||||
|
||||
- name: Build Windows Bundle
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
run: npm run build-desktop
|
||||
|
||||
- name: Stage Artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
rm -rf target/artifacts
|
||||
mkdir -p target/artifacts
|
||||
cp -R target/release/Graphite target/artifacts/Graphite
|
||||
|
||||
- name: Upload Windows Bundle
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: graphite-windows-bundle
|
||||
path: target/artifacts
|
||||
|
||||
- name: Azure login
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
enable-AzPSSession: true
|
||||
|
||||
- name: Sign
|
||||
uses: azure/artifact-signing-action@v1
|
||||
with:
|
||||
endpoint: https://eus.codesigning.azure.net/
|
||||
signing-account-name: Graphite
|
||||
certificate-profile-name: Graphite
|
||||
files: |
|
||||
${{ github.workspace }}\target\artifacts\Graphite\Graphite.exe
|
||||
${{ github.workspace }}\target\artifacts\Graphite\libcef.dll
|
||||
${{ github.workspace }}\target\artifacts\Graphite\chrome_elf.dll
|
||||
${{ github.workspace }}\target\artifacts\Graphite\vulkan-1.dll
|
||||
${{ github.workspace }}\target\artifacts\Graphite\dxcompiler.dll
|
||||
${{ github.workspace }}\target\artifacts\Graphite\libEGL.dll
|
||||
${{ github.workspace }}\target\artifacts\Graphite\libGLESv2.dll
|
||||
${{ github.workspace }}\target\artifacts\Graphite\vk_swiftshader.dll
|
||||
file-digest: SHA256
|
||||
timestamp-rfc3161: http://timestamp.acs.microsoft.com
|
||||
timestamp-digest: SHA256
|
||||
correlation-id: ${{ github.sha }}
|
||||
|
||||
- name: Verify Signatures
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$TargetDir = "target\artifacts\Graphite"
|
||||
|
||||
if (-not (Test-Path $TargetDir)) {
|
||||
throw "TargetDir not found: $TargetDir"
|
||||
}
|
||||
|
||||
$UnsignedOrBad = @()
|
||||
|
||||
Get-ChildItem -Path $TargetDir -Recurse -File -Include *.exe,*.dll | ForEach-Object {
|
||||
$sig = Get-AuthenticodeSignature -FilePath $_.FullName
|
||||
|
||||
if ($sig.Status -ne 'Valid') {
|
||||
$UnsignedOrBad += "$($_.FullName) (Status=$($sig.Status))"
|
||||
}
|
||||
}
|
||||
|
||||
if ($UnsignedOrBad.Count -gt 0) {
|
||||
Write-Host "Unsigned or invalid binaries detected:"
|
||||
$UnsignedOrBad | ForEach-Object {
|
||||
Write-Host "::error::$_"
|
||||
}
|
||||
|
||||
if ($env:GITHUB_STEP_SUMMARY) {
|
||||
"### ❌ Unsigned or invalid binaries detected" |
|
||||
Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
|
||||
"" | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
|
||||
$UnsignedOrBad | ForEach-Object {
|
||||
"* `$_" | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
|
||||
}
|
||||
}
|
||||
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "All binaries are signed and valid."
|
||||
|
||||
if ($env:GITHUB_STEP_SUMMARY) {
|
||||
"### ✅ All binaries are signed and valid" |
|
||||
Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
|
||||
}
|
||||
|
||||
- name: Upload Windows Bundle Signed
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: graphite-windows-bundle-signed
|
||||
path: target/artifacts
|
||||
1
.github/workflows/cargo-deny.yml
vendored
1
.github/workflows/cargo-deny.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
cargo-deny:
|
||||
if: github.repository == 'GraphiteEditor/Graphite' # Don't run on forks by default
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
@@ -59,10 +59,10 @@ jobs:
|
||||
with:
|
||||
node-version: "latest"
|
||||
|
||||
- name: 🚧 Install Node dependencies
|
||||
- name: 🚧 Install build dependencies
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci
|
||||
npm run setup
|
||||
|
||||
- name: 🦀 Install the latest Rust
|
||||
run: |
|
||||
|
||||
190
.github/workflows/comment-profiling-changes.yaml
vendored
190
.github/workflows/comment-profiling-changes.yaml
vendored
@@ -2,6 +2,10 @@ name: Profiling Changes
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'node-graph/**'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -9,6 +13,7 @@ env:
|
||||
jobs:
|
||||
profile:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -33,12 +38,12 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cargo/bin/iai-callgrind-runner
|
||||
key: ${{ runner.os }}-iai-callgrind-runner-0.12.3
|
||||
key: ${{ runner.os }}-iai-callgrind-runner-0.16.1
|
||||
|
||||
- name: Install iai-callgrind
|
||||
if: steps.cache-iai.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cargo install iai-callgrind-runner@0.12.3
|
||||
cargo install iai-callgrind-runner@0.16.1
|
||||
|
||||
- name: Checkout master branch
|
||||
run: |
|
||||
@@ -49,21 +54,30 @@ jobs:
|
||||
id: master-sha
|
||||
run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get CPU info
|
||||
id: cpu-info
|
||||
run: |
|
||||
# Get CPU model and create a short hash for cache key
|
||||
CPU_MODEL=$(cat /proc/cpuinfo | grep "model name" | head -1 | cut -d: -f2 | xargs)
|
||||
CPU_HASH=$(echo "$CPU_MODEL" | sha256sum | cut -c1-8)
|
||||
echo "cpu-hash=$CPU_HASH" >> $GITHUB_OUTPUT
|
||||
echo "CPU: $CPU_MODEL (hash: $CPU_HASH)"
|
||||
|
||||
- name: Cache benchmark baselines
|
||||
id: cache-benchmark-baselines
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: target/iai
|
||||
key: ${{ runner.os }}-benchmark-baselines-master-${{ steps.master-sha.outputs.sha }}
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-${{ steps.cpu-info.outputs.cpu-hash }}-benchmark-baselines-master-${{ steps.master-sha.outputs.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-benchmark-baselines-master-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.cpu-info.outputs.cpu-hash }}-benchmark-baselines-master-
|
||||
|
||||
- name: Run baseline benchmarks
|
||||
if: steps.cache-benchmark-baselines.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
# Compile benchmarks
|
||||
cargo bench --bench compile_demo_art_iai -- --save-baseline=master
|
||||
|
||||
|
||||
# Runtime benchmarks
|
||||
cargo bench --bench update_executor_iai -- --save-baseline=master
|
||||
cargo bench --bench run_once_iai -- --save-baseline=master
|
||||
@@ -74,34 +88,18 @@ jobs:
|
||||
git checkout ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Run PR benchmarks
|
||||
id: benchmark
|
||||
run: |
|
||||
# Compile benchmarks
|
||||
COMPILE_OUTPUT=$(cargo bench --bench compile_demo_art_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g')
|
||||
|
||||
# Runtime benchmarks
|
||||
UPDATE_OUTPUT=$(cargo bench --bench update_executor_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g')
|
||||
RUN_ONCE_OUTPUT=$(cargo bench --bench run_once_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g')
|
||||
RUN_CACHED_OUTPUT=$(cargo bench --bench run_cached_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g')
|
||||
|
||||
# Store outputs
|
||||
echo "COMPILE_OUTPUT<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$COMPILE_OUTPUT" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "UPDATE_OUTPUT<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$UPDATE_OUTPUT" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "RUN_ONCE_OUTPUT<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$RUN_ONCE_OUTPUT" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "RUN_CACHED_OUTPUT<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$RUN_CACHED_OUTPUT" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
cargo bench --bench compile_demo_art_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/compile_output.json
|
||||
|
||||
# Runtime benchmarks
|
||||
cargo bench --bench update_executor_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/update_output.json
|
||||
cargo bench --bench run_once_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/run_once_output.json
|
||||
cargo bench --bench run_cached_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/run_cached_output.json
|
||||
|
||||
- name: Make old comments collapsed by default
|
||||
# Only run if we have write permissions (not a fork)
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
@@ -126,17 +124,79 @@ jobs:
|
||||
});
|
||||
}
|
||||
|
||||
- name: Analyze profiling changes
|
||||
id: analyze
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
function isSignificantChange(diffPct, absoluteChange, benchmarkType) {
|
||||
const meetsPercentageThreshold = Math.abs(diffPct) > 5;
|
||||
const meetsAbsoluteThreshold = absoluteChange > 200000;
|
||||
const isCachedExecution = benchmarkType === 'run_cached' ||
|
||||
benchmarkType.includes('Cached Execution');
|
||||
|
||||
return isCachedExecution
|
||||
? (meetsPercentageThreshold && meetsAbsoluteThreshold)
|
||||
: meetsPercentageThreshold;
|
||||
}
|
||||
|
||||
const allOutputs = [
|
||||
JSON.parse(fs.readFileSync('/tmp/compile_output.json', 'utf8')),
|
||||
JSON.parse(fs.readFileSync('/tmp/update_output.json', 'utf8')),
|
||||
JSON.parse(fs.readFileSync('/tmp/run_once_output.json', 'utf8')),
|
||||
JSON.parse(fs.readFileSync('/tmp/run_cached_output.json', 'utf8'))
|
||||
];
|
||||
const outputNames = ['compile', 'update', 'run_once', 'run_cached'];
|
||||
const sectionTitles = ['Compilation', 'Update', 'Run Once', 'Cached Execution'];
|
||||
|
||||
let hasSignificantChanges = false;
|
||||
let regressionDetails = [];
|
||||
|
||||
for (let i = 0; i < allOutputs.length; i++) {
|
||||
const benchmarkOutput = allOutputs[i];
|
||||
const outputName = outputNames[i];
|
||||
const sectionTitle = sectionTitles[i];
|
||||
|
||||
for (const benchmark of benchmarkOutput) {
|
||||
if (benchmark.profiles?.[0]?.summaries?.parts?.[0]?.metrics_summary?.Callgrind?.Ir?.diffs?.diff_pct) {
|
||||
const diffPct = parseFloat(benchmark.profiles[0].summaries.parts[0].metrics_summary.Callgrind.Ir.diffs.diff_pct);
|
||||
const oldValue = benchmark.profiles[0].summaries.parts[0].metrics_summary.Callgrind.Ir.metrics.Both[1].Int;
|
||||
const newValue = benchmark.profiles[0].summaries.parts[0].metrics_summary.Callgrind.Ir.metrics.Both[0].Int;
|
||||
const absoluteChange = Math.abs(newValue - oldValue);
|
||||
|
||||
if (isSignificantChange(diffPct, absoluteChange, outputName)) {
|
||||
hasSignificantChanges = true;
|
||||
regressionDetails.push({
|
||||
module_path: benchmark.module_path,
|
||||
id: benchmark.id,
|
||||
diffPct,
|
||||
absoluteChange,
|
||||
sectionTitle
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
core.setOutput('has-significant-changes', hasSignificantChanges);
|
||||
core.setOutput('regression-details', JSON.stringify(regressionDetails));
|
||||
|
||||
- name: Comment PR
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const compileOutput = JSON.parse(`${{ steps.benchmark.outputs.COMPILE_OUTPUT }}`);
|
||||
const updateOutput = JSON.parse(`${{ steps.benchmark.outputs.UPDATE_OUTPUT }}`);
|
||||
const runOnceOutput = JSON.parse(`${{ steps.benchmark.outputs.RUN_ONCE_OUTPUT }}`);
|
||||
const runCachedOutput = JSON.parse(`${{ steps.benchmark.outputs.RUN_CACHED_OUTPUT }}`);
|
||||
|
||||
let significantChanges = false;
|
||||
const fs = require('fs');
|
||||
|
||||
const compileOutput = JSON.parse(fs.readFileSync('/tmp/compile_output.json', 'utf8'));
|
||||
const updateOutput = JSON.parse(fs.readFileSync('/tmp/update_output.json', 'utf8'));
|
||||
const runOnceOutput = JSON.parse(fs.readFileSync('/tmp/run_once_output.json', 'utf8'));
|
||||
const runCachedOutput = JSON.parse(fs.readFileSync('/tmp/run_cached_output.json', 'utf8'));
|
||||
|
||||
const hasSignificantChanges = '${{ steps.analyze.outputs.has-significant-changes }}' === 'true';
|
||||
let commentBody = "";
|
||||
|
||||
function formatNumber(num) {
|
||||
@@ -160,13 +220,31 @@ jobs:
|
||||
let sectionBody = "";
|
||||
let hasResults = false;
|
||||
let hasSignificantChanges = false;
|
||||
|
||||
function isSignificantChange(diffPct, absoluteChange, benchmarkType) {
|
||||
const meetsPercentageThreshold = Math.abs(diffPct) > 5;
|
||||
const meetsAbsoluteThreshold = absoluteChange > 200000;
|
||||
const isCachedExecution = benchmarkType === 'run_cached' ||
|
||||
benchmarkType.includes('Cached Execution');
|
||||
|
||||
return isCachedExecution
|
||||
? (meetsPercentageThreshold && meetsAbsoluteThreshold)
|
||||
: meetsPercentageThreshold;
|
||||
}
|
||||
|
||||
for (const benchmark of benchmarkOutput) {
|
||||
if (benchmark.callgrind_summary && benchmark.callgrind_summary.summaries) {
|
||||
const summary = benchmark.callgrind_summary.summaries[0];
|
||||
const irDiff = summary.events.Ir;
|
||||
|
||||
if (irDiff.diff_pct !== null) {
|
||||
if (benchmark.profiles && benchmark.profiles.length > 0) {
|
||||
const profile = benchmark.profiles[0];
|
||||
if (profile.summaries && profile.summaries.parts && profile.summaries.parts.length > 0) {
|
||||
const part = profile.summaries.parts[0];
|
||||
if (part.metrics_summary && part.metrics_summary.Callgrind && part.metrics_summary.Callgrind.Ir) {
|
||||
const irData = part.metrics_summary.Callgrind.Ir;
|
||||
if (irData.diffs && irData.diffs.diff_pct !== null) {
|
||||
const irDiff = {
|
||||
diff_pct: parseFloat(irData.diffs.diff_pct),
|
||||
old: irData.metrics.Both[1].Int,
|
||||
new: irData.metrics.Both[0].Int
|
||||
};
|
||||
hasResults = true;
|
||||
const changePercentage = formatPercentage(irDiff.diff_pct);
|
||||
const color = irDiff.diff_pct > 0 ? "red" : "lime";
|
||||
@@ -178,19 +256,23 @@ jobs:
|
||||
sectionBody += "<details>\n<summary>Detailed metrics</summary>\n\n```\n";
|
||||
sectionBody += `Baselines: master| HEAD\n`;
|
||||
|
||||
for (const [eventKind, costsDiff] of Object.entries(summary.events)) {
|
||||
if (costsDiff.diff_pct !== null) {
|
||||
const changePercentage = formatPercentage(costsDiff.diff_pct);
|
||||
const line = `${padRight(eventKind, 20)} ${padLeft(formatNumber(costsDiff.old), 11)}|${padLeft(formatNumber(costsDiff.new), 11)} ${padLeft(changePercentage, 15)}`;
|
||||
for (const [metricName, metricData] of Object.entries(part.metrics_summary.Callgrind)) {
|
||||
if (metricData.diffs && metricData.diffs.diff_pct !== null) {
|
||||
const changePercentage = formatPercentage(parseFloat(metricData.diffs.diff_pct));
|
||||
const oldValue = metricData.metrics.Both[1].Int || metricData.metrics.Both[1].Float;
|
||||
const newValue = metricData.metrics.Both[0].Int || metricData.metrics.Both[0].Float;
|
||||
const line = `${padRight(metricName, 20)} ${padLeft(formatNumber(Math.round(oldValue)), 11)}|${padLeft(formatNumber(Math.round(newValue)), 11)} ${padLeft(changePercentage, 15)}`;
|
||||
sectionBody += `${line}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
sectionBody += "```\n</details>\n\n";
|
||||
|
||||
if (Math.abs(irDiff.diff_pct) > 5) {
|
||||
significantChanges = true;
|
||||
hasSignificantChanges = true;
|
||||
|
||||
if (isSignificantChange(irDiff.diff_pct, Math.abs(irDiff.new - irDiff.old), sectionTitle)) {
|
||||
significantChanges = true;
|
||||
hasSignificantChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,7 +318,7 @@ jobs:
|
||||
if (commentBody.length > 0) {
|
||||
const output = `<details open>\n<summary>Performance Benchmark Results</summary>\n\n${commentBody}\n</details>`;
|
||||
|
||||
if (significantChanges) {
|
||||
if (hasSignificantChanges) {
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
@@ -250,3 +332,13 @@ jobs:
|
||||
} else {
|
||||
console.log("No benchmark results to display.");
|
||||
}
|
||||
|
||||
- name: Fail on significant regressions
|
||||
if: steps.analyze.outputs.has-significant-changes == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const regressionDetails = JSON.parse('${{ steps.analyze.outputs.regression-details }}');
|
||||
const firstRegression = regressionDetails[0];
|
||||
|
||||
core.setFailed(`Significant performance regression detected: ${firstRegression.module_path} ${firstRegression.id} increased by ${firstRegression.absoluteChange.toLocaleString()} instructions (${firstRegression.diffPct.toFixed(2)}%)`);
|
||||
|
||||
9
.github/workflows/library-rawkit.yml
vendored
9
.github/workflows/library-rawkit.yml
vendored
@@ -38,6 +38,13 @@ jobs:
|
||||
|
||||
- name: 📦 Run sccache-cache
|
||||
uses: mozilla-actions/sccache-action@v0.0.6
|
||||
continue-on-error: true
|
||||
|
||||
- name: 🔧 Fallback if sccache fails
|
||||
if: failure()
|
||||
run: |
|
||||
echo "sccache failed, disabling it"
|
||||
echo "RUSTC_WRAPPER=" >> $GITHUB_ENV
|
||||
|
||||
- name: 🔬 Check Rust formatting
|
||||
run: |
|
||||
@@ -56,4 +63,4 @@ jobs:
|
||||
|
||||
- name: 📈 Run sccache stat for check
|
||||
shell: bash
|
||||
run: sccache --show-stats
|
||||
run: sccache --show-stats || echo "sccache stats unavailable"
|
||||
|
||||
31
.github/workflows/provide-shaders.yml
vendored
Normal file
31
.github/workflows/provide-shaders.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Provide Shaders
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
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 graphene raster nodes shaders
|
||||
run: nix build .nix#raster-nodes-shaders && cp result raster_nodes_shaders_entrypoint.wgsl
|
||||
|
||||
- name: Upload graphene raster nodes shaders to artifacts repository
|
||||
run: |
|
||||
bash .github/workflows/scripts/artifact-upload.bash \
|
||||
${{ vars.ARTIFACTS_REPO_OWNER }} \
|
||||
${{ vars.ARTIFACTS_REPO_NAME }} \
|
||||
${{ vars.ARTIFACTS_REPO_BRANCH }} \
|
||||
rev/${{ github.sha }}/raster_nodes_shaders_entrypoint.wgsl \
|
||||
raster_nodes_shaders_entrypoint.wgsl \
|
||||
"${{ github.sha }} raster_nodes_shaders_entrypoint.wgsl" \
|
||||
${{ secrets.ARTIFACTS_REPO_TOKEN }}
|
||||
89
.github/workflows/scripts/artifact-upload.bash
vendored
Normal file
89
.github/workflows/scripts/artifact-upload.bash
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 <owner> <repo> <branch> <target-path> <artifact-file> <commit-message> <github-token>
|
||||
|
||||
Arguments:
|
||||
owner : GitHub user or organization of the target repo
|
||||
repo : Target repo name
|
||||
branch : Branch name (e.g. main)
|
||||
target-path : Full path (including folders + filename) in the target repo where to upload
|
||||
artifact-file : Local file path to upload
|
||||
commit-message : Commit message for creating/updating the file
|
||||
github-token : GitHub token (PAT or equivalent) with write access to the target repo
|
||||
|
||||
This will perform a GitHub API PUT to /repos/{owner}/{repo}/contents/{target-path}.
|
||||
If a file already exists at that path, it will auto-detect the SHA and update; otherwise it will create a new one.
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ $# -ne 7 ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
OWNER="$1"
|
||||
REPO="$2"
|
||||
BRANCH="$3"
|
||||
TARGET_PATH="$4"
|
||||
ARTIFACT_PATH="$5"
|
||||
COMMIT_MSG="$6"
|
||||
TOKEN="$7"
|
||||
|
||||
if [ ! -f "$ARTIFACT_PATH" ]; then
|
||||
echo "Error: artifact file not found: $ARTIFACT_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOCAL_SHA=$(git hash-object "$ARTIFACT_PATH")
|
||||
echo "Local blob SHA: $LOCAL_SHA"
|
||||
|
||||
GET_URL="https://api.github.com/repos/${OWNER}/${REPO}/contents/${TARGET_PATH}?ref=${BRANCH}"
|
||||
GET_RESPONSE=$(curl -s -H "Authorization: token ${TOKEN}" "$GET_URL")
|
||||
|
||||
REMOTE_SHA=$(echo "$GET_RESPONSE" | jq -r .sha 2>/dev/null || echo "")
|
||||
|
||||
if [ "$REMOTE_SHA" != "null" ] && [ -n "$REMOTE_SHA" ]; then
|
||||
echo "Remote blob SHA: $REMOTE_SHA"
|
||||
if [ "$LOCAL_SHA" = "$REMOTE_SHA" ]; then
|
||||
echo "The remote file is identical. Skipping upload."
|
||||
exit 0
|
||||
else
|
||||
echo "Remote file differs. Preparing to upload."
|
||||
fi
|
||||
else
|
||||
echo "No existing remote file or no SHA found. Creating."
|
||||
fi
|
||||
|
||||
CONTENT_TMP_BASE64=$(mktemp)
|
||||
if base64 --help 2>&1 | grep -q -- "-w"; then
|
||||
base64 -w 0 "$ARTIFACT_PATH" > "$CONTENT_TMP_BASE64"
|
||||
else
|
||||
base64 "$ARTIFACT_PATH" | tr -d '\n' > "$CONTENT_TMP_BASE64"
|
||||
fi
|
||||
|
||||
PAYLOAD_TMP=$(mktemp)
|
||||
jq -n \
|
||||
--arg message "$COMMIT_MSG" \
|
||||
--arg branch "$BRANCH" \
|
||||
--arg sha "$REMOTE_SHA" \
|
||||
--rawfile content "$CONTENT_TMP_BASE64" \
|
||||
'{
|
||||
message: $message,
|
||||
content: $content,
|
||||
branch: $branch
|
||||
} + (if ($sha != "" and $sha != "null") then { sha: $sha } else {} end)' \
|
||||
> "$PAYLOAD_TMP"
|
||||
|
||||
UPLOAD_RESPONSE=$(curl -s -X PUT \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @"$PAYLOAD_TMP" \
|
||||
"https://api.github.com/repos/${OWNER}/${REPO}/contents/${TARGET_PATH}")
|
||||
|
||||
echo "Upload Response:"
|
||||
echo "$UPLOAD_RESPONSE"
|
||||
|
||||
rm -f "$CONTENT_TMP_BASE64" "$PAYLOAD_TMP"
|
||||
31
.github/workflows/website.yml
vendored
31
.github/workflows/website.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
workflow_dispatch: {}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
INDEX_HTML_HEAD_INCLUSION: <script defer data-domain="graphite.rs" data-api="/visit/event" src="/visit/script.hash.js"></script>
|
||||
INDEX_HTML_HEAD_INCLUSION: <script defer data-domain="graphite.art" data-api="/visit/event" src="/visit/script.hash.js"></script>
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -26,10 +26,18 @@ jobs:
|
||||
- name: 📥 Clone and checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# We can remove this step once `ubuntu-latest` has Node.js 22 or newer for its native TypeScript support. See:
|
||||
# https://github.com/actions/runner-images?tab=readme-ov-file#available-images
|
||||
# https://nodejs.org/en/learn/typescript/run-natively
|
||||
- name: 📦 Install the latest Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "latest"
|
||||
|
||||
- name: 🕸 Install Zola
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: zola@0.20.0
|
||||
tool: zola@0.22.0
|
||||
|
||||
- name: 🔍 Check if `website/other` directory changed
|
||||
uses: dorny/paths-filter@v3
|
||||
@@ -59,25 +67,34 @@ jobs:
|
||||
rustup update stable
|
||||
echo "🦀 Latest updated version of Rust:"
|
||||
rustc --version
|
||||
cargo test --package graphite-editor --lib -- messages::message::test::generate_message_tree
|
||||
cd tools/editor-message-tree
|
||||
cargo run
|
||||
cd ../..
|
||||
mkdir artifacts
|
||||
mv hierarchical_message_system_tree.txt artifacts/hierarchical_message_system_tree.txt
|
||||
mv website/generated/hierarchical_message_system_tree.txt artifacts/hierarchical_message_system_tree.txt
|
||||
|
||||
- name: 🚚 Move `artifacts` contents to the project root
|
||||
- name: 🚚 Move `artifacts` contents to website/generated
|
||||
run: |
|
||||
mv artifacts/* .
|
||||
mkdir -p website/generated
|
||||
mv artifacts/* website/generated/
|
||||
|
||||
- name: 🔧 Build auto-generated code docs artifacts into HTML
|
||||
run: |
|
||||
cd website
|
||||
npm run generate-editor-structure
|
||||
|
||||
- name: 📃 Generate node catalog documentation
|
||||
run: |
|
||||
cd tools/node-docs
|
||||
cargo run
|
||||
|
||||
- name: 🌐 Build Graphite website with Zola
|
||||
env:
|
||||
MODE: prod
|
||||
run: |
|
||||
cd website
|
||||
npm run install-fonts
|
||||
npm ci
|
||||
npm run lint
|
||||
zola --config config.toml build --minify
|
||||
|
||||
- name: 📤 Publish to Cloudflare Pages
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,4 +1,7 @@
|
||||
branding/
|
||||
target/
|
||||
result/
|
||||
.flatpak-builder/
|
||||
*.spv
|
||||
*.exrc
|
||||
perf.data*
|
||||
@@ -7,5 +10,4 @@ profile.json.gz
|
||||
flamegraph.svg
|
||||
.idea/
|
||||
.direnv
|
||||
hierarchical_message_system_tree.txt
|
||||
hierarchical_message_system_tree.html
|
||||
.DS_Store
|
||||
|
||||
28
.nix/deps/cef.nix
Normal file
28
.nix/deps/cef.nix
Normal file
@@ -0,0 +1,28 @@
|
||||
{ pkgs, inputs, ... }:
|
||||
|
||||
let
|
||||
cef = pkgs.cef-binary.overrideAttrs {
|
||||
postInstall = ''
|
||||
strip $out/Release/*.so*
|
||||
'';
|
||||
};
|
||||
|
||||
cefPath = pkgs.runCommand "cef-path" { } ''
|
||||
mkdir -p $out
|
||||
|
||||
ln -s ${cef}/include $out/include
|
||||
find ${cef}/Release -name "*" -type f -exec ln -s {} $out/ \;
|
||||
find ${cef}/Resources -name "*" -maxdepth 1 -exec ln -s {} $out/ \;
|
||||
|
||||
echo '${
|
||||
builtins.toJSON {
|
||||
type = "minimal";
|
||||
name = builtins.baseNameOf cef.src.url;
|
||||
sha1 = "";
|
||||
}
|
||||
}' > $out/archive.json
|
||||
'';
|
||||
in
|
||||
{
|
||||
env.CEF_PATH = cefPath;
|
||||
}
|
||||
5
.nix/deps/crane.nix
Normal file
5
.nix/deps/crane.nix
Normal file
@@ -0,0 +1,5 @@
|
||||
{ pkgs, inputs, ... }:
|
||||
|
||||
{
|
||||
lib = inputs.crane.mkLib pkgs;
|
||||
}
|
||||
58
.nix/deps/rust-gpu.nix
Normal file
58
.nix/deps/rust-gpu.nix
Normal file
@@ -0,0 +1,58 @@
|
||||
{ pkgs, inputs, ... }:
|
||||
|
||||
let
|
||||
extensions = [
|
||||
"rust-src"
|
||||
"rust-analyzer"
|
||||
"clippy"
|
||||
"cargo"
|
||||
"rustc-dev"
|
||||
"llvm-tools"
|
||||
];
|
||||
toolchain = pkgs.rust-bin.nightly."2025-06-23".default.override {
|
||||
inherit extensions;
|
||||
};
|
||||
cargo = pkgs.writeShellScriptBin "cargo" ''
|
||||
#!${pkgs.lib.getExe pkgs.bash}
|
||||
|
||||
filtered_args=()
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
+nightly|+nightly-*) ;;
|
||||
*) filtered_args+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
exec ${toolchain}/bin/cargo ${"\${filtered_args[@]}"}
|
||||
'';
|
||||
rustc_codegen_spirv =
|
||||
(pkgs.makeRustPlatform {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
}).buildRustPackage
|
||||
(finalAttrs: {
|
||||
pname = "rustc_codegen_spirv";
|
||||
version = "0-unstable-2025-08-04";
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "Firestar99";
|
||||
repo = "rust-gpu-new";
|
||||
rev = "c12f216121820580731440ee79ebc7403d6ea04f";
|
||||
hash = "sha256-rG1cZvOV0vYb1dETOzzbJ0asYdE039UZImobXZfKIno=";
|
||||
};
|
||||
cargoHash = "sha256-AEigcEc5wiBd3zLqWN/2HSbkfOVFneAqNvg9HsouZf4=";
|
||||
cargoBuildFlags = [
|
||||
"-p"
|
||||
"rustc_codegen_spirv"
|
||||
"--features=use-compiled-tools"
|
||||
"--no-default-features"
|
||||
];
|
||||
doCheck = false;
|
||||
});
|
||||
in
|
||||
{
|
||||
toolchain = toolchain;
|
||||
env = {
|
||||
RUST_GPU_PATH_OVERRIDE = "${cargo}/bin:${toolchain}/bin";
|
||||
RUSTC_CODEGEN_SPIRV_PATH = "${rustc_codegen_spirv}/lib/librustc_codegen_spirv.so";
|
||||
};
|
||||
}
|
||||
22
.nix/dev.nix
Normal file
22
.nix/dev.nix
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
pkgs,
|
||||
deps,
|
||||
libs,
|
||||
tools,
|
||||
...
|
||||
}:
|
||||
|
||||
pkgs.mkShell (
|
||||
{
|
||||
packages = tools.all ++ libs.all;
|
||||
|
||||
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath libs.all}:${deps.cef.env.CEF_PATH}";
|
||||
XDG_DATA_DIRS = "${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS";
|
||||
|
||||
shellHook = ''
|
||||
alias cargo='mold --run cargo'
|
||||
'';
|
||||
}
|
||||
// deps.cef.env
|
||||
// deps.rustGPU.env
|
||||
)
|
||||
28
.nix/flake.lock
generated
28
.nix/flake.lock
generated
@@ -1,5 +1,20 @@
|
||||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1763938834,
|
||||
"narHash": "sha256-j8iB0Yr4zAvQLueCZ5abxfk6fnG/SJ5JnGUziETjwfg=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "d9e753122e51cee64eb8d2dddfe11148f339f5a2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"locked": {
|
||||
"lastModified": 1733328505,
|
||||
@@ -34,11 +49,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1754214453,
|
||||
"narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=",
|
||||
"lastModified": 1764242076,
|
||||
"narHash": "sha256-sKoIWfnijJ0+9e4wRvIgm/HgE27bzwQxcEmo2J/gNpI=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376",
|
||||
"rev": "2fad6eac6077f03fe109c4d4eb171cf96791faa4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -50,6 +65,7 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
@@ -63,11 +79,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1753238793,
|
||||
"narHash": "sha256-jmQeEpgX+++MEgrcikcwoSiI7vDZWLP0gci7XiWb9uQ=",
|
||||
"lastModified": 1764297505,
|
||||
"narHash": "sha256-qrLpVu2/hA9Cu6IovMEsgh9YRyvmmWS+bSx7C1JGChA=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "0ad7ab4ca8e83febf147197e65c006dff60623ab",
|
||||
"rev": "9623580f8ce09ec444b9aca107566ec5db110e62",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
207
.nix/flake.nix
207
.nix/flake.nix
@@ -12,8 +12,6 @@
|
||||
# - Development shell: `nix develop .nix` from the project root
|
||||
# - Run in dev shell with direnv: add `use flake` to .envrc
|
||||
{
|
||||
description = "Development environment and build configuration";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
rust-overlay = {
|
||||
@@ -21,108 +19,151 @@
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
crane.url = "github:ipetkov/crane";
|
||||
|
||||
# This is used to provide a identical development shell at `shell.nix` for users that do not use flakes
|
||||
flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz";
|
||||
};
|
||||
|
||||
outputs = { nixpkgs, rust-overlay, flake-utils, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
outputs =
|
||||
inputs:
|
||||
inputs.flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs {
|
||||
inherit system overlays;
|
||||
info = {
|
||||
pname = "graphite";
|
||||
version = "unstable";
|
||||
src = pkgs.lib.cleanSourceWith {
|
||||
src = ./..;
|
||||
filter = path: type: !(type == "directory" && builtins.baseNameOf path == ".nix");
|
||||
};
|
||||
};
|
||||
|
||||
rustc-wasm = pkgs.rust-bin.stable.latest.default.override {
|
||||
targets = [ "wasm32-unknown-unknown" ];
|
||||
extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ];
|
||||
pkgs = import inputs.nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ (import inputs.rust-overlay) ];
|
||||
};
|
||||
|
||||
libcef = pkgs.libcef.overrideAttrs (finalAttrs: previousAttrs: {
|
||||
version = "139.0.17";
|
||||
gitRevision = "6c347eb";
|
||||
chromiumVersion = "139.0.7258.31";
|
||||
srcHash = "sha256-kRMO8DP4El1qytDsAZBdHvR9AAHXce90nPdyfJailBg=";
|
||||
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; };
|
||||
};
|
||||
|
||||
__intentionallyOverridingVersion = true;
|
||||
libs = rec {
|
||||
desktop = [
|
||||
pkgs.wayland
|
||||
pkgs.openssl
|
||||
pkgs.vulkan-loader
|
||||
pkgs.libraw
|
||||
pkgs.libGL
|
||||
];
|
||||
desktop-x11 = [
|
||||
pkgs.libxkbcommon
|
||||
pkgs.xorg.libXcursor
|
||||
pkgs.xorg.libxcb
|
||||
pkgs.xorg.libX11
|
||||
];
|
||||
desktop-all = desktop ++ desktop-x11;
|
||||
all = desktop-all;
|
||||
};
|
||||
|
||||
postInstall = ''
|
||||
strip $out/lib/*
|
||||
'';
|
||||
});
|
||||
tools = rec {
|
||||
desktop = [
|
||||
pkgs.pkg-config
|
||||
];
|
||||
frontend = [
|
||||
pkgs.lld
|
||||
pkgs.nodejs
|
||||
pkgs.nodePackages.npm
|
||||
pkgs.binaryen
|
||||
pkgs.wasm-bindgen-cli_0_2_100
|
||||
pkgs.wasm-pack
|
||||
pkgs.cargo-about
|
||||
];
|
||||
dev = [
|
||||
pkgs.rustc
|
||||
pkgs.cargo
|
||||
pkgs.rust-analyzer
|
||||
pkgs.clippy
|
||||
pkgs.rustfmt
|
||||
|
||||
libcefPath = pkgs.runCommand "libcef-path" {} ''
|
||||
mkdir -p $out
|
||||
pkgs.git
|
||||
|
||||
ln -s ${libcef}/include $out/include
|
||||
find ${libcef}/lib -type f -name "*" -exec ln -s {} $out/ \;
|
||||
find ${libcef}/libexec -type f -name "*" -exec ln -s {} $out/ \;
|
||||
cp -r ${libcef}/share/cef/* $out/
|
||||
pkgs.cargo-watch
|
||||
pkgs.cargo-nextest
|
||||
pkgs.cargo-expand
|
||||
|
||||
echo '${builtins.toJSON {
|
||||
type = "minimal";
|
||||
name = builtins.baseNameOf libcef.src.url;
|
||||
sha1 = "";
|
||||
}}' > $out/archive.json
|
||||
'';
|
||||
# Linker
|
||||
pkgs.mold
|
||||
|
||||
# Shared build inputs - system libraries that need to be in LD_LIBRARY_PATH
|
||||
buildInputs = with pkgs; [
|
||||
# System libraries
|
||||
wayland
|
||||
openssl
|
||||
vulkan-loader
|
||||
libraw
|
||||
libGL
|
||||
# Profiling tools
|
||||
pkgs.gnuplot
|
||||
pkgs.samply
|
||||
pkgs.cargo-flamegraph
|
||||
|
||||
# X11 libraries, not needed on wayland! Remove when x11 is finally dead
|
||||
libxkbcommon
|
||||
xorg.libXcursor
|
||||
xorg.libxcb
|
||||
xorg.libX11
|
||||
];
|
||||
|
||||
# Development tools that don't need to be in LD_LIBRARY_PATH
|
||||
buildTools = [
|
||||
rustc-wasm
|
||||
pkgs.nodejs
|
||||
pkgs.nodePackages.npm
|
||||
pkgs.binaryen
|
||||
pkgs.wasm-bindgen-cli
|
||||
pkgs.wasm-pack
|
||||
pkgs.pkg-config
|
||||
pkgs.git
|
||||
pkgs.cargo-about
|
||||
|
||||
# Linker
|
||||
pkgs.mold
|
||||
];
|
||||
# Development tools that don't need to be in LD_LIBRARY_PATH
|
||||
devTools = with pkgs; [
|
||||
cargo-watch
|
||||
cargo-nextest
|
||||
cargo-expand
|
||||
|
||||
# Profiling tools
|
||||
gnuplot
|
||||
samply
|
||||
cargo-flamegraph
|
||||
];
|
||||
# Plotting tools
|
||||
pkgs.graphviz
|
||||
];
|
||||
all = desktop ++ frontend ++ dev;
|
||||
};
|
||||
in
|
||||
{
|
||||
# Development shell configuration
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = buildInputs ++ buildTools ++ devTools;
|
||||
packages = rec {
|
||||
graphiteWithArgs =
|
||||
args:
|
||||
(import ./pkgs/graphite.nix {
|
||||
pkgs = pkgs // {
|
||||
inherit raster-nodes-shaders;
|
||||
};
|
||||
inherit
|
||||
info
|
||||
inputs
|
||||
deps
|
||||
libs
|
||||
tools
|
||||
;
|
||||
})
|
||||
args;
|
||||
graphite = graphiteWithArgs { };
|
||||
graphite-dev = graphiteWithArgs { dev = true; };
|
||||
graphite-without-resources = graphiteWithArgs { embeddedResources = false; };
|
||||
graphite-without-resources-dev = graphiteWithArgs {
|
||||
embeddedResources = false;
|
||||
dev = true;
|
||||
};
|
||||
graphite-bundle = import ./pkgs/graphite-bundle.nix {
|
||||
inherit pkgs graphite;
|
||||
};
|
||||
graphite-flatpak-manifest = import ./pkgs/graphite-flatpak-manifest.nix {
|
||||
inherit pkgs;
|
||||
archive = graphite-bundle.tar;
|
||||
};
|
||||
#TODO: graphene-cli = import ./pkgs/graphene-cli.nix { inherit info pkgs inputs deps libs tools; };
|
||||
raster-nodes-shaders = import ./pkgs/raster-nodes-shaders.nix {
|
||||
inherit
|
||||
info
|
||||
pkgs
|
||||
inputs
|
||||
deps
|
||||
libs
|
||||
tools
|
||||
;
|
||||
};
|
||||
|
||||
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath buildInputs}:${libcefPath}";
|
||||
CEF_PATH = libcefPath;
|
||||
XDG_DATA_DIRS="${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS";
|
||||
|
||||
shellHook = ''
|
||||
alias cargo='mold --run cargo'
|
||||
'';
|
||||
default = graphite;
|
||||
};
|
||||
|
||||
devShells.default = import ./dev.nix {
|
||||
inherit
|
||||
pkgs
|
||||
deps
|
||||
libs
|
||||
tools
|
||||
;
|
||||
};
|
||||
|
||||
formatter = pkgs.nixfmt-tree;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
91
.nix/pkgs/graphite-bundle.nix
Normal file
91
.nix/pkgs/graphite-bundle.nix
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
pkgs,
|
||||
graphite,
|
||||
}:
|
||||
let
|
||||
bundle =
|
||||
{
|
||||
pkgs,
|
||||
graphite,
|
||||
archive ? false,
|
||||
compression ? null,
|
||||
passthru ? {},
|
||||
}:
|
||||
(
|
||||
let
|
||||
tar = if compression == null then archive else true;
|
||||
nameArchiveSuffix = if tar then ".tar" else "";
|
||||
nameCompressionSuffix = if compression == null then "" else "." + compression;
|
||||
name = "graphite-bundle${nameArchiveSuffix}${nameCompressionSuffix}";
|
||||
build = ''
|
||||
mkdir -p out
|
||||
mkdir -p out/bin
|
||||
cp ${graphite}/bin/.graphite-wrapped out/bin/graphite
|
||||
chmod -v +w out/bin/graphite
|
||||
patchelf --set-rpath '$ORIGIN/../lib:$ORIGIN/../lib/cef' --set-interpreter '/lib64/ld-linux-x86-64.so.2' out/bin/graphite
|
||||
mkdir -p out/lib/cef
|
||||
mkdir -p ./cef
|
||||
tar -xvf ${pkgs.cef-binary.src} -C ./cef --strip-components=1
|
||||
cp -r ./cef/Release/* out/lib/cef/
|
||||
cp -r ./cef/Resources/* out/lib/cef/
|
||||
find "out/lib/cef/locales" -type f ! -name 'en-US*' -delete
|
||||
${pkgs.bintools}/bin/strip out/lib/cef/*.so*
|
||||
cp -r ${graphite}/share out/share
|
||||
'';
|
||||
install =
|
||||
if tar then
|
||||
''
|
||||
cd out
|
||||
tar -c \
|
||||
--sort=name \
|
||||
--mtime='@1' --clamp-mtime \
|
||||
--owner=0 --group=0 --numeric-owner \
|
||||
--mode='u=rwX,go=rX' \
|
||||
--format=posix \
|
||||
--pax-option=delete=atime,delete=ctime \
|
||||
--no-acls --no-xattrs --no-selinux \
|
||||
* ${
|
||||
if compression == "xz" then
|
||||
"| xz "
|
||||
else if compression == "gz" then
|
||||
"| gzip -n "
|
||||
else
|
||||
""
|
||||
}> $out
|
||||
''
|
||||
else
|
||||
''
|
||||
mkdir -p $out
|
||||
cp -r out/* $out/
|
||||
'';
|
||||
in
|
||||
|
||||
pkgs.runCommand name
|
||||
{
|
||||
inherit passthru;
|
||||
}
|
||||
''
|
||||
${build}
|
||||
${install}
|
||||
''
|
||||
);
|
||||
in
|
||||
bundle {
|
||||
inherit pkgs graphite;
|
||||
passthru = {
|
||||
tar = bundle {
|
||||
inherit pkgs graphite;
|
||||
archive = true;
|
||||
passthru = {
|
||||
gz = bundle {
|
||||
inherit pkgs graphite;
|
||||
compression = "gz";
|
||||
};
|
||||
xz = bundle {
|
||||
inherit pkgs graphite;
|
||||
compression = "xz";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
37
.nix/pkgs/graphite-flatpak-manifest.nix
Normal file
37
.nix/pkgs/graphite-flatpak-manifest.nix
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
pkgs,
|
||||
archive,
|
||||
}:
|
||||
|
||||
(pkgs.formats.json { }).generate "art.graphite.Graphite.json" {
|
||||
app-id = "art.graphite.Graphite";
|
||||
runtime = "org.freedesktop.Platform";
|
||||
runtime-version = "25.08";
|
||||
sdk = "org.freedesktop.Sdk";
|
||||
command = "graphite";
|
||||
finish-args = [
|
||||
"--device=dri"
|
||||
"--share=ipc"
|
||||
"--socket=wayland"
|
||||
"--socket=fallback-x11"
|
||||
"--share=network"
|
||||
];
|
||||
modules = [
|
||||
{
|
||||
name = "app";
|
||||
buildsystem = "simple";
|
||||
build-commands = [
|
||||
"mkdir -p /app"
|
||||
"cp -r ./* /app/"
|
||||
"chmod +x /app/bin/*"
|
||||
];
|
||||
sources = [
|
||||
{
|
||||
type = "archive";
|
||||
path = archive;
|
||||
strip-components = 0;
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
}
|
||||
146
.nix/pkgs/graphite.nix
Normal file
146
.nix/pkgs/graphite.nix
Normal file
@@ -0,0 +1,146 @@
|
||||
{
|
||||
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;
|
||||
|
||||
npmDeps = pkgs.importNpmLock {
|
||||
npmRoot = "${info.src}/frontend";
|
||||
};
|
||||
|
||||
npmRoot = "frontend";
|
||||
npmConfigScript = "setup";
|
||||
makeCacheWritable = true;
|
||||
|
||||
nativeBuildInputs = tools.frontend ++ [ pkgs.importNpmLock.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
|
||||
{ }
|
||||
) // {
|
||||
GRAPHITE_GIT_COMMIT_HASH = inputs.self.rev or "unknown";
|
||||
GRAPHITE_GIT_COMMIT_DATE = inputs.self.lastModified or "unknown";
|
||||
};
|
||||
|
||||
postUnpack = ''
|
||||
mkdir ./branding
|
||||
cp -r ${branding}/* ./branding
|
||||
'';
|
||||
|
||||
preBuild = if inputs.self ? rev then ''
|
||||
export GRAPHITE_GIT_COMMIT_DATE="$(date -u -d "@$GRAPHITE_GIT_COMMIT_DATE" +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
'' else "";
|
||||
|
||||
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/art.graphite.Graphite.svg
|
||||
mkdir -p $out/share/icons/hicolor/512x512/apps
|
||||
cp ${branding}/app-icons/graphite-512.png $out/share/icons/hicolor/512x512/apps/art.graphite.Graphite.png
|
||||
mkdir -p $out/share/icons/hicolor/256x256/apps
|
||||
cp ${branding}/app-icons/graphite-256.png $out/share/icons/hicolor/256x256/apps/art.graphite.Graphite.png
|
||||
mkdir -p $out/share/icons/hicolor/128x128/apps
|
||||
cp ${branding}/app-icons/graphite-128.png $out/share/icons/hicolor/128x128/apps/art.graphite.Graphite.png
|
||||
'';
|
||||
|
||||
postFixup = ''
|
||||
wrapProgram "$out/bin/graphite" \
|
||||
--prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath libs.desktop-all}:${deps.cef.env.CEF_PATH}" \
|
||||
--set CEF_PATH "${deps.cef.env.CEF_PATH}"
|
||||
'';
|
||||
}
|
||||
)
|
||||
36
.nix/pkgs/raster-nodes-shaders.nix
Normal file
36
.nix/pkgs/raster-nodes-shaders.nix
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
info,
|
||||
pkgs,
|
||||
inputs,
|
||||
deps,
|
||||
libs,
|
||||
tools,
|
||||
...
|
||||
}:
|
||||
|
||||
(deps.crane.lib.overrideToolchain (_: deps.rustGPU.toolchain)).buildPackage {
|
||||
pname = "raster-nodes-shaders";
|
||||
inherit (info) version src;
|
||||
|
||||
cargoVendorDir = deps.crane.lib.vendorMultipleCargoDeps {
|
||||
inherit (deps.crane.lib.findCargoFiles (deps.crane.lib.cleanCargoSource info.src)) cargoConfigs;
|
||||
cargoLockList = [
|
||||
"${info.src}/Cargo.lock"
|
||||
"${deps.rustGPU.toolchain.passthru.availableComponents.rust-src}/lib/rustlib/src/rust/library/Cargo.lock"
|
||||
];
|
||||
};
|
||||
|
||||
strictDeps = true;
|
||||
|
||||
env = deps.rustGPU.env;
|
||||
|
||||
buildPhase = ''
|
||||
cargo build -r -p raster-nodes-shaders
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
cp target/spirv-builder/spirv-unknown-naga-wgsl/release/deps/raster_nodes_shaders_entrypoint.wgsl $out
|
||||
'';
|
||||
|
||||
doCheck = false;
|
||||
}
|
||||
@@ -16,16 +16,13 @@
|
||||
# > nix-shell .nix --command "npm start"
|
||||
|
||||
# Uses flake compat to provide a development shell that is identical to the one defined in the flake
|
||||
(import
|
||||
(
|
||||
let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
nodeName = lock.nodes.root.inputs.flake-compat;
|
||||
in
|
||||
fetchTarball {
|
||||
url = lock.nodes.${nodeName}.locked.url;
|
||||
sha256 = lock.nodes.${nodeName}.locked.narHash;
|
||||
}
|
||||
)
|
||||
{ src = ./.; }
|
||||
).shellNix
|
||||
(import (
|
||||
let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
nodeName = lock.nodes.root.inputs.flake-compat;
|
||||
in
|
||||
fetchTarball {
|
||||
url = lock.nodes.${nodeName}.locked.url;
|
||||
sha256 = lock.nodes.${nodeName}.locked.narHash;
|
||||
}
|
||||
) { src = ./.; }).shellNix
|
||||
|
||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -11,10 +11,10 @@
|
||||
// Code quality
|
||||
"wayou.vscode-todo-highlight",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
// Helpful
|
||||
// Git
|
||||
"mhutchie.git-graph",
|
||||
"waderyan.gitblame",
|
||||
"qezhu.gitlink",
|
||||
// Helpful
|
||||
"wmaurer.change-case"
|
||||
]
|
||||
}
|
||||
|
||||
35
.vscode/settings.json
vendored
35
.vscode/settings.json
vendored
@@ -26,13 +26,22 @@
|
||||
// Configured in `.prettierrc`
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
// Website: don't format Zola/Tera-templated HTML on save
|
||||
"[html]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
// Handlebars: don't save on format
|
||||
// (`about.hbs` is used by Cargo About to encode license information)
|
||||
"[handlebars]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
// Rust Analyzer config
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
"rust-analyzer.cargo.allTargets": false,
|
||||
"rust-analyzer.procMacro.ignored": {
|
||||
"serde_derive": ["Serialize", "Deserialize"],
|
||||
"specta_macros": ["Type"] // Disabled because of: https://github.com/specta-rs/specta/issues/387
|
||||
},
|
||||
// ESLint config
|
||||
"eslint.format.enable": true,
|
||||
"eslint.workingDirectories": ["./frontend", "./website"],
|
||||
@@ -43,13 +52,35 @@
|
||||
"vite-plugin-svelte-css-no-scopable-elements": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"a11y-no-static-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"a11y-no-noninteractive-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"a11y-click-events-have-key-events": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"a11y-click-events-have-key-events": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"a11y_consider_explicit_label": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"a11y_click_events_have_key_events": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
"a11y_no_noninteractive_element_interactions": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
|
||||
},
|
||||
// Git Graph config
|
||||
"git-graph.repository.fetchAndPrune": true,
|
||||
"git-graph.repository.showRemoteHeads": false,
|
||||
"git-graph.repository.commits.fetchAvatars": true,
|
||||
// VS Code Git config
|
||||
"git.autofetch": true,
|
||||
"git.enableStatusBarSync": false,
|
||||
"git.showActionButton": {
|
||||
"sync": false
|
||||
},
|
||||
// CSpell config
|
||||
"cSpell.language": "en-US",
|
||||
"cSpell.logLevel": "Information",
|
||||
"cSpell.allowCompoundWords": true,
|
||||
// Other extensions config
|
||||
"evenBetterToml.formatter.alignComments": false,
|
||||
"package-json-upgrade.ignorePatterns": ["source-sans-pro"],
|
||||
// VS Code config
|
||||
"html.format.wrapLineLength": 200,
|
||||
"files.eol": "\n",
|
||||
"files.insertFinalNewline": true,
|
||||
"files.associations": {
|
||||
"*.graphite": "json"
|
||||
}
|
||||
},
|
||||
"editor.renderWhitespace": "boundary",
|
||||
"editor.minimap.markSectionHeaderRegex": "// ===+\\n\\s*//\\s*(?<label>[^\\n]{1,18})[^\\n]*(\\n\\s*//[^\\n]*)*\\n\\s*// ===+"
|
||||
}
|
||||
|
||||
3061
Cargo.lock
generated
3061
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
214
Cargo.toml
214
Cargo.toml
@@ -1,46 +1,95 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"editor",
|
||||
"desktop",
|
||||
"desktop/wrapper",
|
||||
"proc-macros",
|
||||
"desktop/embedded-resources",
|
||||
"desktop/bundle",
|
||||
"desktop/platform/linux",
|
||||
"desktop/platform/mac",
|
||||
"desktop/platform/win",
|
||||
"editor",
|
||||
"frontend/wasm",
|
||||
"node-graph/gapplication-io",
|
||||
"node-graph/gbrush",
|
||||
"node-graph/gcore",
|
||||
"node-graph/gcore-shaders",
|
||||
"node-graph/gstd",
|
||||
"node-graph/gmath-nodes",
|
||||
"node-graph/gpath-bool",
|
||||
"node-graph/graph-craft",
|
||||
"node-graph/graphene-cli",
|
||||
"node-graph/graster-nodes",
|
||||
"node-graph/gsvg-renderer",
|
||||
"node-graph/interpreted-executor",
|
||||
"node-graph/node-macro",
|
||||
"node-graph/preprocessor",
|
||||
"libraries/dyn-any",
|
||||
"libraries/path-bool",
|
||||
"libraries/math-parser",
|
||||
"node-graph/libraries/application-io",
|
||||
"node-graph/libraries/core-types",
|
||||
"node-graph/libraries/no-std-types",
|
||||
"node-graph/libraries/raster-types",
|
||||
"node-graph/libraries/vector-types",
|
||||
"node-graph/libraries/graphic-types",
|
||||
"node-graph/libraries/rendering",
|
||||
"node-graph/libraries/wgpu-executor",
|
||||
"node-graph/nodes/blending",
|
||||
"node-graph/nodes/brush",
|
||||
"node-graph/nodes/gcore",
|
||||
"node-graph/nodes/graphic",
|
||||
"node-graph/nodes/math",
|
||||
"node-graph/nodes/path-bool",
|
||||
"node-graph/nodes/raster",
|
||||
"node-graph/nodes/raster/shaders",
|
||||
"node-graph/nodes/raster/shaders/entrypoint",
|
||||
"node-graph/nodes/text",
|
||||
"node-graph/nodes/transform",
|
||||
"node-graph/nodes/vector",
|
||||
"node-graph/graph-craft",
|
||||
"node-graph/graphene-cli",
|
||||
"node-graph/nodes/gstd",
|
||||
"node-graph/interpreted-executor",
|
||||
"node-graph/node-macro",
|
||||
"node-graph/preprocessor",
|
||||
"proc-macros",
|
||||
"tools/crate-hierarchy-viz",
|
||||
"tools/editor-message-tree",
|
||||
"tools/node-docs",
|
||||
]
|
||||
default-members = [
|
||||
"editor",
|
||||
"frontend/wasm",
|
||||
"node-graph/gbrush",
|
||||
"node-graph/gcore",
|
||||
"node-graph/gcore-shaders",
|
||||
"node-graph/gstd",
|
||||
"node-graph/gmath-nodes",
|
||||
"node-graph/gpath-bool",
|
||||
"libraries/dyn-any",
|
||||
"libraries/path-bool",
|
||||
"libraries/math-parser",
|
||||
"node-graph/libraries/application-io",
|
||||
"node-graph/libraries/core-types",
|
||||
"node-graph/libraries/no-std-types",
|
||||
"node-graph/libraries/raster-types",
|
||||
"node-graph/libraries/vector-types",
|
||||
"node-graph/libraries/graphic-types",
|
||||
"node-graph/libraries/rendering",
|
||||
"node-graph/libraries/wgpu-executor",
|
||||
"node-graph/nodes/blending",
|
||||
"node-graph/nodes/brush",
|
||||
"node-graph/nodes/gcore",
|
||||
"node-graph/nodes/graphic",
|
||||
"node-graph/nodes/math",
|
||||
"node-graph/nodes/path-bool",
|
||||
"node-graph/nodes/raster",
|
||||
"node-graph/nodes/raster/shaders",
|
||||
"node-graph/nodes/text",
|
||||
"node-graph/nodes/transform",
|
||||
"node-graph/nodes/vector",
|
||||
"node-graph/graph-craft",
|
||||
"node-graph/graphene-cli",
|
||||
"node-graph/graster-nodes",
|
||||
"node-graph/gsvg-renderer",
|
||||
"node-graph/nodes/gstd",
|
||||
"node-graph/interpreted-executor",
|
||||
"node-graph/node-macro",
|
||||
"node-graph/preprocessor",
|
||||
# blocked by https://github.com/rust-lang/cargo/issues/15890
|
||||
# "proc-macros",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.88"
|
||||
edition = "2024"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
homepage = "https://graphite.art"
|
||||
repository = "https://github.com/GraphiteEditor/Graphite"
|
||||
license = "Apache-2.0"
|
||||
version = "0.0.0"
|
||||
readme = "README.md"
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
# Local dependencies
|
||||
dyn-any = { path = "libraries/dyn-any", features = [
|
||||
@@ -53,24 +102,33 @@ dyn-any = { path = "libraries/dyn-any", features = [
|
||||
preprocessor = { path = "node-graph/preprocessor" }
|
||||
math-parser = { path = "libraries/math-parser" }
|
||||
path-bool = { path = "libraries/path-bool" }
|
||||
graphene-application-io = { path = "node-graph/gapplication-io" }
|
||||
graphene-brush = { path = "node-graph/gbrush" }
|
||||
graphene-core = { path = "node-graph/gcore" }
|
||||
graphene-core-shaders = { path = "node-graph/gcore-shaders" }
|
||||
graphene-math-nodes = { path = "node-graph/gmath-nodes" }
|
||||
graphene-path-bool = { path = "node-graph/gpath-bool" }
|
||||
graphene-application-io = { path = "node-graph/libraries/application-io" }
|
||||
core-types = { path = "node-graph/libraries/core-types" }
|
||||
no-std-types = { path = "node-graph/libraries/no-std-types" }
|
||||
raster-types = { path = "node-graph/libraries/raster-types" }
|
||||
vector-types = { path = "node-graph/libraries/vector-types" }
|
||||
graphic-types = { path = "node-graph/libraries/graphic-types" }
|
||||
rendering = { path = "node-graph/libraries/rendering" }
|
||||
brush-nodes = { path = "node-graph/nodes/brush" }
|
||||
blending-nodes = { path = "node-graph/nodes/blending" }
|
||||
graphene-core = { path = "node-graph/nodes/gcore" }
|
||||
graphic-nodes = { path = "node-graph/nodes/graphic" }
|
||||
text-nodes = { path = "node-graph/nodes/text" }
|
||||
transform-nodes = { path = "node-graph/nodes/transform" }
|
||||
vector-nodes = { path = "node-graph/nodes/vector" }
|
||||
math-nodes = { path = "node-graph/nodes/math" }
|
||||
path-bool-nodes = { path = "node-graph/nodes/path-bool" }
|
||||
graph-craft = { path = "node-graph/graph-craft" }
|
||||
graphene-raster-nodes = { path = "node-graph/graster-nodes" }
|
||||
graphene-std = { path = "node-graph/gstd" }
|
||||
graphene-svg-renderer = { path = "node-graph/gsvg-renderer" }
|
||||
raster-nodes = { path = "node-graph/nodes/raster" }
|
||||
graphene-std = { path = "node-graph/nodes/gstd" }
|
||||
interpreted-executor = { path = "node-graph/interpreted-executor" }
|
||||
node-macro = { path = "node-graph/node-macro" }
|
||||
wgpu-executor = { path = "node-graph/wgpu-executor" }
|
||||
wgpu-executor = { path = "node-graph/libraries/wgpu-executor" }
|
||||
graphite-proc-macros = { path = "proc-macros" }
|
||||
|
||||
# Workspace dependencies
|
||||
rustc-hash = "2.0"
|
||||
bytemuck = { version = "1.13", features = ["derive"] }
|
||||
bytemuck = { version = "1.13", features = ["derive", "min_const_generics"] }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_json = "1.0"
|
||||
serde-wasm-bindgen = "0.6"
|
||||
@@ -80,22 +138,23 @@ env_logger = "0.11"
|
||||
log = "0.4"
|
||||
bitflags = { version = "2.4", features = ["serde"] }
|
||||
ctor = "0.2"
|
||||
convert_case = "0.7"
|
||||
convert_case = "0.8"
|
||||
indoc = "2.0.5"
|
||||
derivative = "2.2"
|
||||
thiserror = "2"
|
||||
anyhow = "1.0"
|
||||
proc-macro2 = { version = "1", features = ["span-locations"] }
|
||||
quote = "1.0"
|
||||
chrono = "0.4"
|
||||
ron = "0.8"
|
||||
ron = "0.11"
|
||||
fastnoise-lite = "1.1"
|
||||
wgpu = { version = "25.0.2", features = [
|
||||
wgpu = { version = "27.0", features = [
|
||||
# We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm
|
||||
"fragile-send-sync-non-atomic-wasm",
|
||||
"spirv",
|
||||
"strict_asserts",
|
||||
] }
|
||||
once_cell = "1.13" # Remove when `core::cell::LazyCell` (<https://doc.rust-lang.org/core/cell/struct.LazyCell.html>) is stabilized in Rust 1.80 and we bump our MSRV
|
||||
once_cell = "1.13" # Remove and replace with `core::cell::LazyCell` (<https://doc.rust-lang.org/core/cell/struct.LazyCell.html>)
|
||||
wasm-bindgen = "=0.2.100" # NOTICE: ensure this stays in sync with the `wasm-bindgen-cli` version in `website/content/volunteer/guide/project-setup/_index.md`. We pin this version because wasm-bindgen upgrades may break various things.
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "=0.3.77"
|
||||
@@ -117,18 +176,26 @@ web-sys = { version = "=0.3.77", features = [
|
||||
"HtmlImageElement",
|
||||
"ImageBitmapRenderingContext",
|
||||
] }
|
||||
winit = { version = "0.30", features = ["wayland", "rwh_06"] }
|
||||
winit = { git = "https://github.com/rust-windowing/winit.git" }
|
||||
keyboard-types = "0.8"
|
||||
url = "2.5"
|
||||
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt"] }
|
||||
vello = { git = "https://github.com/linebender/vello.git" } # TODO switch back to stable when a release is made
|
||||
resvg = "0.44"
|
||||
usvg = "0.44"
|
||||
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt", "rt-multi-thread"] }
|
||||
# Linebender ecosystem (BEGIN)
|
||||
kurbo = { version = "0.12", features = ["serde"] }
|
||||
vello = { git = "https://github.com/linebender/vello" }
|
||||
vello_encoding = { git = "https://github.com/linebender/vello" }
|
||||
resvg = "0.45"
|
||||
usvg = "0.45"
|
||||
parley = "0.6"
|
||||
skrifa = "0.36"
|
||||
polycool = "0.4"
|
||||
# Linebender ecosystem (END)
|
||||
rand = { version = "0.9", default-features = false, features = ["std_rng"] }
|
||||
rand_chacha = "0.9"
|
||||
glam = { version = "0.29", default-features = false, features = [
|
||||
"serde",
|
||||
"nostd-libm",
|
||||
"scalar-math",
|
||||
"debug-glam-assert",
|
||||
"bytemuck",
|
||||
] }
|
||||
base64 = "0.22"
|
||||
image = { version = "0.25", default-features = false, features = [
|
||||
@@ -136,13 +203,11 @@ image = { version = "0.25", default-features = false, features = [
|
||||
"jpeg",
|
||||
"bmp",
|
||||
] }
|
||||
parley = "0.5.0"
|
||||
skrifa = "0.32.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
pretty_assertions = "1.4"
|
||||
fern = { version = "0.7", features = ["colored"] }
|
||||
num_enum = "0.7"
|
||||
num_enum = { version = "0.7", default-features = false }
|
||||
num-derive = "0.4"
|
||||
num-traits = { version = "0.2", default-features = false, features = ["i128"] }
|
||||
num-traits = { version = "0.2", default-features = false, features = ["libm"] }
|
||||
specta = { version = "2.0.0-rc.22", features = [
|
||||
"glam",
|
||||
"derive",
|
||||
@@ -159,34 +224,36 @@ syn = { version = "2.0", default-features = false, features = [
|
||||
"extra-traits",
|
||||
"proc-macro",
|
||||
] }
|
||||
kurbo = { version = "0.11.0", features = ["serde"] }
|
||||
lyon_geom = "1.0"
|
||||
petgraph = { version = "0.7.1", default-features = false, features = [
|
||||
"graphmap",
|
||||
] }
|
||||
half = { version = "2.4.1", default-features = false, features = ["bytemuck"] }
|
||||
petgraph = { version = "0.7", default-features = false, features = ["graphmap"] }
|
||||
half = { version = "2.4", default-features = false, features = ["bytemuck"] }
|
||||
tinyvec = { version = "1", features = ["std"] }
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
iai-callgrind = { version = "0.12.3" }
|
||||
ndarray = "0.16.1"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
criterion = { version = "0.7", features = ["html_reports"] }
|
||||
iai-callgrind = { version = "0.16" }
|
||||
ndarray = "0.16"
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
dirs = "6.0"
|
||||
cef = "139.0.1"
|
||||
include_dir = "0.7.4"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
tracing = "0.1.41"
|
||||
rfd = "0.15.4"
|
||||
open = "5.3.2"
|
||||
poly-cool = "0.2.0"
|
||||
cef = "142"
|
||||
cef-dll-sys = "142"
|
||||
include_dir = "0.7"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing = "0.1"
|
||||
rfd = "0.15"
|
||||
open = "5.3"
|
||||
spin = "0.10"
|
||||
clap = "4.5"
|
||||
spirv-std = { git = "https://github.com/Firestar99/rust-gpu-new", rev = "c12f216121820580731440ee79ebc7403d6ea04f", features = ["bytemuck"] }
|
||||
cargo-gpu = { git = "https://github.com/Firestar99/cargo-gpu", rev = "3952a22d16edbd38689f3a876e417899f21e1fe7", default-features = false }
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(target_arch, values("spirv"))'] }
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
[profile.dev.package]
|
||||
graphite-editor = { opt-level = 1 }
|
||||
graphene-core-shaders = { opt-level = 1 }
|
||||
graphene-core = { opt-level = 1 }
|
||||
graphene-std = { opt-level = 1 }
|
||||
no-std-types = { opt-level = 1 }
|
||||
core-types= { opt-level = 1 }
|
||||
interpreted-executor = { opt-level = 1 } # This is a mitigation for https://github.com/rustwasm/wasm-pack/issues/981 which is needed because the node_registry function is too large
|
||||
graphite-proc-macros = { opt-level = 1 }
|
||||
image = { opt-level = 2 }
|
||||
@@ -194,6 +261,7 @@ rustc-hash = { opt-level = 3 }
|
||||
serde_derive = { opt-level = 1 }
|
||||
specta-macros = { opt-level = 1 }
|
||||
syn = { opt-level = 1 }
|
||||
node-macro = { opt-level = 2 }
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
@@ -202,3 +270,7 @@ debug = true
|
||||
[profile.profiling]
|
||||
inherits = "release"
|
||||
debug = true
|
||||
|
||||
[patch.crates-io]
|
||||
# Force cargo to use only one version of the dpi crate (vendoring breaks without this)
|
||||
dpi = { git = "https://github.com/rust-windowing/winit.git" }
|
||||
|
||||
14
README.md
14
README.md
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
<a href="https://graphite.rs/">
|
||||
<a href="https://graphite.art/">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/9366c148-4405-484f-909a-9a3526eb9209">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/791508ab-bcd5-4e31-a3b9-1187cfd7a2f6">
|
||||
@@ -10,14 +10,14 @@
|
||||
|
||||
# Your procedural toolbox for 2D content creation
|
||||
|
||||
**Graphite is a free, open source vector and raster graphics engine, [available now](https://editor.graphite.rs) in alpha. Get creative with a fully nondestructive editing workflow that combines layer-based compositing with node-based generative design.**
|
||||
**Graphite is a free, open source vector and raster graphics engine, [available now](https://editor.graphite.art) in alpha. Get creative with a fully nondestructive editing workflow that combines layer-based compositing with node-based generative design.**
|
||||
|
||||
Having begun life as a vector editor, Graphite continues evolving into a generalized, all-in-one graphics toolbox that's built more like a game engine than a conventional creative app. The editor's tools wrap its node graph core, providing user-friendly workflows for vector, raster, and beyond. Photo editing, motion graphics, digital painting, desktop publishing, and VFX compositing are additional competencies on the planned [roadmap](https://graphite.rs/features/#roadmap) making Graphite into a highly versatile content creation tool.
|
||||
Having begun life as a vector editor, Graphite continues evolving into a generalized, all-in-one graphics toolbox that's built more like a game engine than a conventional creative app. The editor's tools wrap its node graph core, providing user-friendly workflows for vector, raster, and beyond. Photo editing, motion graphics, digital painting, desktop publishing, and VFX compositing are additional competencies on the planned [roadmap](https://graphite.art/features/#roadmap) making Graphite into a highly versatile content creation tool.
|
||||
|
||||
Learn more from the [website](https://graphite.rs/), subscribe to the [newsletter](https://graphite.rs/#newsletter), consider [volunteering](https://graphite.rs/volunteer/) or [donating](https://graphite.rs/donate/), and remember to give this repository a ⭐!
|
||||
Learn more from the [website](https://graphite.art/), subscribe to the [newsletter](https://graphite.art/#newsletter), consider [volunteering](https://graphite.art/volunteer/) or [donating](https://graphite.art/donate/), and remember to give this repository a ⭐!
|
||||
|
||||
<br />
|
||||
<a href="https://discord.graphite.rs/">
|
||||
<a href="https://discord.graphite.art/">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/ad185fac-3b48-446d-863c-2bcb0724abee">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/aa23f503-f3bf-444a-9080-8eaa19fa2fa8">
|
||||
@@ -62,7 +62,7 @@ https://github.com/user-attachments/assets/f4604aea-e8f1-45ce-9218-46ddc666f11d
|
||||
|
||||
## Support our mission ❤️
|
||||
|
||||
Graphite is 100% community built and funded. Please become a part of keeping the project alive and thriving with a [donation](https://graphite.rs/donate/) if you share a belief in our **mission**:
|
||||
Graphite is 100% community built and funded. Please become a part of keeping the project alive and thriving with a [donation](https://graphite.art/donate/) if you share a belief in our **mission**:
|
||||
|
||||
> Graphite strives to unshackle the creativity of every budding artist and seasoned professional by building the best comprehensive art and design tool that's accessible to all.
|
||||
>
|
||||
@@ -78,6 +78,6 @@ Graphite is 100% community built and funded. Please become a part of keeping the
|
||||
|
||||
## Contributing/building the code
|
||||
|
||||
Are you a graphics programmer or Rust developer? Graphite aims to be one of the most approachable projects for putting your engineering skills to use in the world of open source. See [instructions here](https://graphite.rs/volunteer/guide/) for setting up the project and getting started.
|
||||
Are you a graphics programmer or Rust developer? Graphite aims to be one of the most approachable projects for putting your engineering skills to use in the world of open source. See [instructions here](https://graphite.art/volunteer/guide/) for setting up the project and getting started.
|
||||
|
||||
*By submitting code for inclusion in the project, you are agreeing to license your changes under the Apache 2.0 license, and that you have the authority to do so. Some directories may have other licenses, like dual-licensed MIT/Apache 2.0, and code submissions to those directories mean you agree to the applicable license(s).*
|
||||
|
||||
2
demo-artwork/changing-seasons.graphite
generated
2
demo-artwork/changing-seasons.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/isometric-fountain.graphite
generated
2
demo-artwork/isometric-fountain.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/marbled-mandelbrot.graphite
generated
2
demo-artwork/marbled-mandelbrot.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/painted-dreams.graphite
generated
2
demo-artwork/painted-dreams.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/parametric-dunescape.graphite
generated
2
demo-artwork/parametric-dunescape.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/procedural-string-lights.graphite
generated
2
demo-artwork/procedural-string-lights.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/red-dress.graphite
generated
2
demo-artwork/red-dress.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/valley-of-spires.graphite
generated
2
demo-artwork/valley-of-spires.graphite
generated
File diff suppressed because one or more lines are too long
@@ -39,11 +39,7 @@ db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
"RUSTSEC-2024-0370", # Unmaintained but still fully functional crate `proc-macro-error`
|
||||
"RUSTSEC-2024-0388", # Unmaintained but still fully functional crate `derivative`
|
||||
"RUSTSEC-2025-0007", # Unmaintained but still fully functional crate `ring`
|
||||
"RUSTSEC-2024-0436", # Unmaintained but still fully functional crate `paste`
|
||||
"RUSTSEC-2025-0014", # Unmaintained but still fully functional crate `humantime`
|
||||
]
|
||||
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
||||
# lower than the range specified will be ignored. Note that ignored advisories
|
||||
|
||||
@@ -2,62 +2,73 @@
|
||||
name = "graphite-desktop"
|
||||
version = "0.1.0"
|
||||
description = "Graphite Desktop"
|
||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
license = "Apache-2.0"
|
||||
repository = ""
|
||||
edition = "2024"
|
||||
rust-version = "1.87"
|
||||
|
||||
[features]
|
||||
default = ["gpu", "accelerated_paint"]
|
||||
gpu = ["graphite-desktop-wrapper/gpu"]
|
||||
[[bin]]
|
||||
name = "graphite"
|
||||
path = "src/main.rs"
|
||||
|
||||
# Hardware acceleration features
|
||||
accelerated_paint = ["accelerated_paint_dmabuf", "accelerated_paint_d3d11", "accelerated_paint_iosurface"]
|
||||
accelerated_paint_dmabuf = ["libc", "ash"]
|
||||
accelerated_paint_d3d11 = ["windows", "ash"]
|
||||
accelerated_paint_iosurface = ["objc2-io-surface", "objc2-metal", "core-foundation"]
|
||||
[features]
|
||||
default = ["recommended", "embedded_resources"]
|
||||
recommended = ["gpu", "accelerated_paint"]
|
||||
embedded_resources = ["dep:graphite-desktop-embedded-resources"]
|
||||
gpu = ["graphite-desktop-wrapper/gpu"]
|
||||
accelerated_paint = ["cef/accelerated_osr"]
|
||||
|
||||
[dependencies]
|
||||
# # Local dependencies
|
||||
# Local dependencies
|
||||
graphite-desktop-wrapper = { path = "wrapper" }
|
||||
graphite-desktop-embedded-resources = { path = "embedded-resources", optional = true }
|
||||
|
||||
wgpu = { workspace = true }
|
||||
winit = { workspace = true, features = ["serde"] }
|
||||
winit = { workspace = true, features = [
|
||||
"wayland-csd-adwaita-notitlebar",
|
||||
"serde",
|
||||
] }
|
||||
thiserror = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
cef = { workspace = true }
|
||||
include_dir = { workspace = true }
|
||||
cef-dll-sys = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
ron = { workspace = true}
|
||||
ron = { workspace = true }
|
||||
bytemuck = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
vello = { workspace = true }
|
||||
derivative = { workspace = true }
|
||||
rfd = { workspace = true }
|
||||
open = { workspace = true }
|
||||
|
||||
# Hardware acceleration dependencies
|
||||
ash = { version = "0.38", optional = true }
|
||||
rand = { workspace = true, features = ["thread_rng"] }
|
||||
serde = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
fd-lock = "4.0.4"
|
||||
ctrlc = "3.5.1"
|
||||
window_clipboard = "0.5"
|
||||
|
||||
# Windows-specific dependencies
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Graphics_Direct3D11",
|
||||
"Win32_Graphics_Direct3D12",
|
||||
"Win32_Graphics_Dxgi",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_Foundation"
|
||||
], optional = true }
|
||||
[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_System_Console",
|
||||
"Win32_UI_Controls",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_Shell",
|
||||
] }
|
||||
|
||||
# macOS-specific dependencies
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2-io-surface = { version = "0.3", optional = true }
|
||||
objc2-metal = { version = "0.3", optional = true }
|
||||
core-foundation = { version = "0.9", optional = true }
|
||||
|
||||
# Linux-specific dependencies
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc = { version = "0.2", optional = true }
|
||||
objc2 = { version = "0.6.1", default-features = false }
|
||||
objc2-foundation = { version = "0.3.2", default-features = false }
|
||||
objc2-app-kit = { version = "0.3.2", default-features = false }
|
||||
muda = { git = "https://github.com/timon-schelling/muda.git", rev = "e5bc28bbd6781b18afbfc237981f9ef47eddf863", default-features = false }
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
Name=Graphite
|
||||
GenericName=Vector & Raster Graphics Editor
|
||||
Comment=Open-source vector & raster graphics editor. Featuring node based procedural nondestructive editing workflow.
|
||||
Exec=graphite-editor
|
||||
Exec=graphite
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=graphite-icon-color
|
||||
Icon=art.graphite.Graphite
|
||||
Categories=Graphics;VectorGraphics;RasterGraphics;
|
||||
Keywords=graphite;editor;vector;raster;procedural;design;
|
||||
StartupWMClass=rs.graphite.GraphiteEditor
|
||||
StartupWMClass=art.graphite.Graphite
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 84 KiB |
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024">
|
||||
<path fill="#ffffff" d="M.0007 659.5456c.0027 11.3936.0161 22.786.0834 34.1805.069 11.996.207 23.99.533 35.983.708 26.133 2.246 52.494 6.892 78.337 4.712 26.218 12.404 50.618 24.528 74.438 11.918 23.413 27.489 44.837 46.066 63.413 18.576 18.577 40.001 34.149 63.413 46.066 23.82 12.125 48.22 19.816 74.438 24.529 25.843 4.645 52.204 6.183 78.337 6.892 11.993.325 23.987.463 35.983.532 14.243.084 28.483.084 42.726.084h278c14.243 0 28.483 0 42.726-.084 11.996-.069 23.99-.207 35.983-.532 26.134-.709 52.494-2.247 78.338-6.892 26.217-4.713 50.617-12.404 74.437-24.529 23.413-11.917 44.837-27.489 63.414-46.066 18.576-18.576 34.148-40 46.065-63.413 12.125-23.82 19.816-48.22 24.529-74.438 4.645-25.843 6.183-52.204 6.892-78.337.325-11.993.463-23.987.532-35.983.084-14.243.084-28.483.084-42.726V373c0-14.243 0-28.483-.084-42.726-.069-11.996-.207-23.99-.532-35.983-.709-26.133-2.247-52.494-6.892-78.337-4.713-26.218-12.404-50.618-24.529-74.438-11.917-23.412-27.489-44.837-46.065-63.413-18.577-18.577-40.001-34.149-63.414-46.066-23.82-12.124-48.22-19.816-74.437-24.528-25.844-4.646-52.204-6.184-78.338-6.893-11.993-.325-23.987-.463-35.983-.532C679.483 0 665.243 0 651 0c0 0-275.1519-.002-286.5455.0007-11.3936.0027-22.7861.0161-34.1805.0834-11.996.069-23.99.207-35.983.532-26.133.709-52.494 2.247-78.337 6.893-26.218 4.712-50.618 12.404-74.438 24.528-23.412 11.917-44.837 27.489-63.413 46.066-18.577 18.576-34.148 40.001-46.066 63.413-12.124 23.82-19.816 48.22-24.528 74.438-4.646 25.843-6.184 52.204-6.892 78.337-.326 11.993-.464 23.987-.533 35.983C0 344.517 0 358.757 0 373"/>
|
||||
<path fill="#f1decd" d="m789.9503 428.9507-135.005-233.833c-5.642-8.618-15.035-14.043-25.327-14.632h-270.01c-10.292.589-19.685 6.014-25.327 14.632l-135.036 233.833c-4.619 9.207-4.619 20.026 0 29.233l135.036 233.864c5.642 8.618 15.035 14.043 25.327 14.601h270.01c10.292-.558 19.685-5.983 25.327-14.601l135.036-233.864c4.588-9.207 4.588-20.026-.031-29.233Z"/>
|
||||
<path fill="#3ea8ff" d="m693.8813 243.5087-42.346-73.315h-235.879l200.818 346.301 176.979-100.502-99.572-172.484Z"/>
|
||||
<path fill="#2180ce" d="m552.5523 325.0697-89.373-154.876h-121.148l-37.355 51.832 106.609 184.605 95.604 165.664 142.383-79.732-96.72-167.493Z"/>
|
||||
<path fill="#deba92" d="m800.4283 428.0827-166.532 81.282 205.53 312.542-38.998-393.824Z"/>
|
||||
<path fill="#d49b64" d="m653.4263 499.7547-141.298 81.592 290.098 176.111-148.8-257.703Z"/>
|
||||
<path fill="#473a3a" d="m870.2712 818.8377-.217-2.139c-2.666-29.109-34.565-376.278-34.658-377.766-.713-8.804-3.317-17.36-7.595-25.079-.093-.279-.217-.527-.341-.775l-.186-.465-.093.093-.062-.031.124-.062-36.58-63.364-102.889-178.25c-11.098-19.189-31.558-31-53.723-31h-278.969c-22.134 0-42.594 11.811-53.692 31l-139.5 241.583c-11.067 19.189-11.067 42.811 0 62l139.5 241.583c11.067 19.189 31.527 31 53.692 31h278.969c11.966-.093 23.653-3.689 33.635-10.292l119.629 85.808c-21.607-6.758-43.958-10.757-66.557-11.873-56.141-2.697-220.72-.868-407.402 14.353-76.601 6.231-112.809 24.428-108.593 27.993 10.819 9.238 23.622 12.896 87.42 12.648 56.792-.186 222.115 5.921 272.056 8.99 35.371 2.201 72.571 8.928 99.324 9.207 28.52-.372 56.947-2.542 85.188-6.51 42.594-5.859 99.076-17.67 112.065-33.387 6.851-6.51 10.354-15.841 9.455-25.265Zm-522.939-603.446c5.177-7.905 13.795-12.896 23.25-13.423h247.969c9.424.527 18.011 5.487 23.219 13.361l103.106 178.591c-15.469 16.12-28.892 34.1-39.959 53.506l-76.7869 8.525-45.787 62.248c-22.351-.124-44.64 2.542-66.34 7.874l-174.003-301.413 5.332-9.269Zm23.25 469.774c-9.455-.527-18.073-5.518-23.25-13.423l-124-214.737c-4.247-8.432-4.247-18.414 0-26.846l82.863-143.468 184.76 320.044.124-.062c2.139 3.844 5.084 7.161 8.649 9.765l95.883 68.758-225.029-.031Zm361.956 21.917-179.49-128.743c19.809-4.309 40.021-6.479 60.295-6.479l45.7871-62.217 76.787-8.525c10.106-17.546 22.103-33.945 35.712-48.949l21.793 219.79c-25.792-2.759-50.406 11.439-60.884 35.123Zm-325.841-87.482c4.619 7.223 2.48 16.802-4.712 21.421-7.223 4.619-16.802 2.48-21.421-4.712-.248-.372-.465-.775-.682-1.178l-108.5-187.891c-4.619-7.223-2.48-16.802 4.712-21.421 7.223-4.619 16.802-2.48 21.421 4.712.248.372.465.775.682 1.178l108.5 187.891Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.1 KiB |
16
desktop/bundle/Cargo.toml
Normal file
16
desktop/bundle/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "graphite-desktop-bundle"
|
||||
version = "0.0.0"
|
||||
description = "Graphite Desktop Bundle"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
license = "Apache-2.0"
|
||||
repository = ""
|
||||
edition = "2024"
|
||||
rust-version = "1.87"
|
||||
|
||||
[dependencies]
|
||||
cef-dll-sys = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
serde = { workspace = true }
|
||||
plist = { version = "*" }
|
||||
10
desktop/bundle/build.rs
Normal file
10
desktop/bundle/build.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-env-changed=CARGO_PROFILE");
|
||||
println!("cargo:rerun-if-env-changed=PROFILE");
|
||||
let profile = std::env::var("CARGO_PROFILE").or_else(|_| std::env::var("PROFILE")).unwrap();
|
||||
println!("cargo:rustc-env=CARGO_PROFILE={profile}");
|
||||
|
||||
println!("cargo:rerun-if-env-changed=DEP_CEF_DLL_WRAPPER_CEF_DIR");
|
||||
let cef_dir = std::env::var("DEP_CEF_DLL_WRAPPER_CEF_DIR").unwrap();
|
||||
println!("cargo:rustc-env=CEF_PATH={cef_dir}");
|
||||
}
|
||||
71
desktop/bundle/src/common.rs
Normal file
71
desktop/bundle/src/common.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
pub(crate) const APP_NAME: &str = "Graphite";
|
||||
pub(crate) const APP_BIN: &str = "graphite";
|
||||
|
||||
pub(crate) fn workspace_path() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_WORKSPACE_DIR"))
|
||||
}
|
||||
|
||||
fn profile_name() -> &'static str {
|
||||
let mut profile = env!("CARGO_PROFILE");
|
||||
if profile == "debug" {
|
||||
profile = "dev";
|
||||
}
|
||||
profile
|
||||
}
|
||||
|
||||
pub(crate) fn profile_path() -> PathBuf {
|
||||
workspace_path().join(format!("target/{}", env!("CARGO_PROFILE")))
|
||||
}
|
||||
|
||||
pub(crate) fn cef_path() -> PathBuf {
|
||||
PathBuf::from(env!("CEF_PATH"))
|
||||
}
|
||||
|
||||
pub(crate) fn build_bin(package: &str, bin: Option<&str>) -> Result<PathBuf, Box<dyn Error>> {
|
||||
let profile = &profile_name();
|
||||
let mut args = vec!["build", "--package", package, "--profile", profile];
|
||||
if let Some(bin) = bin {
|
||||
args.push("--bin");
|
||||
args.push(bin);
|
||||
}
|
||||
run_command("cargo", &args)?;
|
||||
let profile_path = profile_path();
|
||||
let mut bin_path = if let Some(bin) = bin { profile_path.join(bin) } else { profile_path.join(APP_BIN) };
|
||||
if cfg!(target_os = "windows") {
|
||||
bin_path.set_extension("exe");
|
||||
}
|
||||
Ok(bin_path)
|
||||
}
|
||||
|
||||
pub(crate) fn run_command(program: &str, args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let status = Command::new(program).args(args).stdout(Stdio::inherit()).stderr(Stdio::inherit()).status()?;
|
||||
if !status.success() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn clean_dir(dir: &Path) {
|
||||
if dir.exists() {
|
||||
fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
fs::create_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
pub(crate) fn copy_dir(src: &Path, dst: &Path) {
|
||||
fs::create_dir_all(dst).unwrap();
|
||||
for entry in fs::read_dir(src).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let dst_path = dst.join(entry.file_name());
|
||||
if entry.file_type().unwrap().is_dir() {
|
||||
copy_dir(&entry.path(), &dst_path);
|
||||
} else {
|
||||
fs::copy(entry.path(), &dst_path).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
21
desktop/bundle/src/linux.rs
Normal file
21
desktop/bundle/src/linux.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::error::Error;
|
||||
|
||||
use crate::common::*;
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
let app_bin = build_bin("graphite-desktop-platform-linux", None)?;
|
||||
|
||||
// TODO: Implement bundling for linux
|
||||
|
||||
// TODO: Consider adding more useful cli
|
||||
if std::env::args().any(|a| a == "open") {
|
||||
run_command(&app_bin.to_string_lossy(), &[]).expect("failed to open app");
|
||||
} else {
|
||||
println!("Binary built and placed at {}", app_bin.to_string_lossy());
|
||||
eprintln!("Bundling for Linux is not yet implemented.");
|
||||
eprintln!("You can still start the app with the `open` subcommand. `cargo run -p graphite-desktop-bundle -- open`");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
127
desktop/bundle/src/mac.rs
Normal file
127
desktop/bundle/src/mac.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::common::*;
|
||||
|
||||
const APP_ID: &str = "art.graphite.Graphite";
|
||||
|
||||
const ICONS_FILE_NAME: &str = "graphite.icns";
|
||||
|
||||
const EXEC_PATH: &str = "Contents/MacOS";
|
||||
const FRAMEWORKS_PATH: &str = "Contents/Frameworks";
|
||||
const RESOURCES_PATH: &str = "Contents/Resources";
|
||||
const CEF_FRAMEWORK: &str = "Chromium Embedded Framework.framework";
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
let app_bin = build_bin("graphite-desktop-platform-mac", None)?;
|
||||
let helper_bin = build_bin("graphite-desktop-platform-mac", Some("helper"))?;
|
||||
|
||||
let profile_path = profile_path();
|
||||
let app_dir = bundle(&profile_path, &app_bin, &helper_bin);
|
||||
|
||||
// TODO: Consider adding more useful cli
|
||||
if std::env::args().any(|a| a == "open") {
|
||||
let executable_path = app_dir.join(EXEC_PATH).join(APP_NAME);
|
||||
run_command(&executable_path.to_string_lossy(), &[]).expect("failed to open app");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bundle(out_dir: &Path, app_bin: &Path, helper_bin: &Path) -> PathBuf {
|
||||
let app_dir = out_dir.join(APP_NAME).with_extension("app");
|
||||
|
||||
clean_dir(&app_dir);
|
||||
|
||||
create_app(&app_dir, APP_ID, APP_NAME, app_bin, false);
|
||||
|
||||
for helper_type in [None, Some("GPU"), Some("Renderer")] {
|
||||
let helper_id_suffix = helper_type.map(|t| format!(".{t}")).unwrap_or_default();
|
||||
let helper_id = format!("{APP_ID}.helper{helper_id_suffix}");
|
||||
let helper_name_suffix = helper_type.map(|t| format!(" ({t})")).unwrap_or_default();
|
||||
let helper_name = format!("{APP_NAME} Helper{helper_name_suffix}");
|
||||
let helper_app_dir = app_dir.join(FRAMEWORKS_PATH).join(&helper_name).with_extension("app");
|
||||
create_app(&helper_app_dir, &helper_id, &helper_name, helper_bin, true);
|
||||
}
|
||||
|
||||
copy_dir(&cef_path().join(CEF_FRAMEWORK), &app_dir.join(FRAMEWORKS_PATH).join(CEF_FRAMEWORK));
|
||||
|
||||
let resource_dir = app_dir.join(RESOURCES_PATH);
|
||||
fs::create_dir_all(&resource_dir).expect("failed to create app resource dir");
|
||||
|
||||
let icon_file = workspace_path().join("branding/app-icons").join(ICONS_FILE_NAME);
|
||||
fs::copy(icon_file, resource_dir.join(ICONS_FILE_NAME)).expect("failed to copy icon file");
|
||||
|
||||
app_dir
|
||||
}
|
||||
|
||||
fn create_app(app_dir: &Path, id: &str, name: &str, bin: &Path, is_helper: bool) {
|
||||
fs::create_dir_all(app_dir.join(EXEC_PATH)).unwrap();
|
||||
|
||||
let app_contents_dir: &Path = &app_dir.join("Contents");
|
||||
create_info_plist(app_contents_dir, id, name, is_helper).unwrap();
|
||||
fs::copy(bin, app_dir.join(EXEC_PATH).join(name)).unwrap();
|
||||
}
|
||||
|
||||
fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let info = InfoPlist {
|
||||
cf_bundle_name: exec_name.to_string(),
|
||||
cf_bundle_identifier: id.to_string(),
|
||||
cf_bundle_display_name: exec_name.to_string(),
|
||||
cf_bundle_executable: exec_name.to_string(),
|
||||
cf_bundle_icon_file: ICONS_FILE_NAME.to_string(),
|
||||
cf_bundle_info_dictionary_version: "6.0".to_string(),
|
||||
cf_bundle_package_type: "APPL".to_string(),
|
||||
cf_bundle_signature: "????".to_string(),
|
||||
cf_bundle_version: "0.0.0".to_string(),
|
||||
cf_bundle_short_version_string: "0.0".to_string(),
|
||||
cf_bundle_development_region: "en".to_string(),
|
||||
ls_environment: [("MallocNanoZone".to_string(), "0".to_string())].iter().cloned().collect(),
|
||||
ls_file_quarantine_enabled: true,
|
||||
ls_minimum_system_version: "11.0".to_string(),
|
||||
ls_ui_element: if is_helper { Some("1".to_string()) } else { None },
|
||||
ns_supports_automatic_graphics_switching: true,
|
||||
};
|
||||
|
||||
let plist_file = dir.join("Info.plist");
|
||||
plist::to_file_xml(plist_file, &info)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct InfoPlist {
|
||||
#[serde(rename = "CFBundleName")]
|
||||
cf_bundle_name: String,
|
||||
#[serde(rename = "CFBundleIdentifier")]
|
||||
cf_bundle_identifier: String,
|
||||
#[serde(rename = "CFBundleDisplayName")]
|
||||
cf_bundle_display_name: String,
|
||||
#[serde(rename = "CFBundleExecutable")]
|
||||
cf_bundle_executable: String,
|
||||
#[serde(rename = "CFBundleIconFile")]
|
||||
cf_bundle_icon_file: String,
|
||||
#[serde(rename = "CFBundleInfoDictionaryVersion")]
|
||||
cf_bundle_info_dictionary_version: String,
|
||||
#[serde(rename = "CFBundlePackageType")]
|
||||
cf_bundle_package_type: String,
|
||||
#[serde(rename = "CFBundleSignature")]
|
||||
cf_bundle_signature: String,
|
||||
#[serde(rename = "CFBundleVersion")]
|
||||
cf_bundle_version: String,
|
||||
#[serde(rename = "CFBundleShortVersionString")]
|
||||
cf_bundle_short_version_string: String,
|
||||
#[serde(rename = "CFBundleDevelopmentRegion")]
|
||||
cf_bundle_development_region: String,
|
||||
#[serde(rename = "LSEnvironment")]
|
||||
ls_environment: HashMap<String, String>,
|
||||
#[serde(rename = "LSFileQuarantineEnabled")]
|
||||
ls_file_quarantine_enabled: bool,
|
||||
#[serde(rename = "LSMinimumSystemVersion")]
|
||||
ls_minimum_system_version: String,
|
||||
#[serde(rename = "LSUIElement")]
|
||||
ls_ui_element: Option<String>,
|
||||
#[serde(rename = "NSSupportsAutomaticGraphicsSwitching")]
|
||||
ns_supports_automatic_graphics_switching: bool,
|
||||
}
|
||||
17
desktop/bundle/src/main.rs
Normal file
17
desktop/bundle/src/main.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
mod common;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win;
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_os = "linux")]
|
||||
linux::main().unwrap();
|
||||
#[cfg(target_os = "macos")]
|
||||
mac::main().unwrap();
|
||||
#[cfg(target_os = "windows")]
|
||||
win::main().unwrap();
|
||||
}
|
||||
59
desktop/bundle/src/win.rs
Normal file
59
desktop/bundle/src/win.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
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);
|
||||
|
||||
if let Err(e) = remove_unnecessary_cef_files(&app_dir) {
|
||||
eprintln!("Failed to remove unnecessary CEF files: {}", e);
|
||||
}
|
||||
|
||||
let bin_path = app_dir.join(EXECUTABLE);
|
||||
fs::copy(app_bin, &bin_path).unwrap();
|
||||
|
||||
bin_path
|
||||
}
|
||||
|
||||
fn remove_unnecessary_cef_files(app_dir: &Path) -> Result<(), Box<dyn Error>> {
|
||||
fs::remove_dir_all(app_dir.join("cmake"))?;
|
||||
fs::remove_dir_all(app_dir.join("include"))?;
|
||||
fs::remove_dir_all(app_dir.join("libcef_dll"))?;
|
||||
|
||||
for entry in fs::read_dir(app_dir.join("locales"))? {
|
||||
let path = entry?.path();
|
||||
if path.is_file() && path.file_name() != Some("en-US.pak".as_ref()) {
|
||||
fs::remove_file(path)?;
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_file(app_dir.join("archive.json"))?;
|
||||
fs::remove_file(app_dir.join("CMakeLists.txt"))?;
|
||||
fs::remove_file(app_dir.join("bootstrapc.exe"))?;
|
||||
fs::remove_file(app_dir.join("bootstrap.exe"))?;
|
||||
fs::remove_file(app_dir.join("libcef.lib"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
15
desktop/embedded-resources/Cargo.toml
Normal file
15
desktop/embedded-resources/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "graphite-desktop-embedded-resources"
|
||||
version = "0.1.0"
|
||||
description = "Graphite Desktop Embedded Resources"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
license = "Apache-2.0"
|
||||
repository = ""
|
||||
edition = "2024"
|
||||
rust-version = "1.87"
|
||||
|
||||
[dependencies]
|
||||
include_dir = { workspace = true }
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(embedded_resources)'] }
|
||||
32
desktop/embedded-resources/build.rs
Normal file
32
desktop/embedded-resources/build.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
const EMBEDDED_RESOURCES_ENV: &str = "EMBEDDED_RESOURCES";
|
||||
const DEFAULT_RESOURCES_DIR: &str = "../../frontend/dist";
|
||||
|
||||
fn main() {
|
||||
let mut embedded_resources: Option<String> = None;
|
||||
|
||||
println!("cargo:rerun-if-env-changed={EMBEDDED_RESOURCES_ENV}");
|
||||
if let Ok(embedded_resources_env) = std::env::var(EMBEDDED_RESOURCES_ENV)
|
||||
&& std::path::PathBuf::from(&embedded_resources_env).exists()
|
||||
{
|
||||
embedded_resources = Some(embedded_resources_env);
|
||||
}
|
||||
|
||||
if embedded_resources.is_none() {
|
||||
// Check if the directory `DEFAULT_RESOURCES_DIR` exists and sets the embedded_resources cfg accordingly
|
||||
// Absolute path of `DEFAULT_RESOURCES_DIR` available via the `EMBEDDED_RESOURCES` environment variable
|
||||
let crate_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
println!("cargo:rerun-if-changed={DEFAULT_RESOURCES_DIR}");
|
||||
if let Ok(resources) = crate_dir.join(DEFAULT_RESOURCES_DIR).canonicalize()
|
||||
&& resources.exists()
|
||||
{
|
||||
embedded_resources = Some(resources.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(embedded_resources) = embedded_resources {
|
||||
println!("cargo:rustc-cfg=embedded_resources");
|
||||
println!("cargo:rustc-env={EMBEDDED_RESOURCES_ENV}={embedded_resources}");
|
||||
} else {
|
||||
println!("cargo:warning=Resource directory does not exist. Resources will not be embedded. Did you forget to build the frontend?");
|
||||
}
|
||||
}
|
||||
10
desktop/embedded-resources/src/lib.rs
Normal file
10
desktop/embedded-resources/src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! This crate provides `EMBEDDED_RESOURCES` that can be included in the desktop application binary.
|
||||
//! It is intended to be used by the `embedded_resources` feature of the `graphite-desktop` crate.
|
||||
//! The build script checks if the specified resources directory exists and sets the `embedded_resources` cfg flag accordingly.
|
||||
//! If the resources directory does not exist, resources will not be embedded and a warning will be reported during compilation.
|
||||
|
||||
#[cfg(embedded_resources)]
|
||||
pub static EMBEDDED_RESOURCES: Option<include_dir::Dir> = Some(include_dir::include_dir!("$EMBEDDED_RESOURCES"));
|
||||
|
||||
#[cfg(not(embedded_resources))]
|
||||
pub static EMBEDDED_RESOURCES: Option<include_dir::Dir> = None;
|
||||
16
desktop/platform/linux/Cargo.toml
Normal file
16
desktop/platform/linux/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "graphite-desktop-platform-linux"
|
||||
version = "0.0.0"
|
||||
description = "Graphite Desktop Platform Linux"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
license = "Apache-2.0"
|
||||
repository = ""
|
||||
edition = "2024"
|
||||
rust-version = "1.87"
|
||||
|
||||
[[bin]]
|
||||
name = "graphite"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
graphite-desktop = { path = "../.." }
|
||||
3
desktop/platform/linux/src/main.rs
Normal file
3
desktop/platform/linux/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
graphite_desktop::start();
|
||||
}
|
||||
20
desktop/platform/mac/Cargo.toml
Normal file
20
desktop/platform/mac/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "graphite-desktop-platform-mac"
|
||||
version = "0.0.0"
|
||||
description = "Graphite Desktop Platform Mac"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
license = "Apache-2.0"
|
||||
repository = ""
|
||||
edition = "2024"
|
||||
rust-version = "1.87"
|
||||
|
||||
[[bin]]
|
||||
name = "graphite"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "helper"
|
||||
path = "src/helper.rs"
|
||||
|
||||
[dependencies]
|
||||
graphite-desktop = { path = "../.." }
|
||||
3
desktop/platform/mac/src/helper.rs
Normal file
3
desktop/platform/mac/src/helper.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
graphite_desktop::start_helper();
|
||||
}
|
||||
3
desktop/platform/mac/src/main.rs
Normal file
3
desktop/platform/mac/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
graphite_desktop::start();
|
||||
}
|
||||
19
desktop/platform/win/Cargo.toml
Normal file
19
desktop/platform/win/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "graphite-desktop-platform-win"
|
||||
version = "0.0.0"
|
||||
description = "Graphite Desktop Platform Windows"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
license = "Apache-2.0"
|
||||
repository = ""
|
||||
edition = "2024"
|
||||
rust-version = "1.87"
|
||||
|
||||
[[bin]]
|
||||
name = "graphite"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
graphite-desktop = { path = "../.." }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.build-dependencies]
|
||||
winres = "0.1"
|
||||
32
desktop/platform/win/build.rs
Normal file
32
desktop/platform/win/build.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let mut res = winres::WindowsResource::new();
|
||||
|
||||
res.set_icon("../../../branding/app-icons/graphite.ico");
|
||||
|
||||
res.set_language(0x0409); // English (US)
|
||||
|
||||
// TODO: Replace with actual version
|
||||
res.set_version_info(winres::VersionInfo::FILEVERSION, {
|
||||
const MAJOR: u64 = 0;
|
||||
const MINOR: u64 = 0;
|
||||
const PATCH: u64 = 0;
|
||||
const RELEASE: u64 = 0;
|
||||
(MAJOR << 48) | (MINOR << 32) | (PATCH << 16) | RELEASE
|
||||
});
|
||||
res.set("FileVersion", "0.0.0.0");
|
||||
res.set("ProductVersion", "0.0.0.0");
|
||||
|
||||
res.set("OriginalFilename", "Graphite.exe");
|
||||
|
||||
res.set("FileDescription", "Graphite");
|
||||
res.set("ProductName", "Graphite");
|
||||
|
||||
// TODO: Pull this year from the Git commit date
|
||||
res.set("LegalCopyright", "Copyright © 2026 Graphite Labs, LLC");
|
||||
res.set("CompanyName", "Graphite Labs, LLC");
|
||||
|
||||
res.compile().expect("Failed to compile Windows resources");
|
||||
}
|
||||
}
|
||||
4
desktop/platform/win/src/main.rs
Normal file
4
desktop/platform/win/src/main.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
fn main() {
|
||||
graphite_desktop::start();
|
||||
}
|
||||
@@ -1,74 +1,179 @@
|
||||
use crate::CustomEvent;
|
||||
use crate::cef::WindowSize;
|
||||
use crate::consts::{APP_NAME, CEF_MESSAGE_LOOP_MAX_ITERATIONS};
|
||||
use crate::render::GraphicsState;
|
||||
use graphite_desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, Platform};
|
||||
use graphite_desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
||||
|
||||
use rand::Rng;
|
||||
use rfd::AsyncFileDialog;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::mpsc::SyncSender;
|
||||
use std::fs;
|
||||
use std::sync::mpsc::{Receiver, Sender, SyncSender};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use std::time::{Duration, Instant};
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::dpi::PhysicalSize;
|
||||
use winit::event::WindowEvent;
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
use winit::event_loop::ControlFlow;
|
||||
use winit::event_loop::EventLoopProxy;
|
||||
use winit::window::Window;
|
||||
use winit::dpi::{PhysicalPosition, PhysicalSize};
|
||||
use winit::event::{ButtonSource, ElementState, MouseButton, StartCause, WindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
use winit::window::WindowId;
|
||||
|
||||
use crate::cef;
|
||||
use crate::cli::Cli;
|
||||
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};
|
||||
use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
||||
|
||||
pub(crate) struct WinitApp {
|
||||
cef_context: Box<dyn cef::CefContext>,
|
||||
window: Option<Arc<Window>>,
|
||||
cef_schedule: Option<Instant>,
|
||||
window_size_sender: Sender<WindowSize>,
|
||||
graphics_state: Option<GraphicsState>,
|
||||
pub(crate) struct App {
|
||||
render_state: Option<RenderState>,
|
||||
wgpu_context: WgpuContext,
|
||||
event_loop_proxy: EventLoopProxy<CustomEvent>,
|
||||
window: Option<Window>,
|
||||
window_scale: f64,
|
||||
window_size: PhysicalSize<u32>,
|
||||
window_maximized: bool,
|
||||
window_fullscreen: bool,
|
||||
pointer_position: PhysicalPosition<f64>,
|
||||
pointer_lock_position: Option<PhysicalPosition<f64>>,
|
||||
ui_scale: f64,
|
||||
app_event_receiver: Receiver<AppEvent>,
|
||||
app_event_scheduler: AppEventScheduler,
|
||||
desktop_wrapper: DesktopWrapper,
|
||||
last_ui_update: Instant,
|
||||
avg_frame_time: f32,
|
||||
cef_context: Box<dyn cef::CefContext>,
|
||||
cef_schedule: Option<Instant>,
|
||||
cef_view_info_sender: Sender<cef::ViewInfoUpdate>,
|
||||
cef_init_successful: bool,
|
||||
start_render_sender: SyncSender<()>,
|
||||
web_communication_initialized: bool,
|
||||
web_communication_startup_buffer: Vec<Vec<u8>>,
|
||||
persistent_data: PersistentData,
|
||||
cli: Cli,
|
||||
startup_time: Option<Instant>,
|
||||
exit_reason: ExitReason,
|
||||
}
|
||||
|
||||
impl WinitApp {
|
||||
pub(crate) fn new(cef_context: Box<dyn cef::CefContext>, window_size_sender: Sender<WindowSize>, wgpu_context: WgpuContext, event_loop_proxy: EventLoopProxy<CustomEvent>) -> Self {
|
||||
let rendering_loop_proxy = event_loop_proxy.clone();
|
||||
impl App {
|
||||
pub(crate) fn init() {
|
||||
Window::init();
|
||||
}
|
||||
|
||||
pub(crate) fn new(
|
||||
cef_context: Box<dyn cef::CefContext>,
|
||||
cef_view_info_sender: Sender<cef::ViewInfoUpdate>,
|
||||
wgpu_context: WgpuContext,
|
||||
app_event_receiver: Receiver<AppEvent>,
|
||||
app_event_scheduler: AppEventScheduler,
|
||||
cli: Cli,
|
||||
) -> Self {
|
||||
let ctrlc_app_event_scheduler = app_event_scheduler.clone();
|
||||
ctrlc::set_handler(move || {
|
||||
tracing::info!("Termination signal received, exiting...");
|
||||
ctrlc_app_event_scheduler.schedule(AppEvent::Exit);
|
||||
})
|
||||
.expect("Error setting Ctrl-C handler");
|
||||
|
||||
let rendering_app_event_scheduler = app_event_scheduler.clone();
|
||||
let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1);
|
||||
std::thread::spawn(move || {
|
||||
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||
loop {
|
||||
let result = futures::executor::block_on(DesktopWrapper::execute_node_graph());
|
||||
let _ = rendering_loop_proxy.send_event(CustomEvent::NodeGraphExecutionResult(result));
|
||||
let result = runtime.block_on(DesktopWrapper::execute_node_graph());
|
||||
rendering_app_event_scheduler.schedule(AppEvent::NodeGraphExecutionResult(result));
|
||||
let _ = start_render_receiver.recv();
|
||||
}
|
||||
});
|
||||
|
||||
let mut persistent_data = PersistentData::default();
|
||||
persistent_data.load_from_disk();
|
||||
|
||||
let desktop_wrapper = DesktopWrapper::new(rand::rng().random());
|
||||
|
||||
Self {
|
||||
cef_context,
|
||||
window: None,
|
||||
cef_schedule: Some(Instant::now()),
|
||||
graphics_state: None,
|
||||
window_size_sender,
|
||||
render_state: None,
|
||||
wgpu_context,
|
||||
event_loop_proxy,
|
||||
desktop_wrapper: DesktopWrapper::new(),
|
||||
last_ui_update: Instant::now(),
|
||||
avg_frame_time: 0.,
|
||||
window: None,
|
||||
window_scale: 1.,
|
||||
window_size: PhysicalSize { width: 0, height: 0 },
|
||||
window_maximized: false,
|
||||
window_fullscreen: false,
|
||||
pointer_position: Default::default(),
|
||||
pointer_lock_position: Default::default(),
|
||||
ui_scale: 1.,
|
||||
app_event_receiver,
|
||||
app_event_scheduler,
|
||||
desktop_wrapper,
|
||||
cef_context,
|
||||
cef_schedule: Some(Instant::now()),
|
||||
cef_view_info_sender,
|
||||
cef_init_successful: false,
|
||||
start_render_sender,
|
||||
web_communication_initialized: false,
|
||||
web_communication_startup_buffer: Vec::new(),
|
||||
persistent_data,
|
||||
cli,
|
||||
exit_reason: ExitReason::Shutdown,
|
||||
startup_time: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_desktop_frontend_message(&mut self, message: DesktopFrontendMessage) {
|
||||
pub(crate) fn run(mut self, event_loop: EventLoop) -> ExitReason {
|
||||
event_loop.run_app(&mut self).unwrap();
|
||||
self.exit_reason
|
||||
}
|
||||
|
||||
fn exit(&mut self, reason: Option<ExitReason>) {
|
||||
if let Some(reason) = reason {
|
||||
self.exit_reason = reason;
|
||||
}
|
||||
self.app_event_scheduler.schedule(AppEvent::Exit);
|
||||
}
|
||||
|
||||
fn resize(&mut self) {
|
||||
let Some(window) = &self.window else {
|
||||
tracing::error!("Resize failed due to missing window");
|
||||
return;
|
||||
};
|
||||
|
||||
let maximized = window.is_maximized();
|
||||
if maximized != self.window_maximized {
|
||||
self.window_maximized = maximized;
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::UpdateMaximized { maximized }));
|
||||
}
|
||||
|
||||
let fullscreen = window.is_fullscreen();
|
||||
if fullscreen != self.window_fullscreen {
|
||||
self.window_fullscreen = fullscreen;
|
||||
self.app_event_scheduler
|
||||
.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::UpdateFullscreen { fullscreen }));
|
||||
}
|
||||
|
||||
let size = window.surface_size();
|
||||
let scale = window.scale_factor() * self.ui_scale;
|
||||
let is_new_size = size != self.window_size;
|
||||
let is_new_scale = scale != self.window_scale;
|
||||
|
||||
if !is_new_size && !is_new_scale {
|
||||
return;
|
||||
}
|
||||
|
||||
if is_new_size {
|
||||
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));
|
||||
}
|
||||
|
||||
self.cef_context.notify_view_info_changed();
|
||||
|
||||
if let Some(render_state) = &mut self.render_state {
|
||||
render_state.resize(size.width, size.height);
|
||||
}
|
||||
|
||||
window.request_redraw();
|
||||
|
||||
self.window_size = size;
|
||||
self.window_scale = scale;
|
||||
}
|
||||
|
||||
fn handle_desktop_frontend_message(&mut self, message: DesktopFrontendMessage, responses: &mut Vec<DesktopWrapperMessage>) {
|
||||
match message {
|
||||
DesktopFrontendMessage::ToWeb(messages) => {
|
||||
let Some(bytes) = serialize_frontend_messages(messages) else {
|
||||
@@ -78,7 +183,7 @@ impl WinitApp {
|
||||
self.send_or_queue_web_message(bytes);
|
||||
}
|
||||
DesktopFrontendMessage::OpenFileDialog { title, filters, context } => {
|
||||
let event_loop_proxy = self.event_loop_proxy.clone();
|
||||
let app_event_scheduler = self.app_event_scheduler.clone();
|
||||
let _ = thread::spawn(move || {
|
||||
let mut dialog = AsyncFileDialog::new().set_title(title);
|
||||
for filter in filters {
|
||||
@@ -88,10 +193,10 @@ impl WinitApp {
|
||||
let show_dialog = async move { dialog.pick_file().await.map(|f| f.path().to_path_buf()) };
|
||||
|
||||
if let Some(path) = futures::executor::block_on(show_dialog)
|
||||
&& let Ok(content) = std::fs::read(&path)
|
||||
&& let Ok(content) = fs::read(&path)
|
||||
{
|
||||
let message = DesktopWrapperMessage::OpenFileDialogResult { path, content, context };
|
||||
let _ = event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message));
|
||||
let message = DesktopWrapperMessage::FileDialogResult { path, content, context };
|
||||
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -102,7 +207,7 @@ impl WinitApp {
|
||||
filters,
|
||||
context,
|
||||
} => {
|
||||
let event_loop_proxy = self.event_loop_proxy.clone();
|
||||
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 {
|
||||
@@ -116,12 +221,12 @@ impl WinitApp {
|
||||
|
||||
if let Some(path) = futures::executor::block_on(show_dialog) {
|
||||
let message = DesktopWrapperMessage::SaveFileDialogResult { path, context };
|
||||
let _ = event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message));
|
||||
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
});
|
||||
}
|
||||
DesktopFrontendMessage::WriteFile { path, content } => {
|
||||
if let Err(e) = std::fs::write(&path, content) {
|
||||
if let Err(e) = fs::write(&path, content) {
|
||||
tracing::error!("Failed to write file {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
@@ -132,41 +237,177 @@ impl WinitApp {
|
||||
}
|
||||
});
|
||||
}
|
||||
DesktopFrontendMessage::UpdateViewportBounds { x, y, width, height } => {
|
||||
if let Some(graphics_state) = &mut self.graphics_state
|
||||
DesktopFrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height } => {
|
||||
if let Some(render_state) = &mut self.render_state
|
||||
&& let Some(window) = &self.window
|
||||
{
|
||||
let window_size = window.inner_size();
|
||||
let window_size = window.surface_size();
|
||||
|
||||
let viewport_offset_x = x / window_size.width as f32;
|
||||
let viewport_offset_y = y / window_size.height as f32;
|
||||
graphics_state.set_viewport_offset([viewport_offset_x, viewport_offset_y]);
|
||||
let viewport_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 f32 / width } else { 1.0 };
|
||||
let viewport_scale_y = if height != 0.0 { window_size.height as f32 / height } else { 1.0 };
|
||||
graphics_state.set_viewport_scale([viewport_scale_x, viewport_scale_y]);
|
||||
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(graphics_state) = &mut self.graphics_state {
|
||||
graphics_state.set_overlays_scene(scene);
|
||||
if let Some(render_state) = &mut self.render_state {
|
||||
render_state.set_overlays_scene(scene);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::UpdateWindowState { maximized, minimized } => {
|
||||
if let Some(window) = &self.window {
|
||||
window.set_maximized(maximized);
|
||||
window.set_minimized(minimized);
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::CloseWindow => {
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::CloseWindow);
|
||||
DesktopFrontendMessage::PersistenceWriteDocument { id, document } => {
|
||||
self.persistent_data.write_document(id, document);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceDeleteDocument { id } => {
|
||||
self.persistent_data.delete_document(&id);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceUpdateCurrentDocument { id } => {
|
||||
self.persistent_data.set_current_document(id);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceUpdateDocumentsList { ids } => {
|
||||
self.persistent_data.set_document_order(ids);
|
||||
}
|
||||
DesktopFrontendMessage::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.cli.files.is_empty() {
|
||||
return;
|
||||
}
|
||||
let app_event_scheduler = self.app_event_scheduler.clone();
|
||||
let launch_documents = std::mem::take(&mut self.cli.files);
|
||||
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 {
|
||||
tracing::error!("Failed to read file: {}", path.display());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
DesktopFrontendMessage::UpdateMenu { entries } => {
|
||||
if let Some(window) = &self.window {
|
||||
window.update_menu(entries);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::ClipboardRead => {
|
||||
if let Some(window) = &self.window {
|
||||
let content = window.clipboard_read();
|
||||
let message = DesktopWrapperMessage::ClipboardReadResult { content };
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::ClipboardWrite { content } => {
|
||||
if let Some(window) = &mut self.window {
|
||||
window.clipboard_write(content);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::PointerLock => {
|
||||
self.pointer_lock_position = Some(self.pointer_position);
|
||||
if let Some(window) = &self.window {
|
||||
window.start_pointer_lock();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::WindowClose => {
|
||||
self.app_event_scheduler.schedule(AppEvent::Exit);
|
||||
}
|
||||
DesktopFrontendMessage::WindowMinimize => {
|
||||
if let Some(window) = &self.window {
|
||||
window.minimize();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::WindowMaximize => {
|
||||
if let Some(window) = &self.window {
|
||||
window.toggle_maximize();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::WindowFullscreen => {
|
||||
if let Some(window) = &mut self.window {
|
||||
window.toggle_fullscreen();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::WindowDrag => {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_desktop_frontend_messages(&mut self, messages: Vec<DesktopFrontendMessage>) {
|
||||
let mut responses = Vec::new();
|
||||
for message in messages {
|
||||
self.handle_desktop_frontend_message(message);
|
||||
self.handle_desktop_frontend_message(message, &mut responses);
|
||||
}
|
||||
for message in responses {
|
||||
self.dispatch_desktop_wrapper_message(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,13 +423,229 @@ impl WinitApp {
|
||||
self.web_communication_startup_buffer.push(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler<CustomEvent> for WinitApp {
|
||||
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||
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) => {
|
||||
if let Some(render_state) = self.render_state.as_mut() {
|
||||
render_state.bind_ui_texture(texture);
|
||||
}
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
if !self.cef_init_successful {
|
||||
self.cef_init_successful = true;
|
||||
}
|
||||
}
|
||||
AppEvent::ScheduleBrowserWork(instant) => {
|
||||
if instant <= Instant::now() {
|
||||
self.cef_context.work();
|
||||
} else {
|
||||
self.cef_schedule = Some(instant);
|
||||
}
|
||||
}
|
||||
AppEvent::CursorChange(cursor) => {
|
||||
if let Some(window) = &mut self.window {
|
||||
window.set_cursor(event_loop, cursor);
|
||||
}
|
||||
}
|
||||
AppEvent::Exit => {
|
||||
tracing::info!("Exiting main event loop");
|
||||
event_loop.exit();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
AppEvent::MenuEvent { id } => {
|
||||
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ApplicationHandler for App {
|
||||
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
|
||||
let window = Window::new(event_loop, self.app_event_scheduler.clone());
|
||||
self.window = Some(window);
|
||||
|
||||
let render_state = RenderState::new(self.window.as_ref().unwrap(), self.wgpu_context.clone());
|
||||
self.render_state = Some(render_state);
|
||||
|
||||
if let Some(window) = &self.window.as_ref() {
|
||||
window.show();
|
||||
}
|
||||
|
||||
self.resize();
|
||||
|
||||
self.desktop_wrapper.init(self.wgpu_context.clone());
|
||||
|
||||
self.startup_time = Some(Instant::now());
|
||||
}
|
||||
|
||||
fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) {
|
||||
while let Ok(event) = self.app_event_receiver.try_recv() {
|
||||
self.user_event(event_loop, event);
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(&mut self, _event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
|
||||
// Handle pointer lock release
|
||||
if let Some(pointer_lock_position) = self.pointer_lock_position
|
||||
&& let WindowEvent::PointerButton {
|
||||
state: ElementState::Released,
|
||||
button: ButtonSource::Mouse(MouseButton::Left),
|
||||
..
|
||||
} = event
|
||||
{
|
||||
self.pointer_lock_position = None;
|
||||
if let Some(window) = &self.window {
|
||||
window.end_pointer_lock();
|
||||
}
|
||||
self.cef_context.handle_window_event(&WindowEvent::PointerMoved {
|
||||
device_id: None,
|
||||
position: pointer_lock_position,
|
||||
primary: true,
|
||||
source: winit::event::PointerSource::Mouse,
|
||||
});
|
||||
}
|
||||
|
||||
self.cef_context.handle_window_event(&event);
|
||||
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
self.app_event_scheduler.schedule(AppEvent::Exit);
|
||||
}
|
||||
WindowEvent::SurfaceResized(_) | WindowEvent::ScaleFactorChanged { .. } => {
|
||||
self.resize();
|
||||
}
|
||||
WindowEvent::RedrawRequested => {
|
||||
#[cfg(target_os = "macos")]
|
||||
self.resize();
|
||||
|
||||
let Some(render_state) = &mut self.render_state else { return };
|
||||
if let Some(window) = &self.window {
|
||||
if !window.can_render() {
|
||||
return;
|
||||
}
|
||||
|
||||
match render_state.render(window) {
|
||||
Ok(_) => {}
|
||||
Err(RenderError::OutdatedUITextureError) => {
|
||||
self.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");
|
||||
self.exit(None);
|
||||
}
|
||||
Err(RenderError::SurfaceError(e)) => tracing::error!("Render error: {:?}", e),
|
||||
}
|
||||
let _ = self.start_render_sender.try_send(());
|
||||
}
|
||||
|
||||
if !self.cef_init_successful
|
||||
&& !self.cli.disable_ui_acceleration
|
||||
&& self.web_communication_initialized
|
||||
&& let Some(startup_time) = self.startup_time
|
||||
&& startup_time.elapsed() > Duration::from_secs(3)
|
||||
{
|
||||
tracing::error!("UI acceleration not working, exiting.");
|
||||
self.exit(Some(ExitReason::UiAccelerationFailure));
|
||||
}
|
||||
}
|
||||
WindowEvent::DragDropped { paths, .. } => {
|
||||
for path in paths {
|
||||
match fs::read(&path) {
|
||||
Ok(content) => {
|
||||
let message = DesktopWrapperMessage::ImportFile { path, content };
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to read dropped file {}: {}", path.display(), e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Forward and Back buttons are not supported by CEF and thus need to be directly forwarded the editor
|
||||
WindowEvent::PointerButton {
|
||||
button: ButtonSource::Mouse(button),
|
||||
state: ElementState::Pressed,
|
||||
..
|
||||
} => {
|
||||
let mouse_keys = match button {
|
||||
MouseButton::Back => Some(MouseKeys::BACK),
|
||||
MouseButton::Forward => Some(MouseKeys::FORWARD),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(mouse_keys) = mouse_keys {
|
||||
let message = DesktopWrapperMessage::Input(InputMessage::PointerDown {
|
||||
editor_mouse_state: MouseState { mouse_keys, ..Default::default() },
|
||||
modifier_keys: Default::default(),
|
||||
});
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
|
||||
let message = DesktopWrapperMessage::Input(InputMessage::PointerUp {
|
||||
editor_mouse_state: Default::default(),
|
||||
modifier_keys: Default::default(),
|
||||
});
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::PointerMoved { position, .. } | WindowEvent::PointerLeft { position: Some(position), .. } | WindowEvent::PointerEntered { position, .. }
|
||||
if self.pointer_lock_position.is_none() =>
|
||||
{
|
||||
self.pointer_position = position;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Notify cef of possible input events
|
||||
self.cef_context.work();
|
||||
}
|
||||
|
||||
fn device_event(&mut self, _event_loop: &dyn ActiveEventLoop, _device_id: Option<winit::event::DeviceId>, event: winit::event::DeviceEvent) {
|
||||
if self.pointer_lock_position.is_some()
|
||||
&& let winit::event::DeviceEvent::PointerMotion { delta: (x, y) } = event
|
||||
{
|
||||
let message = DesktopWrapperMessage::PointerLockMove { x, y };
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
}
|
||||
|
||||
fn new_events(&mut self, _event_loop: &dyn ActiveEventLoop, cause: winit::event::StartCause) {
|
||||
if let StartCause::ResumeTimeReached { .. } = cause
|
||||
&& let Some(window) = &self.window
|
||||
{
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) {
|
||||
// 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));
|
||||
let mut wait_until = Instant::now() + Duration::from_millis(10);
|
||||
if let Some(schedule) = self.cef_schedule
|
||||
&& schedule < Instant::now()
|
||||
{
|
||||
@@ -197,148 +654,14 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
||||
for _ in 0..CEF_MESSAGE_LOOP_MAX_ITERATIONS {
|
||||
self.cef_context.work();
|
||||
}
|
||||
} else if let Some(cef_schedule) = self.cef_schedule {
|
||||
wait_until = wait_until.min(cef_schedule);
|
||||
}
|
||||
if let Some(window) = &self.window.as_ref() {
|
||||
window.request_redraw();
|
||||
}
|
||||
|
||||
event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until));
|
||||
}
|
||||
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let mut window = Window::default_attributes()
|
||||
.with_title(APP_NAME)
|
||||
.with_min_inner_size(winit::dpi::LogicalSize::new(400, 300))
|
||||
.with_inner_size(winit::dpi::LogicalSize::new(1200, 800));
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use crate::consts::APP_ID;
|
||||
use winit::platform::wayland::ActiveEventLoopExtWayland;
|
||||
|
||||
window = if event_loop.is_wayland() {
|
||||
winit::platform::wayland::WindowAttributesExtWayland::with_name(window, APP_ID, "")
|
||||
} else {
|
||||
winit::platform::x11::WindowAttributesExtX11::with_name(window, APP_ID, APP_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
let window = Arc::new(event_loop.create_window(window).unwrap());
|
||||
let graphics_state = GraphicsState::new(window.clone(), self.wgpu_context.clone());
|
||||
|
||||
self.window = Some(window);
|
||||
self.graphics_state = Some(graphics_state);
|
||||
|
||||
tracing::info!("Winit window created and ready");
|
||||
|
||||
self.desktop_wrapper.init(self.wgpu_context.clone());
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let platform = Platform::Windows;
|
||||
#[cfg(target_os = "macos")]
|
||||
let platform = Platform::Mac;
|
||||
#[cfg(target_os = "linux")]
|
||||
let platform = Platform::Linux;
|
||||
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::UpdatePlatform(platform));
|
||||
}
|
||||
|
||||
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: CustomEvent) {
|
||||
match event {
|
||||
CustomEvent::WebCommunicationInitialized => {
|
||||
self.web_communication_initialized = true;
|
||||
for message in self.web_communication_startup_buffer.drain(..) {
|
||||
self.cef_context.send_web_message(message);
|
||||
}
|
||||
}
|
||||
CustomEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message),
|
||||
CustomEvent::NodeGraphExecutionResult(result) => match result {
|
||||
NodeGraphExecutionResult::HasRun(texture) => {
|
||||
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::PollNodeGraphEvaluation);
|
||||
if let Some(texture) = texture
|
||||
&& let Some(graphics_state) = self.graphics_state.as_mut()
|
||||
&& let Some(window) = self.window.as_ref()
|
||||
{
|
||||
graphics_state.bind_viewport_texture(texture);
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
NodeGraphExecutionResult::NotRun => {}
|
||||
},
|
||||
CustomEvent::UiUpdate(texture) => {
|
||||
if let Some(graphics_state) = self.graphics_state.as_mut() {
|
||||
graphics_state.resize(texture.width(), texture.height());
|
||||
graphics_state.bind_ui_texture(texture);
|
||||
let elapsed = self.last_ui_update.elapsed().as_secs_f32();
|
||||
self.last_ui_update = Instant::now();
|
||||
if elapsed < 0.5 {
|
||||
self.avg_frame_time = (self.avg_frame_time * 3. + elapsed) / 4.;
|
||||
}
|
||||
}
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
CustomEvent::ScheduleBrowserWork(instant) => {
|
||||
if instant <= Instant::now() {
|
||||
self.cef_context.work();
|
||||
} else {
|
||||
self.cef_schedule = Some(instant);
|
||||
}
|
||||
}
|
||||
CustomEvent::CloseWindow => {
|
||||
// TODO: Implement graceful shutdown
|
||||
|
||||
tracing::info!("Exiting main event loop");
|
||||
event_loop.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
|
||||
self.cef_context.handle_window_event(&event);
|
||||
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::CloseWindow);
|
||||
}
|
||||
WindowEvent::Resized(PhysicalSize { width, height }) => {
|
||||
let _ = self.window_size_sender.send(WindowSize::new(width as usize, height as usize));
|
||||
self.cef_context.notify_of_resize();
|
||||
}
|
||||
WindowEvent::RedrawRequested => {
|
||||
let Some(ref mut graphics_state) = self.graphics_state else { return };
|
||||
// Only rerender once we have a new ui texture to display
|
||||
if let Some(window) = &self.window {
|
||||
match graphics_state.render(window.as_ref()) {
|
||||
Ok(_) => {}
|
||||
Err(wgpu::SurfaceError::Lost) => {
|
||||
tracing::warn!("lost surface");
|
||||
}
|
||||
Err(wgpu::SurfaceError::OutOfMemory) => {
|
||||
event_loop.exit();
|
||||
}
|
||||
Err(e) => tracing::error!("{:?}", e),
|
||||
}
|
||||
let _ = self.start_render_sender.try_send(());
|
||||
}
|
||||
}
|
||||
// Currently not supported on wayland see https://github.com/rust-windowing/winit/issues/1881
|
||||
WindowEvent::DroppedFile(path) => {
|
||||
match std::fs::read(&path) {
|
||||
Ok(content) => {
|
||||
let message = DesktopWrapperMessage::OpenFile { path, content };
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to read dropped file {}: {}", path.display(), e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Notify cef of possible input events
|
||||
self.cef_context.work();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum ExitReason {
|
||||
Shutdown,
|
||||
UiAccelerationFailure,
|
||||
}
|
||||
|
||||
@@ -10,16 +10,21 @@
|
||||
//! - **Windows**: D3D11 shared textures via either Vulkan or D3D12 interop (`accelerated_paint_d3d11` feature)
|
||||
//! - **macOS**: IOSurface via Metal/Vulkan interop (`accelerated_paint_iosurface` feature)
|
||||
//!
|
||||
//!
|
||||
//! The system gracefully falls back to CPU textures when hardware acceleration is unavailable.
|
||||
|
||||
use crate::CustomEvent;
|
||||
use crate::render::FrameBufferRef;
|
||||
use graphite_desktop_wrapper::{WgpuContext, deserialize_editor_message};
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::event::{AppEvent, AppEventScheduler};
|
||||
use crate::render::FrameBufferRef;
|
||||
use crate::window::Cursor;
|
||||
use crate::wrapper::{WgpuContext, deserialize_editor_message};
|
||||
|
||||
mod consts;
|
||||
mod context;
|
||||
mod dirs;
|
||||
@@ -27,80 +32,121 @@ mod input;
|
||||
mod internal;
|
||||
mod ipc;
|
||||
mod platform;
|
||||
mod scheme_handler;
|
||||
mod utility;
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
mod texture_import;
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
use texture_import::SharedTextureHandle;
|
||||
use cef::osr_texture_import::SharedTextureHandle;
|
||||
|
||||
pub(crate) use context::{CefContext, CefContextBuilder, InitError};
|
||||
use winit::event_loop::EventLoopProxy;
|
||||
|
||||
pub(crate) trait CefEventHandler: Clone {
|
||||
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>);
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
fn draw_gpu(&self, shared_texture: SharedTextureHandle);
|
||||
/// Scheudule the main event loop to run the cef event loop after the timeout
|
||||
/// [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation.
|
||||
fn load_resource(&self, path: PathBuf) -> Option<Resource>;
|
||||
fn cursor_change(&self, cursor: Cursor);
|
||||
/// Schedule the main event loop to run the CEF event loop after the timeout.
|
||||
/// See [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation.
|
||||
fn schedule_cef_message_loop_work(&self, scheduled_time: Instant);
|
||||
fn initialized_web_communication(&self);
|
||||
fn receive_web_message(&self, message: &[u8]);
|
||||
fn duplicate(&self) -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct WindowSize {
|
||||
pub(crate) width: usize,
|
||||
pub(crate) height: usize,
|
||||
pub(crate) struct ViewInfo {
|
||||
width: u32,
|
||||
height: u32,
|
||||
scale: f64,
|
||||
}
|
||||
impl ViewInfo {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { width: 1, height: 1, scale: 1. }
|
||||
}
|
||||
pub(crate) fn apply_update(&mut self, update: ViewInfoUpdate) {
|
||||
match update {
|
||||
ViewInfoUpdate::Size { width, height } if width > 0 && height > 0 => {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
ViewInfoUpdate::Scale(scale) if scale > 0. => {
|
||||
self.scale = scale;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
pub(crate) fn zoom(&self) -> f64 {
|
||||
self.scale.ln() / 1.2_f64.ln()
|
||||
}
|
||||
pub(crate) fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
pub(crate) fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
impl Default for ViewInfo {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowSize {
|
||||
pub(crate) fn new(width: usize, height: usize) -> Self {
|
||||
Self { width, height }
|
||||
}
|
||||
pub(crate) enum ViewInfoUpdate {
|
||||
Size { width: u32, height: u32 },
|
||||
Scale(f64),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CefHandler {
|
||||
window_size_receiver: Arc<Mutex<WindowSizeReceiver>>,
|
||||
event_loop_proxy: EventLoopProxy<CustomEvent>,
|
||||
wgpu_context: WgpuContext,
|
||||
pub(crate) struct Resource {
|
||||
pub(crate) reader: ResourceReader,
|
||||
pub(crate) mimetype: Option<String>,
|
||||
}
|
||||
struct WindowSizeReceiver {
|
||||
receiver: Receiver<WindowSize>,
|
||||
window_size: WindowSize,
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ResourceReader {
|
||||
Embedded(io::Cursor<&'static [u8]>),
|
||||
File(Arc<File>),
|
||||
}
|
||||
impl WindowSizeReceiver {
|
||||
fn new(window_size_receiver: Receiver<WindowSize>) -> Self {
|
||||
Self {
|
||||
window_size: WindowSize { width: 1, height: 1 },
|
||||
receiver: window_size_receiver,
|
||||
impl Read for ResourceReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
match self {
|
||||
ResourceReader::Embedded(cursor) => cursor.read(buf),
|
||||
ResourceReader::File(file) => file.as_ref().read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct CefHandler {
|
||||
wgpu_context: WgpuContext,
|
||||
app_event_scheduler: AppEventScheduler,
|
||||
view_info_receiver: Arc<Mutex<ViewInfoReceiver>>,
|
||||
}
|
||||
|
||||
impl CefHandler {
|
||||
pub(crate) fn new(window_size_receiver: Receiver<WindowSize>, event_loop_proxy: EventLoopProxy<CustomEvent>, wgpu_context: WgpuContext) -> Self {
|
||||
pub(crate) fn new(wgpu_context: WgpuContext, app_event_scheduler: AppEventScheduler, view_info_receiver: Receiver<ViewInfoUpdate>) -> Self {
|
||||
Self {
|
||||
window_size_receiver: Arc::new(Mutex::new(WindowSizeReceiver::new(window_size_receiver))),
|
||||
event_loop_proxy,
|
||||
wgpu_context,
|
||||
app_event_scheduler,
|
||||
view_info_receiver: Arc::new(Mutex::new(ViewInfoReceiver::new(view_info_receiver))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CefEventHandler for CefHandler {
|
||||
fn window_size(&self) -> WindowSize {
|
||||
let Ok(mut guard) = self.window_size_receiver.lock() else {
|
||||
tracing::error!("Failed to lock window_size_receiver");
|
||||
return WindowSize::new(1, 1);
|
||||
fn view_info(&self) -> ViewInfo {
|
||||
let Ok(mut guard) = self.view_info_receiver.lock() else {
|
||||
tracing::error!("Failed to lock view_info_receiver");
|
||||
return ViewInfo::new();
|
||||
};
|
||||
let WindowSizeReceiver { receiver, window_size } = &mut *guard;
|
||||
for new_window_size in receiver.try_iter() {
|
||||
*window_size = new_window_size;
|
||||
let ViewInfoReceiver { receiver, view_info } = &mut *guard;
|
||||
for update in receiver.try_iter() {
|
||||
view_info.apply_update(update);
|
||||
}
|
||||
*window_size
|
||||
*view_info
|
||||
}
|
||||
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>) {
|
||||
let width = frame_buffer.width() as u32;
|
||||
@@ -115,7 +161,7 @@ impl CefEventHandler for CefHandler {
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||
format: wgpu::TextureFormat::Bgra8Unorm,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
});
|
||||
@@ -139,15 +185,86 @@ impl CefEventHandler for CefHandler {
|
||||
},
|
||||
);
|
||||
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
|
||||
self.app_event_scheduler.schedule(AppEvent::UiUpdate(texture));
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
fn draw_gpu(&self, shared_texture: SharedTextureHandle) {
|
||||
match shared_texture.import_texture(&self.wgpu_context.device) {
|
||||
Ok(texture) => {
|
||||
self.app_event_scheduler.schedule(AppEvent::UiUpdate(texture));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to 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) {
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::ScheduleBrowserWork(scheduled_time));
|
||||
self.app_event_scheduler.schedule(AppEvent::ScheduleBrowserWork(scheduled_time));
|
||||
}
|
||||
|
||||
fn initialized_web_communication(&self) {
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::WebCommunicationInitialized);
|
||||
self.app_event_scheduler.schedule(AppEvent::WebCommunicationInitialized);
|
||||
}
|
||||
|
||||
fn receive_web_message(&self, message: &[u8]) {
|
||||
@@ -155,18 +272,27 @@ impl CefEventHandler for CefHandler {
|
||||
tracing::error!("Failed to deserialize web message");
|
||||
return;
|
||||
};
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(desktop_wrapper_message));
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(desktop_wrapper_message));
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
fn draw_gpu(&self, shared_texture: SharedTextureHandle) {
|
||||
match shared_texture.import_texture(&self.wgpu_context.device) {
|
||||
Ok(texture) => {
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to import shared texture: {}", e);
|
||||
}
|
||||
fn 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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,22 @@
|
||||
pub(crate) const GRAPHITE_SCHEME: &str = "graphite-static";
|
||||
pub(crate) const FRONTEND_DOMAIN: &str = "frontend";
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) const RESOURCE_SCHEME: &str = "resources";
|
||||
pub(crate) const RESOURCE_DOMAIN: &str = "resources";
|
||||
|
||||
pub(crate) const SCROLL_LINE_HEIGHT: usize = 40;
|
||||
pub(crate) const SCROLL_LINE_WIDTH: usize = 40;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) const SCROLL_SPEED_X: f32 = 3.0;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) const SCROLL_SPEED_Y: f32 = 3.0;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) const SCROLL_SPEED_X: f32 = 1.0;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) const SCROLL_SPEED_Y: f32 = 1.0;
|
||||
|
||||
pub(crate) const PINCH_ZOOM_SPEED: f64 = 300.0;
|
||||
|
||||
pub(crate) const MULTICLICK_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
pub(crate) const MULTICLICK_ALLOWED_TRAVEL: usize = 4;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mod multithreaded;
|
||||
mod singlethreaded;
|
||||
|
||||
@@ -9,7 +10,7 @@ pub(crate) trait CefContext {
|
||||
|
||||
fn handle_window_event(&mut self, event: &winit::event::WindowEvent);
|
||||
|
||||
fn notify_of_resize(&self);
|
||||
fn notify_view_info_changed(&self);
|
||||
|
||||
fn send_web_message(&self, message: Vec<u8>);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,47 @@
|
||||
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, RenderHandler, RequestContext, Settings, WindowInfo, api_hash, browser_host_create_browser_sync, execute_process,
|
||||
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::CefHandler;
|
||||
use crate::cef::consts::{FRONTEND_DOMAIN, GRAPHITE_SCHEME};
|
||||
use crate::cef::dirs::{cef_cache_dir, cef_data_dir};
|
||||
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, RenderHandlerImpl, RenderProcessAppImpl};
|
||||
use crate::cef::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderProcessAppImpl, SchemeHandlerFactoryImpl};
|
||||
|
||||
pub(crate) struct CefContextBuilder {
|
||||
pub(crate) struct CefContextBuilder<H: CefEventHandler> {
|
||||
pub(crate) args: Args,
|
||||
pub(crate) is_sub_process: bool,
|
||||
_marker: std::marker::PhantomData<H>,
|
||||
}
|
||||
|
||||
unsafe impl Send for CefContextBuilder {}
|
||||
unsafe impl<H: CefEventHandler> Send for CefContextBuilder<H> {}
|
||||
|
||||
impl CefContextBuilder {
|
||||
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 = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), false);
|
||||
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();
|
||||
@@ -34,7 +49,11 @@ impl CefContextBuilder {
|
||||
let switch = CefString::from("type");
|
||||
let is_sub_process = cmd.has_switch(Some(&switch)) == 1;
|
||||
|
||||
Self { args, is_sub_process }
|
||||
Self {
|
||||
args,
|
||||
is_sub_process,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_sub_process(&self) -> bool {
|
||||
@@ -45,7 +64,7 @@ impl CefContextBuilder {
|
||||
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::app();
|
||||
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())
|
||||
@@ -54,35 +73,52 @@ impl CefContextBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) fn initialize(self, event_handler: CefHandler) -> Result<impl CefContext, InitError> {
|
||||
let settings = Settings {
|
||||
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,
|
||||
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()
|
||||
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)
|
||||
create_browser(event_handler, instance_dir, disable_gpu_acceleration)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub(crate) fn initialize(self, event_handler: CefHandler) -> Result<impl CefContext, InitError> {
|
||||
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 {
|
||||
windowless_rendering_enabled: 1,
|
||||
multi_threaded_message_loop: 1,
|
||||
root_cache_path: cef_data_dir().to_str().map(CefString::from).unwrap(),
|
||||
cache_path: cef_cache_dir().to_str().map(CefString::from).unwrap(),
|
||||
..Default::default()
|
||||
#[cfg(target_os = "linux")]
|
||||
no_sandbox: 1,
|
||||
..Self::common_settings(&instance_dir)
|
||||
};
|
||||
|
||||
self.initialize_inner(&event_handler, settings)?;
|
||||
|
||||
super::multithreaded::run_on_ui_thread(move || match create_browser(event_handler) {
|
||||
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);
|
||||
@@ -97,11 +133,11 @@ impl CefContextBuilder {
|
||||
Ok(super::multithreaded::MultiThreadedCefContextProxy)
|
||||
}
|
||||
|
||||
fn initialize_inner(self, event_handler: &CefHandler, settings: Settings) -> Result<(), InitError> {
|
||||
let mut cef_app = App::new(BrowserProcessAppImpl::new(event_handler.clone()));
|
||||
let result = cef::initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut());
|
||||
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 {
|
||||
@@ -113,16 +149,20 @@ impl CefContextBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_browser(event_handler: CefHandler) -> Result<SingleThreadedCefContext, InitError> {
|
||||
let render_handler = RenderHandler::new(RenderHandlerImpl::new(event_handler.clone()));
|
||||
let mut client = Client::new(BrowserProcessClientImpl::new(render_handler, event_handler.clone()));
|
||||
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));
|
||||
|
||||
let url = CefString::from(format!("{GRAPHITE_SCHEME}://{FRONTEND_DOMAIN}/").as_str());
|
||||
#[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: if crate::cef::platform::should_enable_hardware_acceleration() { 1 } else { 0 },
|
||||
shared_texture_enabled: use_accelerated_paint as i32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -132,19 +172,38 @@ fn create_browser(event_handler: CefHandler) -> Result<SingleThreadedCefContext,
|
||||
..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,
|
||||
Option::<&mut RequestContext>::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");
|
||||
@@ -166,6 +225,8 @@ pub(crate) enum InitError {
|
||||
InitializationFailed(u32),
|
||||
#[error("Browser creation failed")]
|
||||
BrowserCreationFailed,
|
||||
#[error("Request context creation failed")]
|
||||
RequestContextCreationFailed,
|
||||
#[error("Another instance is already running")]
|
||||
AlreadyRunning,
|
||||
}
|
||||
|
||||
@@ -30,11 +30,11 @@ impl CefContext for MultiThreadedCefContextProxy {
|
||||
});
|
||||
}
|
||||
|
||||
fn notify_of_resize(&self) {
|
||||
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_of_resize();
|
||||
context.notify_view_info_changed();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use cef::{Browser, ImplBrowser, ImplBrowserHost};
|
||||
use winit::event::WindowEvent;
|
||||
|
||||
use crate::cef::input;
|
||||
use crate::cef::input::InputState;
|
||||
use crate::cef::ipc::{MessageType, SendMessage};
|
||||
use 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 {
|
||||
@@ -18,11 +20,19 @@ impl CefContext for SingleThreadedCefContext {
|
||||
}
|
||||
|
||||
fn handle_window_event(&mut self, event: &WindowEvent) {
|
||||
input::handle_window_event(&self.browser, &mut self.input_state, event)
|
||||
input::handle_window_event(&self.browser, &mut self.input_state, event);
|
||||
}
|
||||
|
||||
fn notify_of_resize(&self) {
|
||||
self.browser.host().unwrap().was_resized();
|
||||
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>) {
|
||||
@@ -33,6 +43,19 @@ impl CefContext for SingleThreadedCefContext {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::dirs::{ensure_dir_exists, graphite_data_dir};
|
||||
use crate::dirs::{app_data_dir, ensure_dir_exists};
|
||||
|
||||
static CEF_DIR_NAME: &str = "browser";
|
||||
|
||||
pub(crate) fn cef_data_dir() -> PathBuf {
|
||||
let path = graphite_data_dir().join(CEF_DIR_NAME);
|
||||
ensure_dir_exists(&path);
|
||||
path
|
||||
pub(crate) fn delete_instance_dirs() {
|
||||
let cef_dir = app_data_dir().join(CEF_DIR_NAME);
|
||||
if let Ok(entries) = std::fs::read_dir(&cef_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
let _ = std::fs::remove_dir_all(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cef_cache_dir() -> PathBuf {
|
||||
let path = cef_data_dir().join("cache");
|
||||
pub(crate) fn create_instance_dir() -> PathBuf {
|
||||
let instance_id: String = (0..32).map(|_| format!("{:x}", rand::random::<u8>() % 16)).collect();
|
||||
let path = app_data_dir().join(CEF_DIR_NAME).join(instance_id);
|
||||
ensure_dir_exists(&path);
|
||||
path
|
||||
}
|
||||
|
||||
@@ -1,275 +1,149 @@
|
||||
use cef::sys::{cef_event_flags_t, cef_key_event_type_t, cef_mouse_button_type_t};
|
||||
use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, KeyEventType, MouseEvent};
|
||||
use winit::dpi::PhysicalPosition;
|
||||
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
||||
use cef::sys::{cef_key_event_type_t, cef_mouse_button_type_t};
|
||||
use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, MouseEvent};
|
||||
use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
||||
|
||||
mod keymap;
|
||||
use keymap::{ToDomBits, ToVKBits};
|
||||
use keymap::{ToCharRepresentation, ToNativeKeycode, ToVKBits};
|
||||
|
||||
mod state;
|
||||
pub(crate) use state::{CefModifiers, InputState};
|
||||
|
||||
use super::consts::{PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y};
|
||||
|
||||
pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputState, event: &WindowEvent) {
|
||||
match event {
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
if let Some(host) = browser.host() {
|
||||
host.set_focus(1);
|
||||
WindowEvent::PointerMoved { position, .. } | WindowEvent::PointerEntered { position, .. } => {
|
||||
if !input_state.cursor_move(position) {
|
||||
return;
|
||||
}
|
||||
|
||||
input_state.update_mouse_position(position);
|
||||
let mouse_event: MouseEvent = (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, .. } => {
|
||||
if let Some(host) = browser.host() {
|
||||
host.set_focus(1);
|
||||
|
||||
let mouse_up = match state {
|
||||
ElementState::Pressed => 0,
|
||||
ElementState::Released => 1,
|
||||
};
|
||||
|
||||
let cef_button = match button {
|
||||
MouseButton::Left => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_LEFT)),
|
||||
MouseButton::Right => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_RIGHT)),
|
||||
MouseButton::Middle => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_MIDDLE)),
|
||||
MouseButton::Forward => None, //TODO: Handle Forward button
|
||||
MouseButton::Back => None, //TODO: Handle Back button
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let mut mouse_state = input_state.mouse_state.clone();
|
||||
match button {
|
||||
MouseButton::Left => {
|
||||
mouse_state.left = match state {
|
||||
ElementState::Pressed => true,
|
||||
ElementState::Released => false,
|
||||
}
|
||||
}
|
||||
MouseButton::Right => {
|
||||
mouse_state.right = match state {
|
||||
ElementState::Pressed => true,
|
||||
ElementState::Released => false,
|
||||
}
|
||||
}
|
||||
MouseButton::Middle => {
|
||||
mouse_state.middle = match state {
|
||||
ElementState::Pressed => true,
|
||||
ElementState::Released => false,
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
input_state.update_mouse_state(mouse_state);
|
||||
|
||||
let mouse_event: MouseEvent = input_state.into();
|
||||
|
||||
if let Some(button) = cef_button {
|
||||
host.send_mouse_click_event(
|
||||
Some(&mouse_event),
|
||||
button,
|
||||
mouse_up,
|
||||
1, // click count
|
||||
);
|
||||
}
|
||||
WindowEvent::PointerLeft { position, .. } => {
|
||||
if let Some(position) = position {
|
||||
let _ = input_state.cursor_move(position);
|
||||
}
|
||||
|
||||
let Some(host) = browser.host() else { return };
|
||||
host.send_mouse_move_event(Some(&(input_state.into())), 1);
|
||||
}
|
||||
WindowEvent::PointerButton { state, button, .. } => {
|
||||
let mouse_button = match button {
|
||||
ButtonSource::Mouse(mouse_button) => mouse_button,
|
||||
_ => {
|
||||
return; // TODO: Handle touch input
|
||||
}
|
||||
};
|
||||
|
||||
let cef_click_count = input_state.mouse_input(mouse_button, state).into();
|
||||
let cef_mouse_up = match state {
|
||||
ElementState::Pressed => 0,
|
||||
ElementState::Released => 1,
|
||||
};
|
||||
let cef_button = match mouse_button {
|
||||
MouseButton::Left => cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_LEFT),
|
||||
MouseButton::Right => cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_RIGHT),
|
||||
MouseButton::Middle => cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_MIDDLE),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let Some(host) = browser.host() else { return };
|
||||
host.send_mouse_click_event(Some(&input_state.into()), cef_button, cef_mouse_up, cef_click_count);
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, phase: _, device_id: _, .. } => {
|
||||
if let Some(host) = browser.host() {
|
||||
let mouse_event = input_state.into();
|
||||
let line_width = 40; //feels about right, TODO: replace with correct value
|
||||
let line_height = 30; //feels about right, TODO: replace with correct value
|
||||
let (delta_x, delta_y) = match delta {
|
||||
MouseScrollDelta::LineDelta(x, y) => (x * line_width as f32, y * line_height as f32),
|
||||
MouseScrollDelta::PixelDelta(physical_position) => (physical_position.x as f32, physical_position.y as f32),
|
||||
};
|
||||
host.send_mouse_wheel_event(Some(&mouse_event), delta_x as i32, delta_y as i32);
|
||||
}
|
||||
let mouse_event = input_state.into();
|
||||
let (mut delta_x, mut delta_y) = match delta {
|
||||
MouseScrollDelta::LineDelta(x, y) => (x * SCROLL_LINE_WIDTH as f32, y * SCROLL_LINE_HEIGHT as f32),
|
||||
MouseScrollDelta::PixelDelta(physical_position) => (physical_position.x as f32, physical_position.y as f32),
|
||||
};
|
||||
delta_x *= SCROLL_SPEED_X;
|
||||
delta_y *= SCROLL_SPEED_Y;
|
||||
|
||||
let Some(host) = browser.host() else { return };
|
||||
host.send_mouse_wheel_event(Some(&mouse_event), delta_x as i32, delta_y as i32);
|
||||
}
|
||||
WindowEvent::ModifiersChanged(modifiers) => {
|
||||
input_state.update_modifiers(&modifiers.state());
|
||||
input_state.modifiers_changed(&modifiers.state());
|
||||
}
|
||||
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
|
||||
if let Some(host) = browser.host() {
|
||||
host.set_focus(1);
|
||||
let Some(host) = browser.host() else { return };
|
||||
|
||||
let (named_key, character) = match &event.logical_key {
|
||||
winit::keyboard::Key::Named(named_key) => (
|
||||
Some(named_key),
|
||||
match named_key {
|
||||
winit::keyboard::NamedKey::Space => Some(' '),
|
||||
winit::keyboard::NamedKey::Enter => Some('\u{000d}'),
|
||||
_ => None,
|
||||
},
|
||||
),
|
||||
winit::keyboard::Key::Character(str) => {
|
||||
let char = str.chars().next().unwrap_or('\0');
|
||||
(None, Some(char))
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
input_state.modifiers_apply_key_event(&event.logical_key, &event.state);
|
||||
|
||||
let mut key_event = KeyEvent {
|
||||
size: size_of::<KeyEvent>(),
|
||||
focus_on_editable_field: 1,
|
||||
modifiers: input_state.cef_modifiers(&event.location, event.repeat).raw(),
|
||||
is_system_key: 0,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(named_key) = named_key {
|
||||
key_event.native_key_code = named_key.to_dom_bits();
|
||||
key_event.windows_key_code = named_key.to_vk_bits();
|
||||
} else if let Some(char) = character {
|
||||
key_event.native_key_code = char.to_dom_bits();
|
||||
key_event.windows_key_code = char.to_vk_bits();
|
||||
let mut key_event = KeyEvent {
|
||||
type_: match (event.state, &event.logical_key) {
|
||||
(ElementState::Pressed, winit::keyboard::Key::Character(_)) => cef_key_event_type_t::KEYEVENT_CHAR,
|
||||
(ElementState::Pressed, _) => cef_key_event_type_t::KEYEVENT_RAWKEYDOWN,
|
||||
(ElementState::Released, _) => cef_key_event_type_t::KEYEVENT_KEYUP,
|
||||
}
|
||||
.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match event.state {
|
||||
ElementState::Pressed => {
|
||||
key_event.type_ = KeyEventType::from(cef_key_event_type_t::KEYEVENT_RAWKEYDOWN);
|
||||
host.send_key_event(Some(&key_event));
|
||||
key_event.modifiers = input_state.cef_modifiers(&event.location, event.repeat).into();
|
||||
|
||||
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.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.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));
|
||||
}
|
||||
};
|
||||
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));
|
||||
}
|
||||
WindowEvent::PinchGesture { delta, .. } => {
|
||||
if !delta.is_normal() {
|
||||
return;
|
||||
}
|
||||
let Some(host) = browser.host() else { return };
|
||||
|
||||
let mouse_event = MouseEvent {
|
||||
modifiers: CefModifiers::PINCH_MODIFIERS.into(),
|
||||
..input_state.into()
|
||||
};
|
||||
|
||||
let delta = (delta * PINCH_ZOOM_SPEED).round() as i32;
|
||||
|
||||
host.send_mouse_wheel_event(Some(&mouse_event), 0, delta);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct MouseState {
|
||||
left: bool,
|
||||
right: bool,
|
||||
middle: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub(crate) struct MousePosition {
|
||||
x: usize,
|
||||
y: usize,
|
||||
}
|
||||
|
||||
impl From<&PhysicalPosition<f64>> for MousePosition {
|
||||
fn from(position: &PhysicalPosition<f64>) -> Self {
|
||||
Self {
|
||||
x: position.x as usize,
|
||||
y: position.y as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct InputState {
|
||||
modifiers: winit::keyboard::ModifiersState,
|
||||
mouse_position: MousePosition,
|
||||
mouse_state: MouseState,
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
fn update_modifiers(&mut self, modifiers: &winit::keyboard::ModifiersState) {
|
||||
self.modifiers = *modifiers;
|
||||
}
|
||||
|
||||
fn update_mouse_position(&mut self, position: &PhysicalPosition<f64>) {
|
||||
self.mouse_position = position.into();
|
||||
}
|
||||
|
||||
fn update_mouse_state(&mut self, state: MouseState) {
|
||||
self.mouse_state = state;
|
||||
}
|
||||
|
||||
fn cef_modifiers(&self, location: &winit::keyboard::KeyLocation, is_repeat: bool) -> CefModifiers {
|
||||
CefModifiers::new(self, location, is_repeat)
|
||||
}
|
||||
|
||||
fn cef_modifiers_mouse_event(&self) -> CefModifiers {
|
||||
self.cef_modifiers(&winit::keyboard::KeyLocation::Standard, false)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InputState> for CefModifiers {
|
||||
fn from(val: InputState) -> Self {
|
||||
CefModifiers::new(&val, &winit::keyboard::KeyLocation::Standard, false)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&InputState> for MouseEvent {
|
||||
fn from(val: &InputState) -> Self {
|
||||
MouseEvent {
|
||||
x: val.mouse_position.x as i32,
|
||||
y: val.mouse_position.y as i32,
|
||||
modifiers: val.cef_modifiers_mouse_event().raw(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<&mut InputState> for MouseEvent {
|
||||
fn from(val: &mut InputState) -> Self {
|
||||
MouseEvent {
|
||||
x: val.mouse_position.x as i32,
|
||||
y: val.mouse_position.y as i32,
|
||||
modifiers: val.cef_modifiers_mouse_event().raw(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CefModifiers(u32);
|
||||
|
||||
impl CefModifiers {
|
||||
fn new(input_state: &InputState, location: &winit::keyboard::KeyLocation, is_repeat: bool) -> Self {
|
||||
let mut inner = 0;
|
||||
|
||||
if input_state.modifiers.shift_key() {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_SHIFT_DOWN as u32;
|
||||
}
|
||||
if input_state.modifiers.control_key() {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN as u32;
|
||||
}
|
||||
if input_state.modifiers.alt_key() {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_ALT_DOWN as u32;
|
||||
}
|
||||
if input_state.modifiers.super_key() {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_COMMAND_DOWN as u32;
|
||||
}
|
||||
|
||||
if input_state.mouse_state.left {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON as u32;
|
||||
}
|
||||
if input_state.mouse_state.right {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON as u32;
|
||||
}
|
||||
if input_state.mouse_state.middle {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON as u32;
|
||||
}
|
||||
|
||||
if is_repeat {
|
||||
inner |= cef_event_flags_t::EVENTFLAG_IS_REPEAT as u32;
|
||||
}
|
||||
|
||||
inner |= match location {
|
||||
winit::keyboard::KeyLocation::Left => cef_event_flags_t::EVENTFLAG_IS_LEFT as u32,
|
||||
winit::keyboard::KeyLocation::Right => cef_event_flags_t::EVENTFLAG_IS_RIGHT as u32,
|
||||
winit::keyboard::KeyLocation::Numpad => cef_event_flags_t::EVENTFLAG_IS_KEY_PAD as u32,
|
||||
winit::keyboard::KeyLocation::Standard => 0,
|
||||
};
|
||||
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
fn raw(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,48 @@
|
||||
use winit::keyboard::{Key, NamedKey, PhysicalKey};
|
||||
|
||||
pub(crate) trait ToCharRepresentation {
|
||||
fn to_char_representation(&self) -> char;
|
||||
}
|
||||
|
||||
impl ToCharRepresentation for Key {
|
||||
fn to_char_representation(&self) -> char {
|
||||
match self {
|
||||
Key::Named(named) => match named {
|
||||
NamedKey::Tab => '\t',
|
||||
NamedKey::Enter => '\r',
|
||||
NamedKey::Backspace => '\x08',
|
||||
NamedKey::Escape => '\x1b',
|
||||
_ => '\0',
|
||||
},
|
||||
Key::Character(char) => char.chars().next().unwrap_or_default(),
|
||||
_ => '\0',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ToNativeKeycode {
|
||||
fn to_native_keycode(&self) -> i32;
|
||||
}
|
||||
|
||||
impl ToNativeKeycode for PhysicalKey {
|
||||
fn to_native_keycode(&self) -> i32 {
|
||||
use winit::platform::scancode::PhysicalKeyExtScancode;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
self.to_scancode().map(|evdev| (evdev + 8) as i32).unwrap_or_default()
|
||||
}
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
{
|
||||
self.to_scancode().map(|c| c as i32).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ToVKBits {
|
||||
fn to_vk_bits(&self) -> i32;
|
||||
}
|
||||
|
||||
macro_rules! map_enum {
|
||||
($target:expr, $enum:ident, $( ($code:expr, $variant:ident), )+ ) => {
|
||||
match $target {
|
||||
@@ -8,26 +53,8 @@ macro_rules! map_enum {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! map {
|
||||
($target:expr, $( ($code:expr, $variant:literal), )+ ) => {
|
||||
match $target {
|
||||
$(
|
||||
$variant => $code,
|
||||
)+
|
||||
_ => 0,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Windows Virtual keyboard binary representation
|
||||
pub(crate) trait ToVKBits {
|
||||
fn to_vk_bits(&self) -> i32;
|
||||
}
|
||||
|
||||
impl ToVKBits for winit::keyboard::NamedKey {
|
||||
fn to_vk_bits(&self) -> i32 {
|
||||
use winit::keyboard::NamedKey;
|
||||
map_enum!(
|
||||
self,
|
||||
NamedKey,
|
||||
@@ -39,14 +66,12 @@ impl ToVKBits for winit::keyboard::NamedKey {
|
||||
(0x91, ScrollLock),
|
||||
(0x10, Shift),
|
||||
(0x5B, Meta),
|
||||
(0x5C, Super),
|
||||
(0x0D, Enter),
|
||||
(0x09, Tab),
|
||||
(0x20, Space),
|
||||
(0x28, ArrowDown),
|
||||
(0x25, ArrowLeft),
|
||||
(0x27, ArrowRight),
|
||||
(0x26, ArrowUp),
|
||||
(0x27, ArrowRight),
|
||||
(0x28, ArrowDown),
|
||||
(0x23, End),
|
||||
(0x24, Home),
|
||||
(0x22, PageDown),
|
||||
@@ -136,6 +161,16 @@ impl ToVKBits for winit::keyboard::NamedKey {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! map {
|
||||
($target:expr, $( ($code:expr, $variant:literal), )+ ) => {
|
||||
match $target {
|
||||
$(
|
||||
$variant => $code,
|
||||
)+
|
||||
_ => 0,
|
||||
}
|
||||
};
|
||||
}
|
||||
impl ToVKBits for char {
|
||||
fn to_vk_bits(&self) -> i32 {
|
||||
map!(
|
||||
@@ -234,215 +269,7 @@ impl ToVKBits for char {
|
||||
(0xDE, '"'),
|
||||
(0xBF, '/'),
|
||||
(0xBF, '?'),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Chromium dom key binary representation
|
||||
pub(crate) trait ToDomBits {
|
||||
fn to_dom_bits(&self) -> i32;
|
||||
}
|
||||
|
||||
impl ToDomBits for winit::keyboard::NamedKey {
|
||||
fn to_dom_bits(&self) -> i32 {
|
||||
use winit::keyboard::NamedKey;
|
||||
map_enum!(
|
||||
self,
|
||||
NamedKey,
|
||||
(0x00, Hyper),
|
||||
(0x85, Super),
|
||||
(0x25, Control),
|
||||
(0x32, Shift),
|
||||
(0x40, Alt),
|
||||
(0x00, Fn),
|
||||
(0x00, FnLock),
|
||||
(0x24, Enter),
|
||||
(0x09, Escape),
|
||||
(0x16, Backspace),
|
||||
(0x17, Tab),
|
||||
(0x41, Space),
|
||||
(0x42, CapsLock),
|
||||
(0x43, F1),
|
||||
(0x44, F2),
|
||||
(0x45, F3),
|
||||
(0x46, F4),
|
||||
(0x47, F5),
|
||||
(0x48, F6),
|
||||
(0x49, F7),
|
||||
(0x4a, F8),
|
||||
(0x4b, F9),
|
||||
(0x4c, F10),
|
||||
(0x5f, F11),
|
||||
(0x60, F12),
|
||||
(0x6b, PrintScreen),
|
||||
(0x4e, ScrollLock),
|
||||
(0x7f, Pause),
|
||||
(0x76, Insert),
|
||||
(0x6e, Home),
|
||||
(0x70, PageUp),
|
||||
(0x77, Delete),
|
||||
(0x73, End),
|
||||
(0x75, PageDown),
|
||||
(0x72, ArrowRight),
|
||||
(0x71, ArrowLeft),
|
||||
(0x74, ArrowDown),
|
||||
(0x6f, ArrowUp),
|
||||
(0x4d, NumLock),
|
||||
(0x87, ContextMenu),
|
||||
(0x7c, Power),
|
||||
(0xbf, F13),
|
||||
(0xc0, F14),
|
||||
(0xc1, F15),
|
||||
(0xc2, F16),
|
||||
(0xc3, F17),
|
||||
(0xc4, F18),
|
||||
(0xc5, F19),
|
||||
(0xc6, F20),
|
||||
(0xc7, F21),
|
||||
(0xc8, F22),
|
||||
(0xc9, F23),
|
||||
(0xca, F24),
|
||||
(0x8e, Open),
|
||||
(0x92, Help),
|
||||
(0x8c, Select),
|
||||
(0x89, Again),
|
||||
(0x8b, Undo),
|
||||
(0x91, Cut),
|
||||
(0x8d, Copy),
|
||||
(0x8f, Paste),
|
||||
(0x90, Find),
|
||||
(0x79, AudioVolumeMute),
|
||||
(0x7b, AudioVolumeUp),
|
||||
(0x7a, AudioVolumeDown),
|
||||
(0x65, KanaMode),
|
||||
(0x64, Convert),
|
||||
(0x66, NonConvert),
|
||||
(0x00, Props),
|
||||
(0xe9, BrightnessUp),
|
||||
(0xe8, BrightnessDown),
|
||||
(0xd7, MediaPlay),
|
||||
(0xd1, MediaPause),
|
||||
(0xaf, MediaRecord),
|
||||
(0xd8, MediaFastForward),
|
||||
(0xb0, MediaRewind),
|
||||
(0xab, MediaTrackNext),
|
||||
(0xad, MediaTrackPrevious),
|
||||
(0xae, MediaStop),
|
||||
(0xa9, Eject),
|
||||
(0xac, MediaPlayPause),
|
||||
(0xa3, LaunchMail),
|
||||
(0xe1, BrowserSearch),
|
||||
(0xb4, BrowserHome),
|
||||
(0xa6, BrowserBack),
|
||||
(0xa7, BrowserForward),
|
||||
(0x88, BrowserStop),
|
||||
(0xb5, BrowserRefresh),
|
||||
(0xa4, BrowserFavorites),
|
||||
(0xf0, MailReply),
|
||||
(0xf1, MailForward),
|
||||
(0xef, MailSend),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToDomBits for char {
|
||||
fn to_dom_bits(&self) -> i32 {
|
||||
map!(
|
||||
self,
|
||||
(0x26, 'a'),
|
||||
(0x38, 'b'),
|
||||
(0x36, 'c'),
|
||||
(0x28, 'd'),
|
||||
(0x1a, 'e'),
|
||||
(0x29, 'f'),
|
||||
(0x2a, 'g'),
|
||||
(0x2b, 'h'),
|
||||
(0x1f, 'i'),
|
||||
(0x2c, 'j'),
|
||||
(0x2d, 'k'),
|
||||
(0x2e, 'l'),
|
||||
(0x3a, 'm'),
|
||||
(0x39, 'n'),
|
||||
(0x20, 'o'),
|
||||
(0x21, 'p'),
|
||||
(0x18, 'q'),
|
||||
(0x1b, 'r'),
|
||||
(0x27, 's'),
|
||||
(0x1c, 't'),
|
||||
(0x1e, 'u'),
|
||||
(0x37, 'v'),
|
||||
(0x19, 'w'),
|
||||
(0x35, 'x'),
|
||||
(0x1d, 'y'),
|
||||
(0x34, 'z'),
|
||||
(0x26, 'A'),
|
||||
(0x38, 'B'),
|
||||
(0x36, 'C'),
|
||||
(0x28, 'D'),
|
||||
(0x1a, 'E'),
|
||||
(0x29, 'F'),
|
||||
(0x2a, 'G'),
|
||||
(0x2b, 'H'),
|
||||
(0x1f, 'I'),
|
||||
(0x2c, 'J'),
|
||||
(0x2d, 'K'),
|
||||
(0x2e, 'L'),
|
||||
(0x3a, 'M'),
|
||||
(0x39, 'N'),
|
||||
(0x20, 'O'),
|
||||
(0x21, 'P'),
|
||||
(0x18, 'Q'),
|
||||
(0x1b, 'R'),
|
||||
(0x27, 'S'),
|
||||
(0x1c, 'T'),
|
||||
(0x1e, 'U'),
|
||||
(0x37, 'V'),
|
||||
(0x19, 'W'),
|
||||
(0x35, 'X'),
|
||||
(0x1d, 'Y'),
|
||||
(0x34, 'Z'),
|
||||
(0x0a, '1'),
|
||||
(0x0b, '2'),
|
||||
(0x0c, '3'),
|
||||
(0x0d, '4'),
|
||||
(0x0e, '5'),
|
||||
(0x0f, '6'),
|
||||
(0x10, '7'),
|
||||
(0x11, '8'),
|
||||
(0x12, '9'),
|
||||
(0x13, '0'),
|
||||
(0x0a, '!'),
|
||||
(0x0b, '@'),
|
||||
(0x0c, '#'),
|
||||
(0x0d, '$'),
|
||||
(0x0e, '%'),
|
||||
(0x0f, '^'),
|
||||
(0x10, '&'),
|
||||
(0x11, '*'),
|
||||
(0x12, '('),
|
||||
(0x13, ')'),
|
||||
(0x31, '`'),
|
||||
(0x31, '~'),
|
||||
(0x14, '-'),
|
||||
(0x14, '_'),
|
||||
(0x15, '='),
|
||||
(0x15, '+'),
|
||||
(0x22, '['),
|
||||
(0x22, '{'),
|
||||
(0x23, ']'),
|
||||
(0x23, '}'),
|
||||
(0x33, '\\'),
|
||||
(0x33, '|'),
|
||||
(0x2f, ';'),
|
||||
(0x2f, ':'),
|
||||
(0x3b, ','),
|
||||
(0x3b, '<'),
|
||||
(0x3c, '.'),
|
||||
(0x3c, '>'),
|
||||
(0x30, '\''),
|
||||
(0x30, '"'),
|
||||
(0x3d, '/'),
|
||||
(0x3d, '?'),
|
||||
(0x20, ' '),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
275
desktop/src/cef/input/state.rs
Normal file
275
desktop/src/cef/input/state.rs
Normal file
@@ -0,0 +1,275 @@
|
||||
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::{Key, KeyLocation, ModifiersState, NamedKey};
|
||||
|
||||
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 modifiers_apply_key_event(&mut self, key: &Key, state: &ElementState) {
|
||||
let bits = match key {
|
||||
Key::Named(NamedKey::Shift) => ModifiersState::SHIFT,
|
||||
Key::Named(NamedKey::Control) => ModifiersState::CONTROL,
|
||||
Key::Named(NamedKey::Alt) => ModifiersState::ALT,
|
||||
Key::Named(NamedKey::Meta) => ModifiersState::META,
|
||||
_ => return,
|
||||
};
|
||||
let is_pressed = matches!(state, ElementState::Pressed);
|
||||
self.modifiers.set(bits, is_pressed);
|
||||
}
|
||||
|
||||
pub(crate) fn cursor_move(&mut self, position: &PhysicalPosition<f64>) -> bool {
|
||||
let new = position.into();
|
||||
if self.mouse_position == new {
|
||||
return false;
|
||||
}
|
||||
self.mouse_position = new;
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn mouse_input(&mut self, button: &MouseButton, state: &ElementState) -> ClickCount {
|
||||
self.mouse_state.update(button, state);
|
||||
self.mouse_click_tracker.input(button, state, self.mouse_position)
|
||||
}
|
||||
|
||||
pub(crate) fn cef_modifiers(&self, location: &KeyLocation, is_repeat: bool) -> CefModifiers {
|
||||
CefModifiers::new(self, location, is_repeat)
|
||||
}
|
||||
|
||||
pub(crate) fn cef_mouse_modifiers(&self) -> CefModifiers {
|
||||
self.cef_modifiers(&KeyLocation::Standard, false)
|
||||
}
|
||||
}
|
||||
|
||||
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 prev_count: ClickCount = record.down_count;
|
||||
|
||||
let now = Instant::now();
|
||||
record.time = now;
|
||||
record.position = position;
|
||||
|
||||
match state {
|
||||
ElementState::Pressed if record.down_count == ClickCount::Triple => {
|
||||
*record = ClickRecord {
|
||||
down_count: ClickCount::Double,
|
||||
..*record
|
||||
};
|
||||
return ClickCount::Double;
|
||||
}
|
||||
ElementState::Released if record.up_count == ClickCount::Triple => {
|
||||
*record = ClickRecord {
|
||||
up_count: ClickCount::Double,
|
||||
..*record
|
||||
};
|
||||
return ClickCount::Double;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
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 = match (prev_count, within_time, within_dist) {
|
||||
(ClickCount::Double, true, true) => ClickCount::Triple,
|
||||
(_, true, true) => ClickCount::Double,
|
||||
_ => 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,
|
||||
Triple,
|
||||
}
|
||||
impl From<ClickCount> for i32 {
|
||||
fn from(count: ClickCount) -> i32 {
|
||||
match count {
|
||||
ClickCount::Single => 1,
|
||||
ClickCount::Double => 2,
|
||||
ClickCount::Triple => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ClickRecord {
|
||||
time: Instant,
|
||||
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,15 +1,24 @@
|
||||
mod browser_process_app;
|
||||
mod browser_process_client;
|
||||
mod browser_process_handler;
|
||||
mod browser_process_life_span_handler;
|
||||
|
||||
mod render_process_app;
|
||||
mod render_process_handler;
|
||||
mod render_process_v8_handler;
|
||||
|
||||
mod context_menu_handler;
|
||||
mod display_handler;
|
||||
mod life_span_handler;
|
||||
mod load_handler;
|
||||
mod resource_handler;
|
||||
mod scheme_handler_factory;
|
||||
|
||||
pub(super) mod render_handler;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub(super) mod task;
|
||||
|
||||
pub(super) use browser_process_app::BrowserProcessAppImpl;
|
||||
pub(super) use browser_process_client::BrowserProcessClientImpl;
|
||||
pub(super) use render_handler::RenderHandlerImpl;
|
||||
pub(super) use render_process_app::RenderProcessAppImpl;
|
||||
pub(super) use scheme_handler_factory::SchemeHandlerFactoryImpl;
|
||||
|
||||
@@ -5,17 +5,15 @@ use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
|
||||
use cef::{BrowserProcessHandler, CefString, ImplApp, ImplCommandLine, SchemeRegistrar, WrapApp};
|
||||
|
||||
use crate::cef::CefEventHandler;
|
||||
|
||||
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
|
||||
|
||||
use super::browser_process_handler::BrowserProcessHandlerImpl;
|
||||
use super::scheme_handler_factory::SchemeHandlerFactoryImpl;
|
||||
use crate::cef::CefEventHandler;
|
||||
|
||||
pub(crate) struct BrowserProcessAppImpl<H: CefEventHandler> {
|
||||
object: *mut RcImpl<_cef_app_t, Self>,
|
||||
event_handler: H,
|
||||
}
|
||||
impl<H: CefEventHandler + Clone> BrowserProcessAppImpl<H> {
|
||||
impl<H: CefEventHandler> BrowserProcessAppImpl<H> {
|
||||
pub(crate) fn new(event_handler: H) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
@@ -24,17 +22,28 @@ impl<H: CefEventHandler + Clone> BrowserProcessAppImpl<H> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler + Clone> ImplApp for BrowserProcessAppImpl<H> {
|
||||
impl<H: CefEventHandler> ImplApp for BrowserProcessAppImpl<H> {
|
||||
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
|
||||
Some(BrowserProcessHandler::new(BrowserProcessHandlerImpl::new(self.event_handler.clone())))
|
||||
Some(BrowserProcessHandler::new(BrowserProcessHandlerImpl::new(self.event_handler.duplicate())))
|
||||
}
|
||||
|
||||
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
|
||||
GraphiteSchemeHandlerFactory::register_schemes(registrar);
|
||||
SchemeHandlerFactoryImpl::<H>::register_schemes(registrar);
|
||||
}
|
||||
|
||||
fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) {
|
||||
if let Some(cmd) = command_line {
|
||||
cmd.append_switch_with_value(Some(&CefString::from("renderer-process-limit")), Some(&CefString::from("1")));
|
||||
cmd.append_switch_with_value(Some(&CefString::from("password-store")), Some(&CefString::from("basic")));
|
||||
cmd.append_switch_with_value(Some(&CefString::from("disk-cache-size")), Some(&CefString::from("0")));
|
||||
cmd.append_switch(Some(&CefString::from("incognito")));
|
||||
cmd.append_switch(Some(&CefString::from("no-first-run")));
|
||||
cmd.append_switch(Some(&CefString::from("disable-file-system")));
|
||||
cmd.append_switch(Some(&CefString::from("disable-local-storage")));
|
||||
cmd.append_switch(Some(&CefString::from("disable-background-networking")));
|
||||
cmd.append_switch(Some(&CefString::from("disable-audio-input")));
|
||||
cmd.append_switch(Some(&CefString::from("disable-audio-output")));
|
||||
|
||||
#[cfg(not(feature = "accelerated_paint"))]
|
||||
{
|
||||
// Disable GPU acceleration when accelerated_paint feature is not enabled
|
||||
@@ -68,6 +77,20 @@ impl<H: CefEventHandler + Clone> ImplApp for BrowserProcessAppImpl<H> {
|
||||
cmd.append_switch_with_value(Some(&CefString::from("ozone-platform")), Some(&CefString::from("wayland")));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Hide user prompt asking for keychain access
|
||||
cmd.append_switch(Some(&CefString::from("use-mock-keychain")));
|
||||
}
|
||||
|
||||
// Enable browser debugging via environment variable
|
||||
if let Some(env) = std::env::var("GRAPHITE_BROWSER_DEBUG_PORT").ok()
|
||||
&& let Some(port) = env.parse::<u16>().ok()
|
||||
{
|
||||
cmd.append_switch_with_value(Some(&CefString::from("remote-debugging-port")), Some(&CefString::from(port.to_string().as_str())));
|
||||
cmd.append_switch_with_value(Some(&CefString::from("remote-allow-origins")), Some(&CefString::from("*")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +99,7 @@ impl<H: CefEventHandler + Clone> ImplApp for BrowserProcessAppImpl<H> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler + Clone> Clone for BrowserProcessAppImpl<H> {
|
||||
impl<H: CefEventHandler> Clone for BrowserProcessAppImpl<H> {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
@@ -84,7 +107,7 @@ impl<H: CefEventHandler + Clone> Clone for BrowserProcessAppImpl<H> {
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
event_handler: self.event_handler.clone(),
|
||||
event_handler: self.event_handler.duplicate(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,7 +119,7 @@ impl<H: CefEventHandler> Rc for BrowserProcessAppImpl<H> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler + Clone> WrapApp for BrowserProcessAppImpl<H> {
|
||||
impl<H: CefEventHandler> WrapApp for BrowserProcessAppImpl<H> {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_client_t, cef_base_ref_counted_t};
|
||||
use cef::{ImplClient, LifeSpanHandler, RenderHandler, WrapClient};
|
||||
use cef::{ContextMenuHandler, DisplayHandler, ImplClient, LifeSpanHandler, LoadHandler, RenderHandler, WrapClient};
|
||||
|
||||
use crate::cef::CefEventHandler;
|
||||
use crate::cef::ipc::{MessageType, UnpackMessage, UnpackedMessage};
|
||||
|
||||
use super::browser_process_life_span_handler::BrowserProcessLifeSpanHandlerImpl;
|
||||
use super::context_menu_handler::ContextMenuHandlerImpl;
|
||||
use super::display_handler::DisplayHandlerImpl;
|
||||
use super::life_span_handler::LifeSpanHandlerImpl;
|
||||
use super::load_handler::LoadHandlerImpl;
|
||||
use super::render_handler::RenderHandlerImpl;
|
||||
|
||||
pub(crate) struct BrowserProcessClientImpl<H: CefEventHandler> {
|
||||
object: *mut RcImpl<_cef_client_t, Self>,
|
||||
render_handler: RenderHandler,
|
||||
event_handler: H,
|
||||
load_handler: LoadHandler,
|
||||
render_handler: RenderHandler,
|
||||
display_handler: DisplayHandler,
|
||||
}
|
||||
impl<H: CefEventHandler> BrowserProcessClientImpl<H> {
|
||||
pub(crate) fn new(render_handler: RenderHandler, event_handler: H) -> Self {
|
||||
pub(crate) fn new(event_handler: &H) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
render_handler,
|
||||
event_handler,
|
||||
event_handler: event_handler.duplicate(),
|
||||
load_handler: LoadHandler::new(LoadHandlerImpl::new(event_handler.duplicate())),
|
||||
render_handler: RenderHandler::new(RenderHandlerImpl::new(event_handler.duplicate())),
|
||||
display_handler: DisplayHandler::new(DisplayHandlerImpl::new(event_handler.duplicate())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +37,7 @@ impl<H: CefEventHandler> ImplClient for BrowserProcessClientImpl<H> {
|
||||
_frame: Option<&mut cef::Frame>,
|
||||
_source_process: cef::ProcessId,
|
||||
message: Option<&mut cef::ProcessMessage>,
|
||||
) -> ::std::os::raw::c_int {
|
||||
) -> std::ffi::c_int {
|
||||
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
|
||||
match unpacked_message {
|
||||
Some(UnpackedMessage {
|
||||
@@ -49,12 +57,24 @@ impl<H: CefEventHandler> ImplClient for BrowserProcessClientImpl<H> {
|
||||
1
|
||||
}
|
||||
|
||||
fn load_handler(&self) -> Option<cef::LoadHandler> {
|
||||
Some(self.load_handler.clone())
|
||||
}
|
||||
|
||||
fn render_handler(&self) -> Option<RenderHandler> {
|
||||
Some(self.render_handler.clone())
|
||||
}
|
||||
|
||||
fn life_span_handler(&self) -> Option<cef::LifeSpanHandler> {
|
||||
Some(LifeSpanHandler::new(BrowserProcessLifeSpanHandlerImpl::new()))
|
||||
Some(LifeSpanHandler::new(LifeSpanHandlerImpl::new()))
|
||||
}
|
||||
|
||||
fn display_handler(&self) -> Option<cef::DisplayHandler> {
|
||||
Some(self.display_handler.clone())
|
||||
}
|
||||
|
||||
fn context_menu_handler(&self) -> Option<cef::ContextMenuHandler> {
|
||||
Some(ContextMenuHandler::new(ContextMenuHandlerImpl::new()))
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_client_t {
|
||||
@@ -70,8 +90,10 @@ impl<H: CefEventHandler> Clone for BrowserProcessClientImpl<H> {
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
event_handler: self.event_handler.duplicate(),
|
||||
load_handler: self.load_handler.clone(),
|
||||
render_handler: self.render_handler.clone(),
|
||||
event_handler: self.event_handler.clone(),
|
||||
display_handler: self.display_handler.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,9 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_browser_process_handler_t, cef_base_ref_counted_t, cef_browser_process_handler_t};
|
||||
use cef::{CefString, ImplBrowserProcessHandler, SchemeHandlerFactory, WrapBrowserProcessHandler};
|
||||
use cef::{CefString, ImplBrowserProcessHandler, WrapBrowserProcessHandler};
|
||||
|
||||
use crate::cef::CefEventHandler;
|
||||
use crate::cef::consts::GRAPHITE_SCHEME;
|
||||
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
|
||||
|
||||
pub(crate) struct BrowserProcessHandlerImpl<H: CefEventHandler> {
|
||||
object: *mut RcImpl<cef_browser_process_handler_t, Self>,
|
||||
@@ -21,16 +19,12 @@ impl<H: CefEventHandler> BrowserProcessHandlerImpl<H> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler + Clone> ImplBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
|
||||
fn on_context_initialized(&self) {
|
||||
cef::register_scheme_handler_factory(Some(&CefString::from(GRAPHITE_SCHEME)), None, Some(&mut SchemeHandlerFactory::new(GraphiteSchemeHandlerFactory::new())));
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> ImplBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
|
||||
fn on_schedule_message_pump_work(&self, delay_ms: i64) {
|
||||
self.event_handler.schedule_cef_message_loop_work(Instant::now() + Duration::from_millis(delay_ms as u64));
|
||||
}
|
||||
|
||||
fn on_already_running_app_relaunch(&self, _command_line: Option<&mut cef::CommandLine>, _current_directory: Option<&CefString>) -> ::std::os::raw::c_int {
|
||||
fn on_already_running_app_relaunch(&self, _command_line: Option<&mut cef::CommandLine>, _current_directory: Option<&CefString>) -> std::ffi::c_int {
|
||||
1 // Return 1 to prevent default behavior of opening a empty browser window
|
||||
}
|
||||
|
||||
@@ -39,7 +33,7 @@ impl<H: CefEventHandler + Clone> ImplBrowserProcessHandler for BrowserProcessHan
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler + Clone> Clone for BrowserProcessHandlerImpl<H> {
|
||||
impl<H: CefEventHandler> Clone for BrowserProcessHandlerImpl<H> {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
@@ -47,7 +41,7 @@ impl<H: CefEventHandler + Clone> Clone for BrowserProcessHandlerImpl<H> {
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
event_handler: self.event_handler.clone(),
|
||||
event_handler: self.event_handler.duplicate(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,7 +53,7 @@ impl<H: CefEventHandler> Rc for BrowserProcessHandlerImpl<H> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler + Clone> WrapBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
|
||||
impl<H: CefEventHandler> WrapBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_browser_process_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
|
||||
66
desktop/src/cef/internal/context_menu_handler.rs
Normal file
66
desktop/src/cef/internal/context_menu_handler.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_context_menu_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{ImplContextMenuHandler, WrapContextMenuHandler};
|
||||
|
||||
pub(crate) struct ContextMenuHandlerImpl {
|
||||
object: *mut RcImpl<_cef_context_menu_handler_t, Self>,
|
||||
}
|
||||
impl ContextMenuHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplContextMenuHandler for ContextMenuHandlerImpl {
|
||||
fn run_context_menu(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
_frame: Option<&mut cef::Frame>,
|
||||
_params: Option<&mut cef::ContextMenuParams>,
|
||||
_model: Option<&mut cef::MenuModel>,
|
||||
_callback: Option<&mut cef::RunContextMenuCallback>,
|
||||
) -> std::ffi::c_int {
|
||||
// Prevent context menu
|
||||
1
|
||||
}
|
||||
|
||||
fn run_quick_menu(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
_frame: Option<&mut cef::Frame>,
|
||||
_location: Option<&cef::Point>,
|
||||
_size: Option<&cef::Size>,
|
||||
_edit_state_flags: cef::QuickMenuEditStateFlags,
|
||||
_callback: Option<&mut cef::RunQuickMenuCallback>,
|
||||
) -> std::ffi::c_int {
|
||||
// Prevent quick menu
|
||||
1
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_context_menu_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ContextMenuHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for ContextMenuHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapContextMenuHandler for ContextMenuHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_context_menu_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
150
desktop/src/cef/internal/display_handler.rs
Normal file
150
desktop/src/cef/internal/display_handler.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
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_NOTALLOWED => CursorIcon::NotAllowed,
|
||||
CT_ZOOMIN => CursorIcon::ZoomIn,
|
||||
CT_ZOOMOUT => CursorIcon::ZoomOut,
|
||||
CT_GRAB => CursorIcon::Grab,
|
||||
CT_GRABBING => CursorIcon::Grabbing,
|
||||
CT_MIDDLE_PANNING_VERTICAL => CursorIcon::AllScroll,
|
||||
CT_MIDDLE_PANNING_HORIZONTAL => CursorIcon::AllScroll,
|
||||
CT_DND_NONE => CursorIcon::Default,
|
||||
CT_DND_MOVE => CursorIcon::Move,
|
||||
CT_DND_COPY => CursorIcon::Copy,
|
||||
CT_DND_LINK => CursorIcon::Alias,
|
||||
CT_NUM_VALUES => CursorIcon::Default,
|
||||
CT_NONE => {
|
||||
self.event_handler.cursor_change(crate::window::Cursor::None);
|
||||
return 1; // We handled the cursor change.
|
||||
}
|
||||
_ => CursorIcon::Default,
|
||||
};
|
||||
|
||||
self.event_handler.cursor_change(cursor.into());
|
||||
|
||||
1 // We handled the cursor change.
|
||||
}
|
||||
|
||||
fn on_console_message(&self, _browser: Option<&mut cef::Browser>, level: cef::LogSeverity, message: Option<&CefString>, source: Option<&CefString>, line: std::ffi::c_int) -> std::ffi::c_int {
|
||||
let message = message.map(|m| m.to_string()).unwrap_or_default();
|
||||
let source = source.map(|s| s.to_string()).unwrap_or_default();
|
||||
let line = line as i64;
|
||||
let browser_source = format!("{source}:{line}");
|
||||
static BROWSER: &str = "browser";
|
||||
match level.as_ref() {
|
||||
LOGSEVERITY_FATAL | LOGSEVERITY_ERROR => tracing::error!(target: BROWSER, "{browser_source} {message}"),
|
||||
LOGSEVERITY_WARNING => tracing::warn!(target: BROWSER, "{browser_source} {message}"),
|
||||
LOGSEVERITY_INFO => tracing::info!(target: BROWSER, "{browser_source} {message}"),
|
||||
LOGSEVERITY_DEFAULT | LOGSEVERITY_VERBOSE => tracing::debug!(target: BROWSER, "{browser_source} {message}"),
|
||||
_ => tracing::trace!(target: BROWSER, "{browser_source} {message}"),
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_display_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> Clone for DisplayHandlerImpl<H> {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
event_handler: self.event_handler.duplicate(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> Rc for DisplayHandlerImpl<H> {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> WrapDisplayHandler for DisplayHandlerImpl<H> {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_display_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -2,32 +2,32 @@ use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_life_span_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{ImplLifeSpanHandler, WrapLifeSpanHandler};
|
||||
|
||||
pub(crate) struct BrowserProcessLifeSpanHandlerImpl {
|
||||
pub(crate) struct LifeSpanHandlerImpl {
|
||||
object: *mut RcImpl<_cef_life_span_handler_t, Self>,
|
||||
}
|
||||
impl BrowserProcessLifeSpanHandlerImpl {
|
||||
impl LifeSpanHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplLifeSpanHandler for BrowserProcessLifeSpanHandlerImpl {
|
||||
impl ImplLifeSpanHandler for LifeSpanHandlerImpl {
|
||||
fn on_before_popup(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
_frame: Option<&mut cef::Frame>,
|
||||
_popup_id: ::std::os::raw::c_int,
|
||||
_popup_id: std::ffi::c_int,
|
||||
target_url: Option<&cef::CefString>,
|
||||
_target_frame_name: Option<&cef::CefString>,
|
||||
_target_disposition: cef::WindowOpenDisposition,
|
||||
_user_gesture: ::std::os::raw::c_int,
|
||||
_user_gesture: std::ffi::c_int,
|
||||
_popup_features: Option<&cef::PopupFeatures>,
|
||||
_window_info: Option<&mut cef::WindowInfo>,
|
||||
_client: Option<&mut Option<impl cef::ImplClient>>,
|
||||
_client: Option<&mut Option<cef::Client>>,
|
||||
_settings: Option<&mut cef::BrowserSettings>,
|
||||
_extra_info: Option<&mut Option<cef::DictionaryValue>>,
|
||||
_no_javascript_access: Option<&mut ::std::os::raw::c_int>,
|
||||
) -> ::std::os::raw::c_int {
|
||||
_no_javascript_access: Option<&mut std::ffi::c_int>,
|
||||
) -> std::ffi::c_int {
|
||||
let target = target_url.map(|url| url.to_string()).unwrap_or("unknown".to_string());
|
||||
tracing::error!("Browser tried to open a popup at URL: {}", target);
|
||||
|
||||
@@ -40,7 +40,7 @@ impl ImplLifeSpanHandler for BrowserProcessLifeSpanHandlerImpl {
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for BrowserProcessLifeSpanHandlerImpl {
|
||||
impl Clone for LifeSpanHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
@@ -49,7 +49,7 @@ impl Clone for BrowserProcessLifeSpanHandlerImpl {
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for BrowserProcessLifeSpanHandlerImpl {
|
||||
impl Rc for LifeSpanHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
@@ -57,7 +57,7 @@ impl Rc for BrowserProcessLifeSpanHandlerImpl {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapLifeSpanHandler for BrowserProcessLifeSpanHandlerImpl {
|
||||
impl WrapLifeSpanHandler for LifeSpanHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_life_span_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
60
desktop/src/cef/internal/load_handler.rs
Normal file
60
desktop/src/cef/internal/load_handler.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_load_handler_t, cef_base_ref_counted_t, cef_load_handler_t};
|
||||
use cef::{ImplBrowser, ImplBrowserHost, ImplLoadHandler, WrapLoadHandler};
|
||||
|
||||
use crate::cef::CefEventHandler;
|
||||
|
||||
pub(crate) struct LoadHandlerImpl<H: CefEventHandler> {
|
||||
object: *mut RcImpl<cef_load_handler_t, Self>,
|
||||
event_handler: H,
|
||||
}
|
||||
impl<H: CefEventHandler> LoadHandlerImpl<H> {
|
||||
pub(crate) fn new(event_handler: H) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
event_handler,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> ImplLoadHandler for LoadHandlerImpl<H> {
|
||||
fn on_loading_state_change(&self, browser: Option<&mut cef::Browser>, is_loading: std::ffi::c_int, _can_go_back: std::ffi::c_int, _can_go_forward: std::ffi::c_int) {
|
||||
let view_info = self.event_handler.view_info();
|
||||
|
||||
if let Some(browser) = browser
|
||||
&& is_loading == 0
|
||||
{
|
||||
browser.host().unwrap().set_zoom_level(view_info.zoom());
|
||||
}
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_load_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> Clone for LoadHandlerImpl<H> {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
event_handler: self.event_handler.duplicate(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> Rc for LoadHandlerImpl<H> {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> WrapLoadHandler for LoadHandlerImpl<H> {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_load_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ pub(crate) struct RenderHandlerImpl<H: CefEventHandler> {
|
||||
object: *mut RcImpl<_cef_render_handler_t, Self>,
|
||||
event_handler: H,
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> RenderHandlerImpl<H> {
|
||||
pub(crate) fn new(event_handler: H) -> Self {
|
||||
Self {
|
||||
@@ -18,29 +17,21 @@ impl<H: CefEventHandler> RenderHandlerImpl<H> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
|
||||
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
|
||||
if let Some(rect) = rect {
|
||||
let view = self.event_handler.window_size();
|
||||
let view_info = self.event_handler.view_info();
|
||||
*rect = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: view.width as i32,
|
||||
height: view.height as i32,
|
||||
width: view_info.width() as i32,
|
||||
height: view_info.height() as i32,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn on_paint(
|
||||
&self,
|
||||
_browser: Option<&mut Browser>,
|
||||
_type_: PaintElementType,
|
||||
_dirty_rect_count: usize,
|
||||
_dirty_rects: Option<&Rect>,
|
||||
buffer: *const u8,
|
||||
width: ::std::os::raw::c_int,
|
||||
height: ::std::os::raw::c_int,
|
||||
) {
|
||||
fn on_paint(&self, _browser: Option<&mut Browser>, _type_: PaintElementType, _dirty_rects: Option<&[Rect]>, buffer: *const u8, width: std::ffi::c_int, height: std::ffi::c_int) {
|
||||
let buffer_size = (width * height * 4) as usize;
|
||||
let buffer_slice = unsafe { std::slice::from_raw_parts(buffer, buffer_size) };
|
||||
let frame_buffer = FrameBufferRef::new(buffer_slice, width as usize, height as usize).expect("Failed to create frame buffer");
|
||||
@@ -49,8 +40,8 @@ impl<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
fn on_accelerated_paint(&self, _browser: Option<&mut Browser>, type_: PaintElementType, _dirty_rect_count: usize, _dirty_rects: Option<&Rect>, info: Option<&cef::AcceleratedPaintInfo>) {
|
||||
use crate::cef::texture_import::shared_texture_handle::SharedTextureHandle;
|
||||
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;
|
||||
@@ -78,7 +69,7 @@ impl<H: CefEventHandler> Clone for RenderHandlerImpl<H> {
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
event_handler: self.event_handler.clone(),
|
||||
event_handler: self.event_handler.duplicate(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@ use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
|
||||
use cef::{App, ImplApp, RenderProcessHandler, SchemeRegistrar, WrapApp};
|
||||
|
||||
use super::render_process_handler::RenderProcessHandlerImpl;
|
||||
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
|
||||
use super::scheme_handler_factory::SchemeHandlerFactoryImpl;
|
||||
use crate::cef::CefEventHandler;
|
||||
|
||||
pub(crate) struct RenderProcessAppImpl {
|
||||
pub(crate) struct RenderProcessAppImpl<H: CefEventHandler> {
|
||||
object: *mut RcImpl<_cef_app_t, Self>,
|
||||
render_process_handler: RenderProcessHandler,
|
||||
}
|
||||
impl RenderProcessAppImpl {
|
||||
impl<H: CefEventHandler> RenderProcessAppImpl<H> {
|
||||
pub(crate) fn app() -> App {
|
||||
App::new(Self {
|
||||
object: std::ptr::null_mut(),
|
||||
@@ -18,9 +19,9 @@ impl RenderProcessAppImpl {
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplApp for RenderProcessAppImpl {
|
||||
impl<H: CefEventHandler> ImplApp for RenderProcessAppImpl<H> {
|
||||
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
|
||||
GraphiteSchemeHandlerFactory::register_schemes(registrar);
|
||||
SchemeHandlerFactoryImpl::<H>::register_schemes(registrar);
|
||||
}
|
||||
|
||||
fn render_process_handler(&self) -> Option<RenderProcessHandler> {
|
||||
@@ -32,7 +33,7 @@ impl ImplApp for RenderProcessAppImpl {
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RenderProcessAppImpl {
|
||||
impl<H: CefEventHandler> Clone for RenderProcessAppImpl<H> {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
@@ -44,7 +45,7 @@ impl Clone for RenderProcessAppImpl {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for RenderProcessAppImpl {
|
||||
impl<H: CefEventHandler> Rc for RenderProcessAppImpl<H> {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
@@ -52,7 +53,7 @@ impl Rc for RenderProcessAppImpl {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapApp for RenderProcessAppImpl {
|
||||
impl<H: CefEventHandler> WrapApp for RenderProcessAppImpl<H> {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use cef::{CefString, ImplFrame, ImplRenderProcessHandler, ImplV8Context, ImplV8V
|
||||
|
||||
use crate::cef::ipc::{MessageType, UnpackMessage, UnpackedMessage};
|
||||
|
||||
use super::render_process_v8_handler::BrowserProcessV8HandlerImpl;
|
||||
use super::render_process_v8_handler::RenderProcessV8HandlerImpl;
|
||||
|
||||
pub(crate) struct RenderProcessHandlerImpl {
|
||||
object: *mut RcImpl<cef_render_process_handler_t, Self>,
|
||||
@@ -22,7 +22,7 @@ impl ImplRenderProcessHandler for RenderProcessHandlerImpl {
|
||||
frame: Option<&mut cef::Frame>,
|
||||
_source_process: cef::ProcessId,
|
||||
message: Option<&mut cef::ProcessMessage>,
|
||||
) -> ::std::os::raw::c_int {
|
||||
) -> std::ffi::c_int {
|
||||
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
|
||||
match unpacked_message {
|
||||
Some(UnpackedMessage {
|
||||
@@ -77,7 +77,7 @@ impl ImplRenderProcessHandler for RenderProcessHandlerImpl {
|
||||
|
||||
fn on_context_created(&self, _browser: Option<&mut cef::Browser>, _frame: Option<&mut cef::Frame>, context: Option<&mut cef::V8Context>) {
|
||||
let register_js_function = |context: &mut cef::V8Context, name: &'static str| {
|
||||
let mut v8_handler = V8Handler::new(BrowserProcessV8HandlerImpl::new());
|
||||
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;
|
||||
|
||||
@@ -2,17 +2,16 @@ use cef::{ImplV8Handler, ImplV8Value, V8Value, WrapV8Handler, rc::Rc, v8_context
|
||||
|
||||
use crate::cef::ipc::{MessageType, SendMessage};
|
||||
|
||||
pub struct BrowserProcessV8HandlerImpl {
|
||||
pub struct RenderProcessV8HandlerImpl {
|
||||
object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>,
|
||||
}
|
||||
|
||||
impl BrowserProcessV8HandlerImpl {
|
||||
impl RenderProcessV8HandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplV8Handler for BrowserProcessV8HandlerImpl {
|
||||
impl ImplV8Handler for RenderProcessV8HandlerImpl {
|
||||
fn execute(
|
||||
&self,
|
||||
name: Option<&cef::CefString>,
|
||||
@@ -20,7 +19,7 @@ impl ImplV8Handler for BrowserProcessV8HandlerImpl {
|
||||
arguments: Option<&[Option<V8Value>]>,
|
||||
_retval: Option<&mut Option<V8Value>>,
|
||||
_exception: Option<&mut cef::CefString>,
|
||||
) -> ::std::os::raw::c_int {
|
||||
) -> std::ffi::c_int {
|
||||
match name.map(|s| s.to_string()).unwrap_or_default().as_str() {
|
||||
"initializeNativeCommunication" => {
|
||||
v8_context_get_current_context().send_message(MessageType::Initialized, vec![0u8].as_slice());
|
||||
@@ -63,7 +62,7 @@ impl ImplV8Handler for BrowserProcessV8HandlerImpl {
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for BrowserProcessV8HandlerImpl {
|
||||
impl Clone for RenderProcessV8HandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
@@ -72,8 +71,7 @@ impl Clone for BrowserProcessV8HandlerImpl {
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
|
||||
impl Rc for BrowserProcessV8HandlerImpl {
|
||||
impl Rc for RenderProcessV8HandlerImpl {
|
||||
fn as_base(&self) -> &cef::sys::cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
@@ -81,8 +79,7 @@ impl Rc for BrowserProcessV8HandlerImpl {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WrapV8Handler for BrowserProcessV8HandlerImpl {
|
||||
impl WrapV8Handler for RenderProcessV8HandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
|
||||
108
desktop/src/cef/internal/resource_handler.rs
Normal file
108
desktop/src/cef/internal/resource_handler.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_resource_handler_t, cef_base_ref_counted_t};
|
||||
use cef::{Callback, CefString, ImplResourceHandler, ImplResponse, Request, ResourceReadCallback, Response, WrapResourceHandler};
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::c_int;
|
||||
use std::io::Read;
|
||||
|
||||
use crate::cef::{Resource, ResourceReader};
|
||||
|
||||
pub(crate) struct ResourceHandlerImpl {
|
||||
object: *mut RcImpl<_cef_resource_handler_t, Self>,
|
||||
reader: Option<RefCell<ResourceReader>>,
|
||||
mimetype: Option<String>,
|
||||
}
|
||||
|
||||
impl ResourceHandlerImpl {
|
||||
pub fn new(resource: Option<Resource>) -> Self {
|
||||
if let Some(resource) = resource {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
reader: Some(resource.reader.into()),
|
||||
mimetype: resource.mimetype,
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
reader: None,
|
||||
mimetype: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplResourceHandler for ResourceHandlerImpl {
|
||||
fn open(&self, _request: Option<&mut Request>, handle_request: Option<&mut c_int>, _callback: Option<&mut Callback>) -> c_int {
|
||||
if let Some(handle_request) = handle_request {
|
||||
*handle_request = 1;
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
fn response_headers(&self, response: Option<&mut Response>, response_length: Option<&mut i64>, _redirect_url: Option<&mut CefString>) {
|
||||
if let Some(response_length) = response_length {
|
||||
*response_length = -1; // Indicating that the length is unknown
|
||||
}
|
||||
if let Some(response) = response {
|
||||
if self.reader.is_some() {
|
||||
if let Some(mimetype) = &self.mimetype {
|
||||
let cef_mime = CefString::from(mimetype.as_str());
|
||||
response.set_mime_type(Some(&cef_mime));
|
||||
} else {
|
||||
response.set_mime_type(None);
|
||||
}
|
||||
response.set_status(200);
|
||||
} else {
|
||||
response.set_status(404);
|
||||
response.set_mime_type(Some(&CefString::from("text/plain")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&self, data_out: *mut u8, bytes_to_read: c_int, bytes_read: Option<&mut c_int>, _callback: Option<&mut ResourceReadCallback>) -> c_int {
|
||||
let Some(bytes_read) = bytes_read else { unreachable!() };
|
||||
let out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read as usize) };
|
||||
if let Some(reader) = &self.reader {
|
||||
if let Ok(read) = reader.borrow_mut().read(out) {
|
||||
*bytes_read = read as i32;
|
||||
if read > 0 {
|
||||
return 1; // Indicating that data was read
|
||||
}
|
||||
} else {
|
||||
*bytes_read = -2; // Indicating ERR_FAILED
|
||||
}
|
||||
}
|
||||
0 // Indicating no data was read
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_resource_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ResourceHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
reader: self.reader.clone(),
|
||||
mimetype: self.mimetype.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for ResourceHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapResourceHandler for ResourceHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_resource_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
74
desktop/src/cef/internal/scheme_handler_factory.rs
Normal file
74
desktop/src/cef/internal/scheme_handler_factory.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_scheme_handler_factory_t, cef_base_ref_counted_t, cef_scheme_options_t};
|
||||
use cef::{Browser, CefString, Frame, ImplRequest, ImplSchemeHandlerFactory, ImplSchemeRegistrar, Request, ResourceHandler, SchemeRegistrar, WrapSchemeHandlerFactory};
|
||||
|
||||
use super::resource_handler::ResourceHandlerImpl;
|
||||
use crate::cef::CefEventHandler;
|
||||
use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
|
||||
|
||||
pub(crate) struct SchemeHandlerFactoryImpl<H: CefEventHandler> {
|
||||
object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>,
|
||||
event_handler: H,
|
||||
}
|
||||
impl<H: CefEventHandler> SchemeHandlerFactoryImpl<H> {
|
||||
pub(crate) fn new(event_handler: H) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
event_handler,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn register_schemes(registrar: Option<&mut SchemeRegistrar>) {
|
||||
if let Some(registrar) = registrar {
|
||||
let mut scheme_options = 0;
|
||||
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32;
|
||||
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32;
|
||||
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32;
|
||||
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32;
|
||||
registrar.add_custom_scheme(Some(&CefString::from(RESOURCE_SCHEME)), scheme_options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> ImplSchemeHandlerFactory for SchemeHandlerFactoryImpl<H> {
|
||||
fn create(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, _scheme_name: Option<&CefString>, request: Option<&mut Request>) -> Option<ResourceHandler> {
|
||||
if let Some(request) = request {
|
||||
let url = CefString::from(&request.url()).to_string();
|
||||
let path = url
|
||||
.strip_prefix(&format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/"))
|
||||
.expect("CEF should only call this for our custom scheme and domain that we registered this factory for");
|
||||
let resource = self.event_handler.load_resource(path.to_string().into());
|
||||
return Some(ResourceHandler::new(ResourceHandlerImpl::new(resource)));
|
||||
}
|
||||
None
|
||||
}
|
||||
fn get_raw(&self) -> *mut _cef_scheme_handler_factory_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> Clone for SchemeHandlerFactoryImpl<H> {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
event_handler: self.event_handler.duplicate(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> Rc for SchemeHandlerFactoryImpl<H> {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> WrapSchemeHandlerFactory for SchemeHandlerFactoryImpl<H> {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
use super::consts::{FRONTEND_DOMAIN, GRAPHITE_SCHEME};
|
||||
|
||||
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()),
|
||||
"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,
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
//! Common utilities and traits for texture import across platforms
|
||||
|
||||
use crate::cef::texture_import::*;
|
||||
use ash::vk;
|
||||
use cef::sys::cef_color_type_t;
|
||||
use wgpu::Device;
|
||||
|
||||
/// Common format conversion utilities
|
||||
pub mod format {
|
||||
use super::*;
|
||||
|
||||
/// Convert CEF color type to wgpu texture format
|
||||
pub fn cef_to_wgpu(format: cef_color_type_t) -> Result<wgpu::TextureFormat, TextureImportError> {
|
||||
match format {
|
||||
cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(wgpu::TextureFormat::Bgra8UnormSrgb),
|
||||
cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(wgpu::TextureFormat::Rgba8UnormSrgb),
|
||||
_ => Err(TextureImportError::UnsupportedFormat { format }),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
/// Convert CEF color type to Vulkan format
|
||||
pub fn cef_to_vulkan(format: cef_color_type_t) -> Result<vk::Format, TextureImportError> {
|
||||
match format {
|
||||
cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(vk::Format::B8G8R8A8_UNORM),
|
||||
cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(vk::Format::R8G8B8A8_UNORM),
|
||||
_ => Err(TextureImportError::UnsupportedFormat { format }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Common texture creation utilities
|
||||
pub mod texture {
|
||||
use super::*;
|
||||
|
||||
/// Create a fallback CPU texture with the given dimensions and format
|
||||
pub fn create_fallback(device: &Device, width: u32, height: u32, format: cef_color_type_t, label: &str) -> TextureImportResult {
|
||||
let wgpu_format = format::cef_to_wgpu(format)?;
|
||||
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some(label),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu_format,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
tracing::warn!(
|
||||
"Using fallback CPU texture for CEF rendering ({}x{}, {:?}) - hardware acceleration failed or unavailable. Consider checking GPU driver support.",
|
||||
width,
|
||||
height,
|
||||
format
|
||||
);
|
||||
Ok(texture)
|
||||
}
|
||||
}
|
||||
|
||||
/// Common Vulkan utilities
|
||||
pub mod vulkan {
|
||||
use super::*;
|
||||
|
||||
/// Find a suitable memory type index for Vulkan allocation
|
||||
pub fn find_memory_type_index(type_filter: u32, properties: vk::MemoryPropertyFlags, mem_properties: &vk::PhysicalDeviceMemoryProperties) -> Option<u32> {
|
||||
(0..mem_properties.memory_type_count).find(|&i| (type_filter & (1 << i)) != 0 && mem_properties.memory_types[i as usize].property_flags.contains(properties))
|
||||
}
|
||||
|
||||
/// Check if the wgpu device is using Vulkan backend
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn is_vulkan_backend(device: &Device) -> bool {
|
||||
use wgpu::hal::api;
|
||||
let mut is_vulkan = false;
|
||||
unsafe {
|
||||
device.as_hal::<api::Vulkan, _, _>(|device| {
|
||||
is_vulkan = device.is_some();
|
||||
});
|
||||
}
|
||||
is_vulkan
|
||||
}
|
||||
|
||||
/// Check if the wgpu device is using D3D12 backend
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn is_d3d12_backend(device: &Device) -> bool {
|
||||
use wgpu::hal::api;
|
||||
let mut is_d3d12 = false;
|
||||
unsafe {
|
||||
device.as_hal::<api::Dx12, _, _>(|device| {
|
||||
is_d3d12 = device.is_some();
|
||||
});
|
||||
}
|
||||
is_d3d12
|
||||
}
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
//! Windows D3D11 shared texture import implementation
|
||||
|
||||
use super::common::{format, texture, vulkan};
|
||||
use super::{TextureImportError, TextureImportResult, TextureImporter};
|
||||
use ash::vk;
|
||||
use cef::{AcceleratedPaintInfo, sys::cef_color_type_t};
|
||||
use std::os::raw::c_void;
|
||||
use wgpu::hal::api;
|
||||
|
||||
pub struct D3D11Importer {
|
||||
pub handle: *mut c_void,
|
||||
pub format: cef_color_type_t,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl TextureImporter for D3D11Importer {
|
||||
fn new(info: &AcceleratedPaintInfo) -> Self {
|
||||
Self {
|
||||
handle: info.shared_texture_handle,
|
||||
format: *info.format.as_ref(),
|
||||
width: info.extra.coded_size.width as u32,
|
||||
height: info.extra.coded_size.height as u32,
|
||||
}
|
||||
}
|
||||
|
||||
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
// Try hardware acceleration first
|
||||
if self.supports_hardware_acceleration(device) {
|
||||
// Try D3D12 first (most efficient on Windows)
|
||||
if vulkan::is_d3d12_backend(device) {
|
||||
match self.import_via_d3d12(device) {
|
||||
Ok(texture) => {
|
||||
tracing::info!("Successfully imported D3D11 shared texture via D3D12");
|
||||
return Ok(texture);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to import D3D11 via D3D12: {}, trying Vulkan fallback", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try Vulkan as fallback
|
||||
if vulkan::is_vulkan_backend(device) {
|
||||
match self.import_via_vulkan(device) {
|
||||
Ok(texture) => {
|
||||
tracing::info!("Successfully imported D3D11 shared texture via Vulkan");
|
||||
return Ok(texture);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to import D3D11 via Vulkan: {}, falling back to CPU texture", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to CPU texture
|
||||
texture::create_fallback(device, self.width, self.height, self.format, "CEF D3D11 Texture (fallback)")
|
||||
}
|
||||
|
||||
fn supports_hardware_acceleration(&self, device: &wgpu::Device) -> bool {
|
||||
// Check if handle is valid
|
||||
if self.handle.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if wgpu is using D3D12 or Vulkan backend
|
||||
vulkan::is_d3d12_backend(device) || vulkan::is_vulkan_backend(device)
|
||||
}
|
||||
}
|
||||
|
||||
impl D3D11Importer {
|
||||
fn import_via_d3d12(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
// Get wgpu's D3D12 device
|
||||
use wgpu::hal::api;
|
||||
let hal_texture = unsafe {
|
||||
device.as_hal::<api::Dx12, _, _>(|device| {
|
||||
let Some(device) = device else {
|
||||
return Err(TextureImportError::HardwareUnavailable {
|
||||
reason: "Device is not using D3D12 backend".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
// Import D3D11 shared handle directly into D3D12 resource
|
||||
let d3d12_resource = self.import_d3d11_handle_to_d3d12(device)?;
|
||||
|
||||
// Wrap D3D12 resource in wgpu-hal texture
|
||||
let hal_texture = <api::Dx12 as wgpu::hal::Api>::Device::texture_from_raw(
|
||||
d3d12_resource,
|
||||
format::cef_to_wgpu(self.format)?,
|
||||
wgpu::TextureDimension::D2,
|
||||
wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
1, // mip_level_count
|
||||
1, // sample_count
|
||||
);
|
||||
|
||||
Ok(hal_texture)
|
||||
})
|
||||
}?;
|
||||
|
||||
// Import hal texture into wgpu
|
||||
let texture = unsafe {
|
||||
device.create_texture_from_hal::<api::Dx12>(
|
||||
hal_texture,
|
||||
&wgpu::TextureDescriptor {
|
||||
label: Some("CEF D3D11→D3D12 Shared Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: format::cef_to_wgpu(self.format)?,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
fn import_via_vulkan(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
// Get wgpu's Vulkan instance and device
|
||||
use wgpu::{TextureUses, wgc::api::Vulkan};
|
||||
let hal_texture = unsafe {
|
||||
device.as_hal::<api::Vulkan, _, _>(|device| {
|
||||
let Some(device) = device else {
|
||||
return Err(TextureImportError::HardwareUnavailable {
|
||||
reason: "Device is not using Vulkan backend".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
// Import D3D11 shared handle into Vulkan
|
||||
let vk_image = self.import_d3d11_handle_to_vulkan(device)?;
|
||||
|
||||
// Wrap VkImage in wgpu-hal texture
|
||||
let hal_texture = <api::Vulkan as wgpu::hal::Api>::Device::texture_from_raw(
|
||||
vk_image,
|
||||
&wgpu::hal::TextureDescriptor {
|
||||
label: Some("CEF D3D11 Shared Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: format::cef_to_wgpu(self.format)?,
|
||||
usage: TextureUses::COPY_DST | TextureUses::RESOURCE,
|
||||
memory_flags: wgpu::hal::MemoryFlags::empty(),
|
||||
view_formats: vec![],
|
||||
},
|
||||
None, // drop_callback
|
||||
);
|
||||
|
||||
Ok(hal_texture)
|
||||
})
|
||||
}?;
|
||||
|
||||
// Import hal texture into wgpu
|
||||
let texture = unsafe {
|
||||
device.create_texture_from_hal::<Vulkan>(
|
||||
hal_texture,
|
||||
&wgpu::TextureDescriptor {
|
||||
label: Some("CEF D3D11 Shared Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: format::cef_to_wgpu(self.format)?,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
fn import_d3d11_handle_to_vulkan(&self, hal_device: &<api::Vulkan as wgpu::hal::Api>::Device) -> Result<vk::Image, TextureImportError> {
|
||||
// Get raw Vulkan handles
|
||||
let device = hal_device.raw_device();
|
||||
let _instance = hal_device.shared_instance().raw_instance();
|
||||
|
||||
// Validate dimensions
|
||||
if self.width == 0 || self.height == 0 {
|
||||
return Err(TextureImportError::InvalidHandle("Invalid D3D11 texture dimensions".to_string()));
|
||||
}
|
||||
|
||||
// Create external memory image info
|
||||
let mut external_memory_info = vk::ExternalMemoryImageCreateInfo::default().handle_types(vk::ExternalMemoryHandleTypeFlags::D3D11_TEXTURE);
|
||||
|
||||
// Create image create info
|
||||
let image_create_info = vk::ImageCreateInfo::default()
|
||||
.image_type(vk::ImageType::TYPE_2D)
|
||||
.format(format::cef_to_vulkan(self.format)?)
|
||||
.extent(vk::Extent3D {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth: 1,
|
||||
})
|
||||
.mip_levels(1)
|
||||
.array_layers(1)
|
||||
.samples(vk::SampleCountFlags::TYPE_1)
|
||||
.tiling(vk::ImageTiling::OPTIMAL)
|
||||
.usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::COLOR_ATTACHMENT)
|
||||
.sharing_mode(vk::SharingMode::EXCLUSIVE)
|
||||
.push_next(&mut external_memory_info);
|
||||
|
||||
// Create the image
|
||||
let image = unsafe {
|
||||
device.create_image(&image_create_info, None).map_err(|e| TextureImportError::VulkanError {
|
||||
operation: format!("Failed to create Vulkan image: {:?}", e),
|
||||
})?
|
||||
};
|
||||
|
||||
// Get memory requirements
|
||||
let memory_requirements = unsafe { device.get_image_memory_requirements(image) };
|
||||
|
||||
// Import D3D11 handle
|
||||
let mut import_memory_win32 = vk::ImportMemoryWin32HandleInfoKHR::default()
|
||||
.handle_type(vk::ExternalMemoryHandleTypeFlags::D3D11_TEXTURE)
|
||||
.handle(self.handle as isize);
|
||||
|
||||
// Find a suitable memory type
|
||||
let memory_properties = unsafe { hal_device.shared_instance().raw_instance().get_physical_device_memory_properties(hal_device.raw_physical_device()) };
|
||||
|
||||
let memory_type_index =
|
||||
vulkan::find_memory_type_index(memory_requirements.memory_type_bits, vk::MemoryPropertyFlags::empty(), &memory_properties).ok_or_else(|| TextureImportError::VulkanError {
|
||||
operation: "Failed to find suitable memory type for D3D11 texture".to_string(),
|
||||
})?;
|
||||
|
||||
let allocate_info = vk::MemoryAllocateInfo::default()
|
||||
.allocation_size(memory_requirements.size)
|
||||
.memory_type_index(memory_type_index)
|
||||
.push_next(&mut import_memory_win32);
|
||||
|
||||
let device_memory = unsafe {
|
||||
device.allocate_memory(&allocate_info, None).map_err(|e| TextureImportError::VulkanError {
|
||||
operation: format!("Failed to allocate memory for D3D11 texture: {:?}", e),
|
||||
})?
|
||||
};
|
||||
|
||||
// Bind memory to image
|
||||
unsafe {
|
||||
device.bind_image_memory(image, device_memory, 0).map_err(|e| TextureImportError::VulkanError {
|
||||
operation: format!("Failed to bind memory to image: {:?}", e),
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(image)
|
||||
}
|
||||
|
||||
fn import_d3d11_handle_to_d3d12(&self, hal_device: &<wgpu::hal::api::Dx12 as wgpu::hal::Api>::Device) -> Result<windows::Win32::Graphics::Direct3D12::ID3D12Resource, TextureImportError> {
|
||||
use windows::Win32::Graphics::Direct3D12::*;
|
||||
use windows::core::*;
|
||||
|
||||
// Get D3D12 device from wgpu-hal
|
||||
let d3d12_device = hal_device.raw_device();
|
||||
|
||||
// Validate dimensions
|
||||
if self.width == 0 || self.height == 0 {
|
||||
return Err(TextureImportError::InvalidHandle("Invalid D3D11 texture dimensions".to_string()));
|
||||
}
|
||||
|
||||
// Open D3D11 shared handle on D3D12 device
|
||||
unsafe {
|
||||
let mut shared_resource: Option<ID3D12Resource> = None;
|
||||
d3d12_device
|
||||
.OpenSharedHandle(windows::Win32::Foundation::HANDLE(self.handle), &mut shared_resource)
|
||||
.map_err(|e| TextureImportError::PlatformError {
|
||||
message: format!("Failed to open D3D11 shared handle on D3D12: {:?}", e),
|
||||
})?;
|
||||
|
||||
shared_resource.ok_or_else(|| TextureImportError::InvalidHandle("Failed to get D3D12 resource from shared handle".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
//! Linux DMA-BUF texture import implementation
|
||||
|
||||
use super::common::{format, texture, vulkan};
|
||||
use super::{TextureImportError, TextureImportResult, TextureImporter};
|
||||
use ash::vk;
|
||||
use cef::{AcceleratedPaintInfo, sys::cef_color_type_t};
|
||||
use wgpu::hal::api;
|
||||
|
||||
pub(crate) struct DmaBufImporter {
|
||||
fds: Vec<std::os::fd::RawFd>,
|
||||
format: cef_color_type_t,
|
||||
modifier: u64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
strides: Vec<u32>,
|
||||
offsets: Vec<u32>,
|
||||
}
|
||||
|
||||
impl TextureImporter for DmaBufImporter {
|
||||
fn new(info: &AcceleratedPaintInfo) -> Self {
|
||||
Self {
|
||||
fds: extract_fds_from_info(info),
|
||||
format: *info.format.as_ref(),
|
||||
modifier: info.modifier,
|
||||
width: info.extra.coded_size.width as u32,
|
||||
height: info.extra.coded_size.height as u32,
|
||||
strides: extract_strides_from_info(info),
|
||||
offsets: extract_offsets_from_info(info),
|
||||
}
|
||||
}
|
||||
|
||||
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
// Try hardware acceleration first
|
||||
if self.supports_hardware_acceleration(device) {
|
||||
match self.import_via_vulkan(device) {
|
||||
Ok(texture) => {
|
||||
tracing::info!("Successfully imported DMA-BUF texture via Vulkan");
|
||||
return Ok(texture);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to import DMA-BUF via Vulkan: {}, falling back to CPU texture", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to CPU texture
|
||||
texture::create_fallback(device, self.width, self.height, self.format, "CEF DMA-BUF Texture (fallback)")
|
||||
}
|
||||
|
||||
fn supports_hardware_acceleration(&self, device: &wgpu::Device) -> bool {
|
||||
// Check if we have valid file descriptors
|
||||
if self.fds.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
for &fd in &self.fds {
|
||||
if fd < 0 {
|
||||
return false;
|
||||
}
|
||||
// Check if file descriptor is valid
|
||||
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
|
||||
if flags == -1 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if wgpu is using Vulkan backend
|
||||
vulkan::is_vulkan_backend(device)
|
||||
}
|
||||
}
|
||||
|
||||
impl DmaBufImporter {
|
||||
fn import_via_vulkan(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
// Get wgpu's Vulkan instance and device
|
||||
use wgpu::{TextureUses, wgc::api::Vulkan};
|
||||
let hal_texture = unsafe {
|
||||
device.as_hal::<api::Vulkan, _, _>(|device| {
|
||||
let Some(device) = device else {
|
||||
return Err(TextureImportError::HardwareUnavailable {
|
||||
reason: "Device is not using Vulkan backend".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
// Create VkImage from DMA-BUF using external memory
|
||||
let vk_image = self.create_vulkan_image_from_dmabuf(device)?;
|
||||
|
||||
// Wrap VkImage in wgpu-hal texture
|
||||
let hal_texture = <api::Vulkan as wgpu::hal::Api>::Device::texture_from_raw(
|
||||
vk_image,
|
||||
&wgpu::hal::TextureDescriptor {
|
||||
label: Some("CEF DMA-BUF Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: format::cef_to_wgpu(self.format)?,
|
||||
usage: TextureUses::COPY_DST | TextureUses::RESOURCE,
|
||||
memory_flags: wgpu::hal::MemoryFlags::empty(),
|
||||
view_formats: vec![],
|
||||
},
|
||||
None, // drop_callback
|
||||
);
|
||||
|
||||
Ok(hal_texture)
|
||||
})
|
||||
}?;
|
||||
|
||||
// Import hal texture into wgpu
|
||||
let texture = unsafe {
|
||||
device.create_texture_from_hal::<Vulkan>(
|
||||
hal_texture,
|
||||
&wgpu::TextureDescriptor {
|
||||
label: Some("CEF DMA-BUF Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: format::cef_to_wgpu(self.format)?,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
fn create_vulkan_image_from_dmabuf(&self, hal_device: &<api::Vulkan as wgpu::hal::Api>::Device) -> Result<vk::Image, TextureImportError> {
|
||||
// Get raw Vulkan handles
|
||||
let device = hal_device.raw_device();
|
||||
let _instance = hal_device.shared_instance().raw_instance();
|
||||
|
||||
// Validate dimensions
|
||||
if self.width == 0 || self.height == 0 {
|
||||
return Err(TextureImportError::InvalidHandle("Invalid DMA-BUF dimensions".to_string()));
|
||||
}
|
||||
|
||||
// Create external memory image
|
||||
let image_create_info = vk::ImageCreateInfo::default()
|
||||
.image_type(vk::ImageType::TYPE_2D)
|
||||
.format(format::cef_to_vulkan(self.format)?)
|
||||
.extent(vk::Extent3D {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth: 1,
|
||||
})
|
||||
.mip_levels(1)
|
||||
.array_layers(1)
|
||||
.samples(vk::SampleCountFlags::TYPE_1)
|
||||
.tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT)
|
||||
.usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::COLOR_ATTACHMENT)
|
||||
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
||||
|
||||
// Set up DRM format modifier
|
||||
let plane_layouts = self.create_subresource_layouts()?;
|
||||
let mut drm_format_modifier = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default()
|
||||
.drm_format_modifier(self.modifier)
|
||||
.plane_layouts(&plane_layouts);
|
||||
|
||||
let image_create_info = image_create_info.push_next(&mut drm_format_modifier);
|
||||
|
||||
// Create the image
|
||||
let image = unsafe {
|
||||
device.create_image(&image_create_info, None).map_err(|e| TextureImportError::VulkanError {
|
||||
operation: format!("Failed to create Vulkan image: {e:?}"),
|
||||
})?
|
||||
};
|
||||
|
||||
// Import memory from DMA-BUF
|
||||
let memory_requirements = unsafe { device.get_image_memory_requirements(image) };
|
||||
|
||||
// Duplicate the file descriptor to avoid ownership issues
|
||||
let dup_fd = unsafe { libc::dup(self.fds[0]) };
|
||||
if dup_fd == -1 {
|
||||
return Err(TextureImportError::PlatformError {
|
||||
message: "Failed to duplicate DMA-BUF file descriptor".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut import_memory_fd = vk::ImportMemoryFdInfoKHR::default().handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT).fd(dup_fd);
|
||||
|
||||
// Find a suitable memory type
|
||||
let memory_properties = unsafe { hal_device.shared_instance().raw_instance().get_physical_device_memory_properties(hal_device.raw_physical_device()) };
|
||||
|
||||
let memory_type_index =
|
||||
vulkan::find_memory_type_index(memory_requirements.memory_type_bits, vk::MemoryPropertyFlags::empty(), &memory_properties).ok_or_else(|| TextureImportError::VulkanError {
|
||||
operation: "Failed to find suitable memory type for DMA-BUF".to_string(),
|
||||
})?;
|
||||
|
||||
let allocate_info = vk::MemoryAllocateInfo::default()
|
||||
.allocation_size(memory_requirements.size)
|
||||
.memory_type_index(memory_type_index)
|
||||
.push_next(&mut import_memory_fd);
|
||||
|
||||
let device_memory = unsafe {
|
||||
device.allocate_memory(&allocate_info, None).map_err(|e| TextureImportError::VulkanError {
|
||||
operation: format!("Failed to allocate memory for DMA-BUF: {e:?}"),
|
||||
})?
|
||||
};
|
||||
|
||||
// Bind memory to image
|
||||
unsafe {
|
||||
device.bind_image_memory(image, device_memory, 0).map_err(|e| TextureImportError::VulkanError {
|
||||
operation: format!("Failed to bind memory to image: {e:?}"),
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(image)
|
||||
}
|
||||
|
||||
fn create_subresource_layouts(&self) -> Result<Vec<vk::SubresourceLayout>, TextureImportError> {
|
||||
let mut layouts = Vec::new();
|
||||
|
||||
for i in 0..self.fds.len() {
|
||||
layouts.push(vk::SubresourceLayout {
|
||||
offset: self.offsets.get(i).copied().unwrap_or(0) as u64,
|
||||
size: 0, // Will be calculated by driver
|
||||
row_pitch: self.strides.get(i).copied().unwrap_or(0) as u64,
|
||||
array_pitch: 0,
|
||||
depth_pitch: 0,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(layouts)
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_fds_from_info(info: &cef::AcceleratedPaintInfo) -> Vec<std::os::fd::RawFd> {
|
||||
let plane_count = info.plane_count as usize;
|
||||
let mut fds = Vec::with_capacity(plane_count);
|
||||
|
||||
for i in 0..plane_count {
|
||||
if let Some(plane) = info.planes.get(i) {
|
||||
fds.push(plane.fd);
|
||||
}
|
||||
}
|
||||
|
||||
fds
|
||||
}
|
||||
|
||||
fn extract_strides_from_info(info: &cef::AcceleratedPaintInfo) -> Vec<u32> {
|
||||
let plane_count = info.plane_count as usize;
|
||||
let mut strides = Vec::with_capacity(plane_count);
|
||||
|
||||
for i in 0..plane_count {
|
||||
if let Some(plane) = info.planes.get(i) {
|
||||
strides.push(plane.stride);
|
||||
}
|
||||
}
|
||||
|
||||
strides
|
||||
}
|
||||
|
||||
fn extract_offsets_from_info(info: &cef::AcceleratedPaintInfo) -> Vec<u32> {
|
||||
let plane_count = info.plane_count as usize;
|
||||
let mut offsets = Vec::with_capacity(plane_count);
|
||||
|
||||
for i in 0..plane_count {
|
||||
if let Some(plane) = info.planes.get(i) {
|
||||
offsets.push(plane.offset as u32);
|
||||
}
|
||||
}
|
||||
|
||||
offsets
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
//! macOS IOSurface texture import implementation
|
||||
|
||||
use super::common::{format, texture};
|
||||
use super::{TextureImportError, TextureImportResult, TextureImporter};
|
||||
use cef::{AcceleratedPaintInfo, sys::cef_color_type_t};
|
||||
use core_foundation::base::{CFType, TCFType};
|
||||
use objc2_io_surface::{IOSurface, IOSurfaceRef};
|
||||
use objc2_metal::{MTLDevice, MTLPixelFormat, MTLTexture, MTLTextureDescriptor, MTLTextureType, MTLTextureUsage};
|
||||
use std::os::raw::c_void;
|
||||
use wgpu::hal::api;
|
||||
|
||||
pub struct IOSurfaceImporter {
|
||||
pub handle: *mut c_void,
|
||||
pub format: cef_color_type_t,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl TextureImporter for IOSurfaceImporter {
|
||||
fn new(info: &AcceleratedPaintInfo) -> Self {
|
||||
Self {
|
||||
handle: info.shared_texture_handle,
|
||||
format: *info.format.as_ref(),
|
||||
width: info.extra.coded_size.width as u32,
|
||||
height: info.extra.coded_size.height as u32,
|
||||
}
|
||||
}
|
||||
|
||||
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
// Try hardware acceleration first
|
||||
if self.supports_hardware_acceleration(device) {
|
||||
match self.import_via_metal(device) {
|
||||
Ok(texture) => {
|
||||
tracing::trace!("Successfully imported IOSurface texture via Metal");
|
||||
return Ok(texture);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to import IOSurface via Metal: {}, falling back to CPU texture", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to CPU texture
|
||||
texture::create_fallback(device, self.width, self.height, self.format, "CEF IOSurface Texture (fallback)")
|
||||
}
|
||||
|
||||
fn supports_hardware_acceleration(&self, device: &wgpu::Device) -> bool {
|
||||
// Check if handle is valid
|
||||
if self.handle.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if wgpu is using Metal backend
|
||||
self.is_metal_backend(device)
|
||||
}
|
||||
}
|
||||
|
||||
impl IOSurfaceImporter {
|
||||
fn import_via_metal(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
// Get wgpu's Metal device
|
||||
use wgpu::{hal::Api, wgc::api::Metal};
|
||||
let hal_texture = unsafe {
|
||||
device.as_hal::<api::Metal, _, _>(|device| {
|
||||
let Some(device) = device else {
|
||||
return Err(TextureImportError::HardwareUnavailable {
|
||||
reason: "Device is not using Metal backend".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
// Import IOSurface handle into Metal texture
|
||||
let metal_texture = self.import_iosurface_to_metal(device)?;
|
||||
|
||||
// Wrap Metal texture in wgpu-hal texture
|
||||
let hal_texture = <api::Metal as wgpu::hal::Api>::Device::texture_from_raw(
|
||||
metal_texture,
|
||||
&wgpu::hal::TextureDescriptor {
|
||||
label: Some("CEF IOSurface Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: format::cef_to_wgpu(self.format)?,
|
||||
usage: wgpu::hal::TextureUses::RESOURCE,
|
||||
memory_flags: wgpu::hal::MemoryFlags::empty(),
|
||||
view_formats: vec![],
|
||||
},
|
||||
None, // drop_callback
|
||||
);
|
||||
|
||||
Ok(hal_texture)
|
||||
})
|
||||
}?;
|
||||
|
||||
// Import hal texture into wgpu
|
||||
let texture = unsafe {
|
||||
device.create_texture_from_hal::<Metal>(
|
||||
hal_texture,
|
||||
&wgpu::TextureDescriptor {
|
||||
label: Some("CEF IOSurface Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: format::cef_to_wgpu(self.format)?,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
fn import_iosurface_to_metal(&self, hal_device: &<api::Metal as wgpu::hal::Api>::Device) -> Result<<api::Metal as wgpu::hal::Api>::Texture, TextureImportError> {
|
||||
// Validate dimensions
|
||||
if self.width == 0 || self.height == 0 {
|
||||
return Err(TextureImportError::InvalidHandle("Invalid IOSurface texture dimensions".to_string()));
|
||||
}
|
||||
|
||||
// Convert handle to IOSurface
|
||||
let iosurface = unsafe {
|
||||
let cf_type = CFType::wrap_under_get_rule(self.handle as IOSurfaceRef);
|
||||
IOSurface::from(cf_type)
|
||||
};
|
||||
|
||||
// Get the Metal device from wgpu-hal
|
||||
let metal_device = hal_device.raw_device();
|
||||
|
||||
// Convert CEF format to Metal pixel format
|
||||
let metal_format = self.cef_to_metal_format(self.format)?;
|
||||
|
||||
// Create Metal texture descriptor
|
||||
let texture_descriptor = MTLTextureDescriptor::new();
|
||||
texture_descriptor.setTextureType(MTLTextureType::Type2D);
|
||||
texture_descriptor.setPixelFormat(metal_format);
|
||||
texture_descriptor.setWidth(self.width as usize);
|
||||
texture_descriptor.setHeight(self.height as usize);
|
||||
texture_descriptor.setDepth(1);
|
||||
texture_descriptor.setMipmapLevelCount(1);
|
||||
texture_descriptor.setSampleCount(1);
|
||||
texture_descriptor.setUsage(MTLTextureUsage::ShaderRead);
|
||||
|
||||
// Create Metal texture from IOSurface
|
||||
let metal_texture = unsafe { metal_device.newTextureWithDescriptor_iosurface_plane(&texture_descriptor, &iosurface, 0) };
|
||||
|
||||
let Some(metal_texture) = metal_texture else {
|
||||
return Err(TextureImportError::PlatformError {
|
||||
message: "Failed to create Metal texture from IOSurface".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
tracing::trace!("Successfully created Metal texture from IOSurface");
|
||||
Ok(metal_texture)
|
||||
}
|
||||
|
||||
fn cef_to_metal_format(&self, format: cef_color_type_t) -> Result<MTLPixelFormat, TextureImportError> {
|
||||
match format {
|
||||
cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(MTLPixelFormat::BGRA8Unorm_sRGB),
|
||||
cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(MTLPixelFormat::RGBA8Unorm_sRGB),
|
||||
_ => Err(TextureImportError::UnsupportedFormat { format }),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_metal_backend(&self, device: &wgpu::Device) -> bool {
|
||||
use wgpu::hal::api;
|
||||
let mut is_metal = false;
|
||||
unsafe {
|
||||
device.as_hal::<api::Metal, _, _>(|device| {
|
||||
is_metal = device.is_some();
|
||||
});
|
||||
}
|
||||
is_metal
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
//! Unified texture import system for CEF hardware acceleration
|
||||
//!
|
||||
//! This module provides a platform-agnostic interface for importing shared textures
|
||||
//! from CEF into wgpu, with automatic fallback to CPU textures when hardware
|
||||
//! acceleration is not available.
|
||||
//!
|
||||
//! # Supported Platforms
|
||||
//!
|
||||
//! - **Linux**: DMA-BUF via Vulkan external memory
|
||||
//! - **Windows**: D3D11 shared textures via Vulkan interop
|
||||
//! - **macOS**: IOSurface via Metal native API
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! ```no_run
|
||||
//! // Import texture with automatic platform detection
|
||||
//! let texture = shared_handle.import_texture(&device)?;
|
||||
//! ```
|
||||
//!
|
||||
//! # Features
|
||||
//!
|
||||
//! - `accelerated_paint` - Base feature for texture import
|
||||
//! - `accelerated_paint_dmabuf` - Linux DMA-BUF support
|
||||
//! - `accelerated_paint_d3d11` - Windows D3D11 support
|
||||
//! - `accelerated_paint_iosurface` - macOS IOSurface support
|
||||
|
||||
pub(crate) mod common;
|
||||
|
||||
pub(crate) mod shared_texture_handle;
|
||||
pub(crate) use shared_texture_handle::SharedTextureHandle;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) mod dmabuf;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) mod d3d11;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) mod iosurface;
|
||||
|
||||
/// Result type for texture import operations
|
||||
pub type TextureImportResult = Result<wgpu::Texture, TextureImportError>;
|
||||
|
||||
/// Errors that can occur during texture import
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TextureImportError {
|
||||
#[error("Invalid texture handle: {0}")]
|
||||
InvalidHandle(String),
|
||||
|
||||
#[error("Unsupported texture format: {format:?}")]
|
||||
UnsupportedFormat { format: cef::sys::cef_color_type_t },
|
||||
|
||||
#[error("Hardware acceleration not available: {reason}")]
|
||||
HardwareUnavailable { reason: String },
|
||||
|
||||
#[error("Vulkan operation failed: {operation}")]
|
||||
VulkanError { operation: String },
|
||||
|
||||
#[error("Platform-specific error: {message}")]
|
||||
PlatformError { message: String },
|
||||
|
||||
#[error("Unsupported platform for texture import")]
|
||||
UnsupportedPlatform,
|
||||
}
|
||||
|
||||
/// Trait for platform-specific texture importers
|
||||
pub trait TextureImporter {
|
||||
fn new(info: &cef::AcceleratedPaintInfo) -> Self;
|
||||
|
||||
/// Import the texture into wgpu, with automatic fallback to CPU texture
|
||||
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult;
|
||||
|
||||
/// Check if hardware acceleration is available for this texture
|
||||
fn supports_hardware_acceleration(&self, device: &wgpu::Device) -> bool;
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
use cef::AcceleratedPaintInfo;
|
||||
|
||||
use super::{TextureImportError, TextureImportResult, TextureImporter};
|
||||
|
||||
pub(crate) enum SharedTextureHandle {
|
||||
#[cfg(target_os = "linux")]
|
||||
DmaBuf(super::dmabuf::DmaBufImporter),
|
||||
#[cfg(target_os = "windows")]
|
||||
D3D11(super::d3d11::D3D11Importer),
|
||||
#[cfg(target_os = "macos")]
|
||||
IOSurface(super::iosurface::IOSurfaceImporter),
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl SharedTextureHandle {
|
||||
pub(crate) fn new(info: &AcceleratedPaintInfo) -> Self {
|
||||
// Extract DMA-BUF information
|
||||
#[cfg(target_os = "linux")]
|
||||
return Self::DmaBuf(super::dmabuf::DmaBufImporter::new(info));
|
||||
|
||||
// Extract D3D11 shared handle with texture metadata
|
||||
#[cfg(target_os = "windows")]
|
||||
return Self::D3D11(super::d3d11::D3D11Importer::new(info));
|
||||
|
||||
// Extract IOSurface handle with texture metadata
|
||||
#[cfg(target_os = "macos")]
|
||||
return Self::IOSurface(super::iosurface::IOSurfaceImporter::new(info));
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
Self::Unsupported
|
||||
}
|
||||
|
||||
/// Import a texture using the appropriate platform-specific importer
|
||||
pub(crate) fn import_texture(self, device: &wgpu::Device) -> TextureImportResult {
|
||||
match self {
|
||||
#[cfg(target_os = "linux")]
|
||||
SharedTextureHandle::DmaBuf(importer) => importer.import_to_wgpu(device),
|
||||
#[cfg(target_os = "windows")]
|
||||
SharedTextureHandle::D3D11(importer) => importer.import_to_wgpu(device),
|
||||
#[cfg(target_os = "macos")]
|
||||
SharedTextureHandle::IOSurface(importer) => importer.import_to_wgpu(device),
|
||||
SharedTextureHandle::Unsupported => Err(TextureImportError::UnsupportedPlatform),
|
||||
}
|
||||
}
|
||||
}
|
||||
9
desktop/src/cli.rs
Normal file
9
desktop/src/cli.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
#[derive(clap::Parser)]
|
||||
#[clap(name = "graphite", version)]
|
||||
pub struct Cli {
|
||||
#[arg(help = "Files to open on startup")]
|
||||
pub files: Vec<std::path::PathBuf>,
|
||||
|
||||
#[arg(long, action = clap::ArgAction::SetTrue, help = "Disable hardware accelerated UI rendering")]
|
||||
pub disable_ui_acceleration: bool,
|
||||
}
|
||||
@@ -1,6 +1,15 @@
|
||||
pub(crate) static APP_NAME: &str = "Graphite";
|
||||
pub(crate) static APP_ID: &str = "rs.graphite.GraphiteEditor";
|
||||
pub(crate) static APP_DIRECTORY_NAME: &str = "graphite-editor";
|
||||
pub(crate) const APP_NAME: &str = "Graphite";
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
pub(crate) const APP_ID: &str = "art.graphite.Graphite";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) const APP_DIRECTORY_NAME: &str = "graphite";
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) const APP_DIRECTORY_NAME: &str = "Graphite";
|
||||
pub(crate) const APP_LOCK_FILE_NAME: &str = "instance.lock";
|
||||
pub(crate) const APP_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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::fs::create_dir_all;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::consts::APP_DIRECTORY_NAME;
|
||||
use crate::consts::{APP_DIRECTORY_NAME, APP_DOCUMENTS_DIRECTORY_NAME};
|
||||
|
||||
pub(crate) fn ensure_dir_exists(path: &PathBuf) {
|
||||
if !path.exists() {
|
||||
@@ -9,8 +9,14 @@ pub(crate) fn ensure_dir_exists(path: &PathBuf) {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn graphite_data_dir() -> PathBuf {
|
||||
pub(crate) fn app_data_dir() -> PathBuf {
|
||||
let path = dirs::data_dir().expect("Failed to get data directory").join(APP_DIRECTORY_NAME);
|
||||
ensure_dir_exists(&path);
|
||||
path
|
||||
}
|
||||
|
||||
pub(crate) fn app_autosave_documents_dir() -> PathBuf {
|
||||
let path = app_data_dir().join(APP_DOCUMENTS_DIRECTORY_NAME);
|
||||
ensure_dir_exists(&path);
|
||||
path
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user