mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Merge branch 'master' into actions_menu
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
https://github.com/Keavon/graphite-branded-assets/archive/f8b02e68c92f5bbd27626bdd7a51102303b70a40.tar.gz
|
||||
d06fd7b79fa9b7509c23072fa56745415fdc6eb98575d15214b0acc47ea4dd42
|
||||
https://github.com/Keavon/graphite-branded-assets/archive/8ae15dc9c51a3855475d8cab1d0f29d9d9bc622c.tar.gz
|
||||
c19abe4ac848f3c835e43dc065c59e20e60233ae023ea0a064c5fed442be2d3d
|
||||
|
||||
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.
|
||||
-->
|
||||
|
||||
75
.github/workflows/build-linux-bundle.yml
vendored
Normal file
75
.github/workflows/build-linux-bundle.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Build Linux Bundle
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
push_to_cache:
|
||||
description: "Push to Nix Cache"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Free disk space
|
||||
run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache
|
||||
|
||||
- name: Build Nix Package
|
||||
run: nix build --no-link --print-out-paths
|
||||
|
||||
- name: Push to Nix Cache
|
||||
if: github.ref == 'refs/heads/master' || inputs.push_to_cache == true
|
||||
env:
|
||||
NIX_CACHE_AUTH_TOKEN: ${{ secrets.NIX_CACHE_AUTH_TOKEN }}
|
||||
run: |
|
||||
nix run nixpkgs#cachix -- authtoken $NIX_CACHE_AUTH_TOKEN
|
||||
nix build --no-link --print-out-paths | nix run nixpkgs#cachix -- push graphite
|
||||
|
||||
- name: Build Linux Bundle
|
||||
run: nix build .#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 .#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
|
||||
157
.github/workflows/build-mac-bundle.yml
vendored
Normal file
157
.github/workflows/build-mac-bundle.yml
vendored
Normal file
@@ -0,0 +1,157 @@
|
||||
name: Build Mac Bundle
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
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: cargo 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
|
||||
if: github.ref == 'refs/heads/master'
|
||||
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
|
||||
if: github.ref == 'refs/heads/master'
|
||||
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
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: graphite-mac-bundle-signed
|
||||
path: target/artifacts
|
||||
2
.github/workflows/build-nix-package.yml
vendored
2
.github/workflows/build-nix-package.yml
vendored
@@ -14,4 +14,4 @@ jobs:
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
|
||||
- name: Build Nix Package Dev
|
||||
run: nix build .nix#graphite-dev --print-build-logs
|
||||
run: nix build .#graphite-dev --print-build-logs
|
||||
|
||||
4
.github/workflows/build-production.yml
vendored
4
.github/workflows/build-production.yml
vendored
@@ -52,9 +52,7 @@ jobs:
|
||||
- name: 🌐 Build Graphite web code
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: |
|
||||
cd frontend
|
||||
mold -run npm run build
|
||||
run: mold -run cargo run build web
|
||||
|
||||
- name: 📤 Publish to Cloudflare Pages
|
||||
id: cloudflare
|
||||
|
||||
173
.github/workflows/build-win-bundle.yml
vendored
Normal file
173
.github/workflows/build-win-bundle.yml
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
name: Build Windows Bundle
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
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
|
||||
shell: bash # `cargo-about` refuses to run in powershell
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
run: cargo 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
|
||||
if: github.ref == 'refs/heads/master'
|
||||
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
|
||||
if: github.ref == 'refs/heads/master'
|
||||
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
|
||||
if: github.ref == 'refs/heads/master'
|
||||
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
|
||||
if: github.ref == 'refs/heads/master'
|
||||
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:
|
||||
|
||||
153
.github/workflows/ci.yml
vendored
Normal file
153
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,153 @@
|
||||
name: "CI"
|
||||
|
||||
on:
|
||||
pull_request: {}
|
||||
merge_group: {}
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
# Rust format check on GitHub runner
|
||||
rust-fmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 📥 Clone and checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🚦 Check if CI can be skipped
|
||||
id: skip-check
|
||||
uses: cariad-tech/merge-queue-ci-skipper@main
|
||||
|
||||
- name: 🦀 Install the latest Rust
|
||||
if: steps.skip-check.outputs.skip-check != 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
|
||||
- name: 🔬 Check Rust formatting
|
||||
if: steps.skip-check.outputs.skip-check != 'true'
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
# License compatibility check on GitHub runner
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 📥 Clone and checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 📜 Check crate license compatibility for root workspace
|
||||
uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
command: check bans licenses sources
|
||||
|
||||
- name: 📜 Check crate license compatibility for /libraries/rawkit
|
||||
uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
command: check bans licenses sources
|
||||
manifest-path: libraries/rawkit/Cargo.toml
|
||||
|
||||
# Build the web app on the self-hosted wasm runner
|
||||
build:
|
||||
runs-on: [self-hosted, target/wasm]
|
||||
permissions:
|
||||
contents: write
|
||||
deployments: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
env:
|
||||
RUSTC_WRAPPER: /usr/bin/sccache
|
||||
CARGO_INCREMENTAL: 0
|
||||
SCCACHE_DIR: /var/lib/github-actions/.cache
|
||||
|
||||
steps:
|
||||
- name: 📥 Clone and checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🚦 Check if CI can be skipped
|
||||
id: skip-check
|
||||
uses: cariad-tech/merge-queue-ci-skipper@main
|
||||
|
||||
- name: 🗑 Clear wasm-bindgen cache
|
||||
if: steps.skip-check.outputs.skip-check != 'true'
|
||||
run: rm -r ~/.cache/.wasm-pack || true
|
||||
|
||||
- name: 🟢 Install the latest Node.js
|
||||
if: steps.skip-check.outputs.skip-check != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "latest"
|
||||
|
||||
- name: 🚧 Install build dependencies
|
||||
if: steps.skip-check.outputs.skip-check != 'true'
|
||||
run: |
|
||||
cd frontend
|
||||
npm run setup
|
||||
|
||||
- name: 🦀 Install the latest Rust
|
||||
if: steps.skip-check.outputs.skip-check != 'true'
|
||||
run: |
|
||||
rustup update stable
|
||||
|
||||
- name: 🦀 Fetch Rust dependencies
|
||||
if: steps.skip-check.outputs.skip-check != 'true'
|
||||
run: |
|
||||
cargo fetch --locked
|
||||
|
||||
- name: 🌐 Build Graphite web code
|
||||
if: steps.skip-check.outputs.skip-check != 'true'
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: mold -run cargo run build web
|
||||
|
||||
- name: 📤 Publish to Cloudflare Pages
|
||||
if: steps.skip-check.outputs.skip-check != 'true'
|
||||
uses: cloudflare/pages-action@1
|
||||
continue-on-error: true
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
projectName: graphite-dev
|
||||
directory: frontend/dist
|
||||
|
||||
- name: 👕 Lint Graphite web formatting
|
||||
if: steps.skip-check.outputs.skip-check != 'true'
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: |
|
||||
cd frontend
|
||||
npm run lint
|
||||
|
||||
# Run the Rust tests on the self-hosted native runner
|
||||
test:
|
||||
runs-on: [self-hosted, target/native]
|
||||
env:
|
||||
RUSTC_WRAPPER: /usr/bin/sccache
|
||||
CARGO_INCREMENTAL: 0
|
||||
SCCACHE_DIR: /var/lib/github-actions/.cache
|
||||
|
||||
steps:
|
||||
- name: 📥 Clone and checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🚦 Check if CI can be skipped
|
||||
id: skip-check
|
||||
uses: cariad-tech/merge-queue-ci-skipper@main
|
||||
|
||||
- name: 🦀 Install the latest Rust
|
||||
if: steps.skip-check.outputs.skip-check != 'true'
|
||||
run: |
|
||||
rustup update stable
|
||||
|
||||
- name: 🦀 Fetch Rust dependencies
|
||||
if: steps.skip-check.outputs.skip-check != 'true'
|
||||
run: |
|
||||
cargo fetch --locked
|
||||
|
||||
- name: 🧪 Run Rust tests
|
||||
if: steps.skip-check.outputs.skip-check != 'true'
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
run: |
|
||||
mold -run cargo test --all-features
|
||||
56
.github/workflows/comment-!build-commands.yml
vendored
56
.github/workflows/comment-!build-commands.yml
vendored
@@ -1,7 +1,6 @@
|
||||
# USAGE:
|
||||
# After reviewing the code, core team members may comment on a PR with the exact text:
|
||||
# - `!build-dev` to build with debug symbols and optimizations disabled
|
||||
# - `!build-profiling` to build with debug symbols and optimizations enabled
|
||||
# - `!build-debug` to build with debug symbols and optimizations disabled
|
||||
# - `!build` to build without debug symbols and optimizations enabled
|
||||
# The comment may not contain any other text, not even whitespace or newlines.
|
||||
name: "!build PR Command"
|
||||
@@ -21,7 +20,7 @@ jobs:
|
||||
if: >
|
||||
github.event.issue.pull_request &&
|
||||
github.event.comment.author_association == 'MEMBER' &&
|
||||
(github.event.comment.body == '!build-dev' || github.event.comment.body == '!build-profiling' || github.event.comment.body == '!build')
|
||||
(github.event.comment.body == '!build-debug' || github.event.comment.body == '!build')
|
||||
runs-on: self-hosted
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -82,22 +81,43 @@ jobs:
|
||||
- name: ⌨ Set build command based on comment
|
||||
id: build_command
|
||||
run: |
|
||||
if [[ "${{ github.event.comment.body }}" == "!build-dev" ]]; then
|
||||
echo "command=build-dev" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ github.event.comment.body }}" == "!build-profiling" ]]; then
|
||||
echo "command=build-profiling" >> $GITHUB_OUTPUT
|
||||
if [[ "${{ github.event.comment.body }}" == "!build-debug" ]]; then
|
||||
echo "command=build web debug" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ github.event.comment.body }}" == "!build" ]]; then
|
||||
echo "command=build" >> $GITHUB_OUTPUT
|
||||
echo "command=build web" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Failed to detect if the build command written in the comment should have been '!build-dev', '!build-profiling', or '!build'" >> $GITHUB_OUTPUT
|
||||
echo "Failed to detect if the build command written in the comment should have been '!build-debug', or '!build'" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: 💬 Comment Actions run link
|
||||
id: comment_actions_run_link
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.updateComment({
|
||||
comment_id: ${{ github.event.comment.id }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '!build ([Run ID ' + context.runId + '](https://github.com/GraphiteEditor/Graphite/actions/runs/' + context.runId + '))'
|
||||
});
|
||||
|
||||
- name: 🌐 Build Graphite web code
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: |
|
||||
cd frontend
|
||||
mold -run npm run ${{ steps.build_command.outputs.command }}
|
||||
if: ${{ success() || failure()}}
|
||||
run: mold -run cargo run ${{ steps.build_command.outputs.command }}
|
||||
|
||||
- name: ❗ Warn on build failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: 'The build process has failed. Please check the [build logs](https://github.com/GraphiteEditor/Graphite/actions/runs/' + context.runId + ') for details.'
|
||||
});
|
||||
|
||||
- name: 📤 Publish to Cloudflare Pages
|
||||
id: cloudflare
|
||||
@@ -110,6 +130,18 @@ jobs:
|
||||
projectName: graphite-dev
|
||||
directory: frontend/dist
|
||||
|
||||
- name: ❗ Warn on publish failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: 'The deployment to Cloudflare Pages has failed. Please check the [build logs](https://github.com/GraphiteEditor/Graphite/actions/runs/' + context.runId + ') for details.
|
||||
});
|
||||
|
||||
- name: 💬 Comment build link
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
|
||||
@@ -3,9 +3,9 @@ name: Profiling Changes
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'node-graph/**'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- "node-graph/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
let commentBody = "";
|
||||
|
||||
function formatNumber(num) {
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
function formatPercentage(pct) {
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
name: "Editor: Dev & CI"
|
||||
name: "Deploy Master"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
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:
|
||||
runs-on: self-hosted
|
||||
deploy:
|
||||
runs-on: [self-hosted, target/wasm]
|
||||
permissions:
|
||||
contents: write
|
||||
deployments: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
env:
|
||||
RUSTC_WRAPPER: /usr/bin/sccache
|
||||
@@ -41,29 +40,16 @@ jobs:
|
||||
|
||||
- name: 🦀 Install the latest Rust
|
||||
run: |
|
||||
echo "Initial system version:"
|
||||
rustc --version
|
||||
rustup update stable
|
||||
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)
|
||||
git rev-parse --abbrev-ref HEAD | grep master > /dev/null || export INDEX_HTML_HEAD_REPLACEMENT=""
|
||||
sed -i "s|<!-- INDEX_HTML_HEAD_REPLACEMENT -->|$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html
|
||||
|
||||
- name: 🌐 Build Graphite web code
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: |
|
||||
cd frontend
|
||||
mold -run npm run build
|
||||
run: mold -run cargo run build web
|
||||
|
||||
- name: 📤 Publish to Cloudflare Pages
|
||||
id: cloudflare
|
||||
@@ -77,7 +63,6 @@ jobs:
|
||||
directory: frontend/dist
|
||||
|
||||
- name: 💬 Comment build link URL to commit hash page on GitHub
|
||||
if: github.ref == 'refs/heads/master'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -89,36 +74,15 @@ jobs:
|
||||
|-|
|
||||
| ${{ steps.cloudflare.outputs.url }} |"
|
||||
|
||||
- name: 👕 Lint Graphite web formatting
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: |
|
||||
cd frontend
|
||||
npm run lint
|
||||
|
||||
- name: 🔬 Check Rust formatting
|
||||
run: |
|
||||
mold -run cargo fmt --all -- --check
|
||||
|
||||
- name: 🦀 Build Rust code
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
run: |
|
||||
mold -run cargo build --all-features
|
||||
|
||||
- name: 🧪 Run Rust tests
|
||||
run: |
|
||||
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'
|
||||
id: cache-website-code-docs
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
@@ -126,7 +90,6 @@ jobs:
|
||||
key: website-code-docs
|
||||
|
||||
- name: 🔍 Check if auto-generated code docs artifacts changed
|
||||
if: github.ref == 'refs/heads/master'
|
||||
id: website-code-docs-changed
|
||||
run: |
|
||||
if ! diff --brief --recursive artifacts-generated artifacts; then
|
||||
@@ -154,31 +117,3 @@ jobs:
|
||||
run: |
|
||||
rm -rf artifacts
|
||||
gh workflow run website.yml --ref master
|
||||
|
||||
# miri:
|
||||
# runs-on: self-hosted
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v3
|
||||
|
||||
# - name: 🧪 Run Rust miri
|
||||
# run: |
|
||||
# mold -run cargo +nightly miri nextest run -j32 --all-features
|
||||
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 📥 Clone and checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 📜 Check crate license compatibility for root workspace
|
||||
uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
command: check bans licenses sources
|
||||
|
||||
- name: 📜 Check crate license compatibility for /libraries/rawkit
|
||||
uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
command: check bans licenses sources
|
||||
manifest-path: libraries/rawkit/Cargo.toml
|
||||
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 .#graphite-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"
|
||||
18
.github/workflows/website.yml
vendored
18
.github/workflows/website.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
- 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
|
||||
@@ -67,19 +67,27 @@ 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
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
branding/
|
||||
target/
|
||||
third-party-licenses.txt*
|
||||
result/
|
||||
.flatpak-builder/
|
||||
*.spv
|
||||
*.exrc
|
||||
perf.data*
|
||||
@@ -10,5 +12,3 @@ flamegraph.svg
|
||||
.idea/
|
||||
.direnv
|
||||
.DS_Store
|
||||
hierarchical_message_system_tree.txt
|
||||
hierarchical_message_system_tree.html
|
||||
|
||||
81
.nix/default.nix
Normal file
81
.nix/default.nix
Normal file
@@ -0,0 +1,81 @@
|
||||
inputs:
|
||||
|
||||
let
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
];
|
||||
forAllSystems = f: inputs.nixpkgs.lib.genAttrs systems (system: f system);
|
||||
args =
|
||||
system:
|
||||
(
|
||||
let
|
||||
lib = inputs.nixpkgs.lib // {
|
||||
call = p: import p args;
|
||||
};
|
||||
|
||||
pkgs = import inputs.nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ (import inputs.rust-overlay) ];
|
||||
};
|
||||
|
||||
info = {
|
||||
pname = "graphite";
|
||||
version = "unstable";
|
||||
src = inputs.nixpkgs.lib.cleanSourceWith {
|
||||
src = ./..;
|
||||
filter = path: type: !(type == "directory" && builtins.baseNameOf path == ".nix");
|
||||
};
|
||||
cargoVendored = deps.crane.lib.vendorCargoDeps { inherit (info) src; };
|
||||
};
|
||||
|
||||
deps = {
|
||||
crane = lib.call ./deps/crane.nix;
|
||||
cef = lib.call ./deps/cef.nix;
|
||||
rustGPU = lib.call ./deps/rust-gpu.nix;
|
||||
};
|
||||
|
||||
args = {
|
||||
inherit system;
|
||||
inherit (inputs) self;
|
||||
inherit inputs;
|
||||
inherit pkgs;
|
||||
inherit lib;
|
||||
inherit info;
|
||||
inherit deps;
|
||||
}
|
||||
// inputs;
|
||||
in
|
||||
args
|
||||
);
|
||||
withArgs = f: forAllSystems (system: f (args system));
|
||||
in
|
||||
{
|
||||
packages = withArgs (
|
||||
{ lib, ... }:
|
||||
rec {
|
||||
default = graphite;
|
||||
graphite = (lib.call ./pkgs/graphite.nix) { };
|
||||
graphite-dev = (lib.call ./pkgs/graphite.nix) { dev = true; };
|
||||
graphite-raster-nodes-shaders = lib.call ./pkgs/graphite-raster-nodes-shaders.nix;
|
||||
graphite-branding = lib.call ./pkgs/graphite-branding.nix;
|
||||
graphite-bundle = lib.call ./pkgs/graphite-bundle.nix;
|
||||
graphite-flatpak-manifest = lib.call ./pkgs/graphite-flatpak-manifest.nix;
|
||||
|
||||
# TODO: graphene-cli = lib.call ./pkgs/graphene-cli.nix;
|
||||
|
||||
tools = {
|
||||
third-party-licenses = lib.call ./pkgs/tools/third-party-licenses.nix;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
devShells = withArgs (
|
||||
{ lib, ... }:
|
||||
{
|
||||
default = lib.call ./dev.nix;
|
||||
}
|
||||
);
|
||||
|
||||
formatter = withArgs ({ pkgs, ... }: pkgs.nixfmt-tree);
|
||||
}
|
||||
@@ -1,25 +1,26 @@
|
||||
{ pkgs, inputs, ... }:
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
cef = pkgs.cef-binary.overrideAttrs (_: _: {
|
||||
cefPath = pkgs.cef-binary.overrideAttrs (finalAttrs: {
|
||||
postInstall = ''
|
||||
strip $out/Release/*.so*
|
||||
rm -r $out/* $out/.* || true
|
||||
strip ./Release/*.so*
|
||||
mv ./Release/* $out/
|
||||
find "./Resources/locales" -maxdepth 1 -type f ! -name 'en-US.pak' -delete
|
||||
mv ./Resources/* $out/
|
||||
mv ./include $out/
|
||||
|
||||
cat ./CREDITS.html | ${pkgs.xz}/bin/xz -9 -e -c > $out/CREDITS.html.xz
|
||||
|
||||
echo '${
|
||||
builtins.toJSON {
|
||||
type = "minimal";
|
||||
name = builtins.baseNameOf finalAttrs.src.url;
|
||||
sha1 = "";
|
||||
}
|
||||
}' > $out/archive.json
|
||||
'';
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{ pkgs, inputs, ... }:
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
extensions = [
|
||||
|
||||
60
.nix/dev.nix
60
.nix/dev.nix
@@ -1,16 +1,58 @@
|
||||
{
|
||||
pkgs,
|
||||
deps,
|
||||
libs,
|
||||
tools,
|
||||
...
|
||||
}:
|
||||
{ pkgs, deps, ... }:
|
||||
|
||||
let
|
||||
libs = [
|
||||
pkgs.wayland
|
||||
pkgs.vulkan-loader
|
||||
pkgs.libGL
|
||||
pkgs.openssl
|
||||
pkgs.libraw
|
||||
|
||||
# X11 Support
|
||||
pkgs.libxkbcommon
|
||||
pkgs.libXcursor
|
||||
pkgs.libxcb
|
||||
pkgs.libX11
|
||||
];
|
||||
in
|
||||
pkgs.mkShell (
|
||||
{
|
||||
packages = tools.all ++ libs.all;
|
||||
packages = libs ++ [
|
||||
pkgs.pkg-config
|
||||
|
||||
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath libs.all}:${deps.cef.env.CEF_PATH}";
|
||||
pkgs.lld
|
||||
pkgs.nodejs
|
||||
pkgs.nodePackages.npm
|
||||
pkgs.binaryen
|
||||
pkgs.wasm-bindgen-cli_0_2_100
|
||||
pkgs.wasm-pack
|
||||
pkgs.cargo-about
|
||||
|
||||
pkgs.rustc
|
||||
pkgs.cargo
|
||||
pkgs.rust-analyzer
|
||||
pkgs.clippy
|
||||
pkgs.rustfmt
|
||||
|
||||
pkgs.git
|
||||
|
||||
pkgs.cargo-watch
|
||||
pkgs.cargo-nextest
|
||||
pkgs.cargo-expand
|
||||
|
||||
# Linker
|
||||
pkgs.mold
|
||||
|
||||
# Profiling tools
|
||||
pkgs.gnuplot
|
||||
pkgs.samply
|
||||
pkgs.cargo-flamegraph
|
||||
|
||||
# Plotting tools
|
||||
pkgs.graphviz
|
||||
];
|
||||
|
||||
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath libs}:${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 = ''
|
||||
|
||||
113
.nix/flake.lock
generated
113
.nix/flake.lock
generated
@@ -1,113 +0,0 @@
|
||||
{
|
||||
"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,
|
||||
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
|
||||
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
|
||||
"revCount": 69,
|
||||
"type": "tarball",
|
||||
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1764242076,
|
||||
"narHash": "sha256-sKoIWfnijJ0+9e4wRvIgm/HgE27bzwQxcEmo2J/gNpI=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2fad6eac6077f03fe109c4d4eb171cf96791faa4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1764297505,
|
||||
"narHash": "sha256-qrLpVu2/hA9Cu6IovMEsgh9YRyvmmWS+bSx7C1JGChA=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "9623580f8ce09ec444b9aca107566ec5db110e62",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
159
.nix/flake.nix
159
.nix/flake.nix
@@ -1,159 +0,0 @@
|
||||
# This is a helper file for people using NixOS as their operating system.
|
||||
# If you don't know what this file does, you can safely ignore it.
|
||||
# This file defines the reproducible development environment for the project.
|
||||
#
|
||||
# Development Environment:
|
||||
# - Provides all necessary tools for Rust/Wasm development
|
||||
# - Includes dependencies for desktop app development
|
||||
# - Sets up profiling and debugging tools
|
||||
# - Configures mold as the default linker for faster builds
|
||||
#
|
||||
# Usage:
|
||||
# - Development shell: `nix develop .nix` from the project root
|
||||
# - Run in dev shell with direnv: add `use flake` to .envrc
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
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 =
|
||||
inputs:
|
||||
inputs.flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
info = {
|
||||
pname = "graphite";
|
||||
version = "unstable";
|
||||
src = ./..;
|
||||
};
|
||||
|
||||
pkgs = import inputs.nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ (import inputs.rust-overlay) ];
|
||||
};
|
||||
|
||||
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; };
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
pkgs.git
|
||||
|
||||
pkgs.cargo-watch
|
||||
pkgs.cargo-nextest
|
||||
pkgs.cargo-expand
|
||||
|
||||
# Linker
|
||||
pkgs.mold
|
||||
|
||||
# Profiling tools
|
||||
pkgs.gnuplot
|
||||
pkgs.samply
|
||||
pkgs.cargo-flamegraph
|
||||
|
||||
# Plotting tools
|
||||
pkgs.graphviz
|
||||
];
|
||||
all = desktop ++ frontend ++ dev;
|
||||
};
|
||||
in
|
||||
{
|
||||
packages = rec {
|
||||
graphiteWithArgs =
|
||||
args:
|
||||
(import ./pkgs/graphite.nix {
|
||||
pkgs = pkgs // {
|
||||
inherit raster-nodes-shaders;
|
||||
};
|
||||
inherit
|
||||
info
|
||||
inputs
|
||||
deps
|
||||
libs
|
||||
tools
|
||||
;
|
||||
})
|
||||
args;
|
||||
graphite = graphiteWithArgs { };
|
||||
graphite-dev = graphiteWithArgs { dev = true; };
|
||||
graphite-without-resources = graphiteWithArgs { embeddedResources = false; };
|
||||
graphite-without-resources-dev = graphiteWithArgs {
|
||||
embeddedResources = false;
|
||||
dev = true;
|
||||
};
|
||||
#TODO: graphene-cli = import ./pkgs/graphene-cli.nix { inherit info pkgs inputs deps libs tools; };
|
||||
raster-nodes-shaders = import ./pkgs/raster-nodes-shaders.nix {
|
||||
inherit
|
||||
info
|
||||
pkgs
|
||||
inputs
|
||||
deps
|
||||
libs
|
||||
tools
|
||||
;
|
||||
};
|
||||
|
||||
default = graphite;
|
||||
};
|
||||
|
||||
devShells.default = import ./dev.nix {
|
||||
inherit
|
||||
pkgs
|
||||
deps
|
||||
libs
|
||||
tools
|
||||
;
|
||||
};
|
||||
|
||||
formatter = pkgs.nixfmt-tree;
|
||||
}
|
||||
);
|
||||
}
|
||||
20
.nix/pkgs/graphite-branding.nix
Normal file
20
.nix/pkgs/graphite-branding.nix
Normal file
@@ -0,0 +1,20 @@
|
||||
{ info, pkgs, ... }:
|
||||
|
||||
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;
|
||||
}
|
||||
);
|
||||
in
|
||||
pkgs.runCommand "${info.pname}-branding" { } ''
|
||||
mkdir -p $out
|
||||
tar -xvf ${brandingTar} -C $out --strip-components 1
|
||||
''
|
||||
92
.nix/pkgs/graphite-bundle.nix
Normal file
92
.nix/pkgs/graphite-bundle.nix
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
pkgs,
|
||||
self,
|
||||
system,
|
||||
...
|
||||
}:
|
||||
let
|
||||
bundle =
|
||||
{
|
||||
archive ? false,
|
||||
compression ? null,
|
||||
passthru ? { },
|
||||
}:
|
||||
(
|
||||
let
|
||||
graphite = self.packages.${system}.graphite;
|
||||
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 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' \
|
||||
--remove-needed libGL.so \
|
||||
out/bin/graphite
|
||||
cp -r ${graphite}/share out/share
|
||||
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*
|
||||
'';
|
||||
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 {
|
||||
passthru = {
|
||||
tar = bundle {
|
||||
archive = true;
|
||||
passthru = {
|
||||
gz = bundle {
|
||||
compression = "gz";
|
||||
};
|
||||
xz = bundle {
|
||||
compression = "xz";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
39
.nix/pkgs/graphite-flatpak-manifest.nix
Normal file
39
.nix/pkgs/graphite-flatpak-manifest.nix
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
pkgs,
|
||||
self,
|
||||
system,
|
||||
...
|
||||
}:
|
||||
|
||||
(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 = self.packages.${system}.graphite-bundle.tar;
|
||||
strip-components = 0;
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -1,12 +1,4 @@
|
||||
{
|
||||
info,
|
||||
pkgs,
|
||||
inputs,
|
||||
deps,
|
||||
libs,
|
||||
tools,
|
||||
...
|
||||
}:
|
||||
{ info, deps, ... }:
|
||||
|
||||
(deps.crane.lib.overrideToolchain (_: deps.rustGPU.toolchain)).buildPackage {
|
||||
pname = "raster-nodes-shaders";
|
||||
@@ -16,7 +8,7 @@
|
||||
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"
|
||||
"${deps.rustGPU.toolchain.availableComponents.rust-src}/lib/rustlib/src/rust/library/Cargo.lock"
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,61 +1,58 @@
|
||||
{
|
||||
info,
|
||||
pkgs,
|
||||
inputs,
|
||||
self,
|
||||
deps,
|
||||
libs,
|
||||
tools,
|
||||
system,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
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
|
||||
'';
|
||||
branding = self.packages.${system}.graphite-branding;
|
||||
cargoVendorDir = deps.crane.lib.vendorCargoDeps { inherit (info) src; };
|
||||
resourcesCommon = {
|
||||
pname = "${info.pname}-resources";
|
||||
inherit (info) version src;
|
||||
inherit cargoVendorDir;
|
||||
strictDeps = true;
|
||||
doCheck = false;
|
||||
nativeBuildInputs = tools.frontend;
|
||||
nativeBuildInputs = [
|
||||
pkgs.pkg-config
|
||||
pkgs.lld
|
||||
pkgs.nodejs
|
||||
pkgs.nodePackages.npm
|
||||
pkgs.binaryen
|
||||
pkgs.wasm-bindgen-cli_0_2_100
|
||||
pkgs.wasm-pack
|
||||
pkgs.cargo-about
|
||||
];
|
||||
buildInputs = [ pkgs.openssl ];
|
||||
env.CARGO_PROFILE = if dev then "dev" else "release";
|
||||
cargoExtraArgs = "--target wasm32-unknown-unknown -p graphite-wasm --no-default-features --features native";
|
||||
doCheck = false;
|
||||
};
|
||||
resources = deps.crane.lib.buildPackage (
|
||||
resourcesCommon
|
||||
// {
|
||||
cargoArtifacts = deps.crane.lib.buildDepsOnly resourcesCommon;
|
||||
|
||||
# TODO: Remove the need for this hash by using individual package resolutions and hashes from package-lock.json
|
||||
npmDeps = pkgs.fetchNpmDeps {
|
||||
inherit (info) pname version;
|
||||
src = "${info.src}/frontend";
|
||||
hash = "sha256-D8VCNK+Ca3gxO+5wriBn8FszG8/x8n/zM6/MPo9E2j4=";
|
||||
npmDeps = pkgs.importNpmLock {
|
||||
npmRoot = "${info.src}/frontend";
|
||||
};
|
||||
|
||||
npmRoot = "frontend";
|
||||
npmConfigScript = "setup";
|
||||
makeCacheWritable = true;
|
||||
|
||||
nativeBuildInputs = tools.frontend ++ [ pkgs.npmHooks.npmConfigHook ];
|
||||
nativeBuildInputs = [
|
||||
pkgs.importNpmLock.npmConfigHook
|
||||
pkgs.removeReferencesTo
|
||||
]
|
||||
++ resourcesCommon.nativeBuildInputs;
|
||||
|
||||
prePatch = ''
|
||||
mkdir branding
|
||||
@@ -75,19 +72,39 @@ let
|
||||
mkdir -p $out
|
||||
cp -r frontend/dist/* $out/
|
||||
'';
|
||||
|
||||
postFixup = ''
|
||||
find "$out" -type f -exec remove-references-to -t "${cargoVendorDir}" '{}' +
|
||||
'';
|
||||
}
|
||||
);
|
||||
libs = [
|
||||
pkgs.wayland
|
||||
pkgs.vulkan-loader
|
||||
pkgs.libGL
|
||||
pkgs.openssl
|
||||
pkgs.libraw
|
||||
|
||||
# X11 Support
|
||||
pkgs.libxkbcommon
|
||||
pkgs.libXcursor
|
||||
pkgs.libxcb
|
||||
pkgs.libX11
|
||||
];
|
||||
common = {
|
||||
inherit (info) pname version src;
|
||||
inherit cargoVendorDir;
|
||||
strictDeps = true;
|
||||
buildInputs = libs.desktop-all;
|
||||
nativeBuildInputs = tools.desktop ++ [ pkgs.makeWrapper ];
|
||||
buildInputs = libs;
|
||||
nativeBuildInputs = [
|
||||
pkgs.pkg-config
|
||||
pkgs.cargo-about
|
||||
pkgs.removeReferencesTo
|
||||
];
|
||||
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"
|
||||
}";
|
||||
cargoExtraArgs = "-p graphite-desktop";
|
||||
doCheck = false;
|
||||
};
|
||||
in
|
||||
@@ -97,24 +114,34 @@ deps.crane.lib.buildPackage (
|
||||
// {
|
||||
cargoArtifacts = deps.crane.lib.buildDepsOnly common;
|
||||
|
||||
env =
|
||||
common.env
|
||||
// {
|
||||
RASTER_NODES_SHADER_PATH = pkgs.raster-nodes-shaders;
|
||||
}
|
||||
// (
|
||||
if embeddedResources then
|
||||
{
|
||||
EMBEDDED_RESOURCES = resources;
|
||||
}
|
||||
else
|
||||
{ }
|
||||
);
|
||||
env = common.env // {
|
||||
RASTER_NODES_SHADER_PATH = self.packages.${system}.graphite-raster-nodes-shaders;
|
||||
EMBEDDED_RESOURCES = resources;
|
||||
GRAPHITE_GIT_COMMIT_HASH = self.rev or "unknown";
|
||||
GRAPHITE_GIT_COMMIT_DATE = self.lastModified or "unknown";
|
||||
};
|
||||
|
||||
postUnpack = ''
|
||||
mkdir ./branding
|
||||
cp -r ${branding}/* ./branding
|
||||
'';
|
||||
npmDeps = pkgs.importNpmLock {
|
||||
npmRoot = "${info.src}/frontend";
|
||||
};
|
||||
npmRoot = "frontend";
|
||||
nativeBuildInputs = [
|
||||
pkgs.importNpmLock.npmConfigHook
|
||||
pkgs.nodePackages.npm
|
||||
]
|
||||
++ common.nativeBuildInputs;
|
||||
|
||||
preBuild = ''
|
||||
${lib.getExe self.packages.${system}.tools.third-party-licenses}
|
||||
''
|
||||
+ (
|
||||
if 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
|
||||
@@ -124,13 +151,22 @@ deps.crane.lib.buildPackage (
|
||||
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/
|
||||
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}"
|
||||
remove-references-to -t "${cargoVendorDir}" $out/bin/graphite
|
||||
|
||||
patchelf \
|
||||
--set-rpath "${pkgs.lib.makeLibraryPath libs}:${deps.cef.env.CEF_PATH}" \
|
||||
--add-needed libGL.so \
|
||||
$out/bin/graphite
|
||||
'';
|
||||
}
|
||||
)
|
||||
|
||||
31
.nix/pkgs/tools/third-party-licenses.nix
Normal file
31
.nix/pkgs/tools/third-party-licenses.nix
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
info,
|
||||
deps,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cargoVendorDir = deps.crane.lib.vendorCargoDeps { inherit (info) src; };
|
||||
common = {
|
||||
pname = "third-party-licenses";
|
||||
inherit (info) version src;
|
||||
inherit cargoVendorDir;
|
||||
nativeBuildInputs = [ pkgs.pkg-config ];
|
||||
buildInputs = [ pkgs.openssl ];
|
||||
strictDeps = true;
|
||||
env = deps.cef.env // {
|
||||
CARGO_PROFILE = "dev";
|
||||
};
|
||||
cargoExtraArgs = "-p third-party-licenses --features desktop";
|
||||
doCheck = false;
|
||||
};
|
||||
in
|
||||
deps.crane.lib.buildPackage (
|
||||
common
|
||||
// {
|
||||
inherit cargoVendorDir;
|
||||
cargoArtifacts = deps.crane.lib.buildDepsOnly common;
|
||||
meta.mainProgram = "third-party-licenses";
|
||||
}
|
||||
)
|
||||
@@ -1,28 +0,0 @@
|
||||
# This is a helper file for people using NixOS as their operating system.
|
||||
# If you don't know what this file does, you can safely ignore it.
|
||||
|
||||
# If you are using Nix as your package manager, you can run 'nix-shell .nix'
|
||||
# in the root directory of the project and Nix will open a bash shell
|
||||
# with all the packages needed to build and run Graphite installed.
|
||||
# A shell.nix file is used in the Nix ecosystem to define a development
|
||||
# environment with specific dependencies. When you enter a Nix shell using
|
||||
# this file, it ensures that all the specified tools and libraries are
|
||||
# available regardless of the host system's configuration. This provides
|
||||
# a reproducible development environment across different machines and developers.
|
||||
|
||||
# You can enter the Nix shell and run Graphite like normal with:
|
||||
# > npm start
|
||||
# Or you can run it like this without needing to first enter the Nix shell:
|
||||
# > nix-shell .nix --command "npm start"
|
||||
|
||||
# Uses flake compat to provide a development shell that is identical to the one defined in the flake
|
||||
(import (
|
||||
let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
nodeName = lock.nodes.root.inputs.flake-compat;
|
||||
in
|
||||
fetchTarball {
|
||||
url = lock.nodes.${nodeName}.locked.url;
|
||||
sha256 = lock.nodes.${nodeName}.locked.narHash;
|
||||
}
|
||||
) { src = ./.; }).shellNix
|
||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -11,9 +11,10 @@
|
||||
// Code quality
|
||||
"wayou.vscode-todo-highlight",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
// Helpful
|
||||
// Git
|
||||
"mhutchie.git-graph",
|
||||
"qezhu.gitlink",
|
||||
// Helpful
|
||||
"wmaurer.change-case"
|
||||
]
|
||||
}
|
||||
|
||||
32
.vscode/settings.json
vendored
32
.vscode/settings.json
vendored
@@ -26,12 +26,17 @@
|
||||
// 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"],
|
||||
@@ -47,13 +52,36 @@
|
||||
"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`
|
||||
"a11y_no_static_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*// ===+"
|
||||
}
|
||||
|
||||
1273
Cargo.lock
generated
1273
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
86
Cargo.toml
86
Cargo.toml
@@ -12,26 +12,10 @@ members = [
|
||||
"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/libraries/*",
|
||||
"node-graph/nodes/*",
|
||||
"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",
|
||||
@@ -39,7 +23,11 @@ members = [
|
||||
"node-graph/node-macro",
|
||||
"node-graph/preprocessor",
|
||||
"proc-macros",
|
||||
"tools/crate-hierarchy-viz"
|
||||
"tools/cargo-run",
|
||||
"tools/crate-hierarchy-viz",
|
||||
"tools/third-party-licenses",
|
||||
"tools/editor-message-tree",
|
||||
"tools/node-docs",
|
||||
]
|
||||
default-members = [
|
||||
"editor",
|
||||
@@ -47,33 +35,13 @@ default-members = [
|
||||
"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/nodes/gstd",
|
||||
"node-graph/interpreted-executor",
|
||||
"node-graph/node-macro",
|
||||
"node-graph/preprocessor",
|
||||
# blocked by https://github.com/rust-lang/cargo/issues/15890
|
||||
# blocked by https://github.com/rust-lang/cargo/issues/16000
|
||||
# "proc-macros",
|
||||
"tools/cargo-run",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
@@ -114,6 +82,7 @@ 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" }
|
||||
repeat-nodes = { path = "node-graph/nodes/repeat" }
|
||||
math-nodes = { path = "node-graph/nodes/math" }
|
||||
path-bool-nodes = { path = "node-graph/nodes/path-bool" }
|
||||
graph-craft = { path = "node-graph/graph-craft" }
|
||||
@@ -130,20 +99,21 @@ 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"
|
||||
reqwest = { version = "0.12", features = ["blocking", "rustls-tls", "json"] }
|
||||
reqwest = { version = "0.13", features = ["blocking", "json"] }
|
||||
futures = "0.3"
|
||||
env_logger = "0.11"
|
||||
log = "0.4"
|
||||
bitflags = { version = "2.4", features = ["serde"] }
|
||||
ctor = "0.2"
|
||||
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.11"
|
||||
ron = "0.12"
|
||||
fastnoise-lite = "1.1"
|
||||
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
|
||||
@@ -151,7 +121,7 @@ wgpu = { version = "27.0", features = [
|
||||
"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"
|
||||
@@ -176,11 +146,17 @@ web-sys = { version = "=0.3.77", features = [
|
||||
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" }
|
||||
vello_encoding = { git = "https://github.com/linebender/vello" }
|
||||
resvg = "0.45"
|
||||
usvg = "0.45"
|
||||
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt", "rt-multi-thread"] }
|
||||
# Linebender ecosystem (BEGIN)
|
||||
kurbo = { version = "0.13", features = ["serde"] }
|
||||
vello = "0.7"
|
||||
vello_encoding = "0.7"
|
||||
resvg = "0.47"
|
||||
usvg = "0.47"
|
||||
parley = "0.6"
|
||||
skrifa = "0.40"
|
||||
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 = [
|
||||
@@ -194,8 +170,6 @@ image = { version = "0.25", default-features = false, features = [
|
||||
"jpeg",
|
||||
"bmp",
|
||||
] }
|
||||
parley = "0.6"
|
||||
skrifa = "0.36"
|
||||
pretty_assertions = "1.4"
|
||||
fern = { version = "0.7", features = ["colored"] }
|
||||
num_enum = { version = "0.7", default-features = false }
|
||||
@@ -217,7 +191,6 @@ syn = { version = "2.0", default-features = false, features = [
|
||||
"extra-traits",
|
||||
"proc-macro",
|
||||
] }
|
||||
kurbo = { version = "0.12", features = ["serde"] }
|
||||
lyon_geom = "1.0"
|
||||
petgraph = { version = "0.7", default-features = false, features = ["graphmap"] }
|
||||
half = { version = "2.4", default-features = false, features = ["bytemuck"] }
|
||||
@@ -234,11 +207,13 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing = "0.1"
|
||||
rfd = "0.15"
|
||||
open = "5.3"
|
||||
polycool = "0.4"
|
||||
spin = "0.10"
|
||||
clap = "4.5"
|
||||
spirv-std = { git = "https://github.com/Firestar99/rust-gpu-new", rev = "c12f216121820580731440ee79ebc7403d6ea04f", features = ["bytemuck"] }
|
||||
cargo-gpu = { git = "https://github.com/Firestar99/cargo-gpu", rev = "3952a22d16edbd38689f3a876e417899f21e1fe7", default-features = false }
|
||||
qrcodegen = "1.8"
|
||||
lzma-rust2 = { version = "0.16", default-features = false, features = ["std", "encoder", "optimization", "xz"] }
|
||||
scraper = "0.25"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(target_arch, values("spirv"))'] }
|
||||
@@ -262,10 +237,7 @@ node-macro = { opt-level = 2 }
|
||||
lto = "thin"
|
||||
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" }
|
||||
download-cef = { git = "https://github.com/timon-schelling/cef-rs.git", branch = "graphite" }
|
||||
|
||||
27
about.hbs
27
about.hbs
@@ -1,27 +0,0 @@
|
||||
{{!
|
||||
Be careful to prevent auto-formatting from breaking this file's indentation.
|
||||
Replace this file with JSON output once this is resolved: https://github.com/EmbarkStudios/cargo-about/issues/73
|
||||
|
||||
The `GENERATED_BY_CARGO_ABOUT` prefix is a JS labeled statement
|
||||
(<https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/label>)
|
||||
used so the reader of the generated file can verify the file does indeed start with that string,
|
||||
while remaining valid JS for subsequent parsing.
|
||||
}}
|
||||
GENERATED_BY_CARGO_ABOUT: [
|
||||
{{#each licenses}}
|
||||
{
|
||||
licenseName: `{{name}}`,
|
||||
licenseText: `{{text}}`,
|
||||
packages: [
|
||||
{{#each used_by}}
|
||||
{
|
||||
name: `{{crate.name}}`,
|
||||
version: `{{crate.version}}`,
|
||||
author: `{{crate.authors}}`,
|
||||
repository: `{{crate.repository}}`,
|
||||
},
|
||||
{{/each}}
|
||||
],
|
||||
},
|
||||
{{/each}}
|
||||
]
|
||||
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
@@ -25,9 +25,13 @@ graphite-desktop-wrapper = { path = "wrapper" }
|
||||
graphite-desktop-embedded-resources = { path = "embedded-resources", optional = true }
|
||||
|
||||
wgpu = { workspace = true }
|
||||
winit = { workspace = true, features = [ "wayland-csd-adwaita-notitlebar", "serde" ] }
|
||||
winit = { workspace = true, features = [
|
||||
"wayland-csd-adwaita-notitlebar",
|
||||
"serde",
|
||||
] }
|
||||
thiserror = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
cef = { workspace = true }
|
||||
cef-dll-sys = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
@@ -40,10 +44,11 @@ vello = { workspace = true }
|
||||
derivative = { workspace = true }
|
||||
rfd = { workspace = true }
|
||||
open = { workspace = true }
|
||||
rand = { workspace = true, features = ["thread_rng"] }
|
||||
lzma-rust2 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
rand = { workspace = true, features = ["thread_rng"] }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
pidlock = "0.2.2"
|
||||
fd-lock = "4.0.4"
|
||||
ctrlc = "3.5.1"
|
||||
window_clipboard = "0.5"
|
||||
|
||||
@@ -55,6 +60,7 @@ windows = { version = "0.58.0", features = [
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Console",
|
||||
"Win32_UI_Controls",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_UI_HiDpi",
|
||||
@@ -66,4 +72,4 @@ windows = { version = "0.58.0", features = [
|
||||
objc2 = { version = "0.6.1", default-features = false }
|
||||
objc2-foundation = { version = "0.3.2", default-features = false }
|
||||
objc2-app-kit = { version = "0.3.2", default-features = false }
|
||||
muda = { git = "https://github.com/tauri-apps/muda.git", rev = "3f460b8fbaed59cda6d95ceea6904f000f093f15", default-features = false }
|
||||
muda = { git = "https://github.com/timon-schelling/muda.git", rev = "e5bc28bbd6781b18afbfc237981f9ef47eddf863", default-features = false }
|
||||
|
||||
@@ -5,7 +5,7 @@ Comment=Open-source vector & raster graphics editor. Featuring node based proced
|
||||
Exec=graphite
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=graphite
|
||||
Icon=art.graphite.Graphite
|
||||
Categories=Graphics;VectorGraphics;RasterGraphics;
|
||||
Keywords=graphite;editor;vector;raster;procedural;design;
|
||||
StartupWMClass=art.graphite.Graphite
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg_attr(target_os = "linux", allow(unused))] // TODO: Remove this when bundling for linux is implemented
|
||||
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -27,8 +29,7 @@ pub(crate) fn cef_path() -> PathBuf {
|
||||
}
|
||||
|
||||
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];
|
||||
let mut args = vec!["build", "--package", package, "--profile", profile_name()];
|
||||
if let Some(bin) = bin {
|
||||
args.push("--bin");
|
||||
args.push(bin);
|
||||
@@ -45,7 +46,7 @@ pub(crate) fn build_bin(package: &str, bin: Option<&str>) -> Result<PathBuf, Box
|
||||
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);
|
||||
return Err(format!("Command '{}' with args {:?} failed with status: {}", program, args, status).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
use std::error::Error;
|
||||
|
||||
use crate::common::*;
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
pub fn main() -> Result<(), Box<dyn std::error::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");
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if let Some(pos) = args.iter().position(|a| a == "open") {
|
||||
let extra_args: Vec<&str> = args[pos + 1..].iter().map(|s| s.as_str()).collect();
|
||||
run_command(&app_bin.to_string_lossy(), &extra_args).expect("failed to open app");
|
||||
} else {
|
||||
println!("Binary built and placed at {}", app_bin.to_string_lossy());
|
||||
eprintln!("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(())
|
||||
|
||||
@@ -22,9 +22,11 @@ pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
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");
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if let Some(pos) = args.iter().position(|a| a == "open") {
|
||||
let executable = app_dir.join(EXEC_PATH).join(APP_NAME);
|
||||
let extra_args: Vec<&str> = args[pos + 1..].iter().map(|s| s.as_str()).collect();
|
||||
run_command(&executable.to_string_lossy(), &extra_args).expect("failed to open app");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -12,9 +12,10 @@ pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
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")
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if let Some(pos) = args.iter().position(|a| a == "open") {
|
||||
let extra_args: Vec<&str> = args[pos + 1..].iter().map(|s| s.as_str()).collect();
|
||||
run_command(&executable.to_string_lossy(), &extra_args).expect("failed to open app")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -27,8 +28,33 @@ fn bundle(out_dir: &Path, app_bin: &Path) -> PathBuf {
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ fn main() {
|
||||
res.set("FileDescription", "Graphite");
|
||||
res.set("ProductName", "Graphite");
|
||||
|
||||
res.set("LegalCopyright", "Copyright © 2025 Graphite Labs, LLC");
|
||||
// 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");
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
use rand::Rng;
|
||||
use rfd::AsyncFileDialog;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::io::Read;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::{Receiver, Sender, SyncSender};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::dpi::PhysicalSize;
|
||||
use winit::event::{ButtonSource, ElementState, MouseButton, WindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow};
|
||||
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::preferences;
|
||||
use crate::render::{RenderError, RenderState};
|
||||
use crate::window::Window;
|
||||
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Platform};
|
||||
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState};
|
||||
use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
||||
|
||||
pub(crate) struct App {
|
||||
@@ -27,6 +32,8 @@ pub(crate) struct App {
|
||||
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,
|
||||
@@ -34,13 +41,15 @@ pub(crate) struct App {
|
||||
cef_context: Box<dyn cef::CefContext>,
|
||||
cef_schedule: Option<Instant>,
|
||||
cef_view_info_sender: Sender<cef::ViewInfoUpdate>,
|
||||
last_ui_update: Instant,
|
||||
avg_frame_time: f32,
|
||||
cef_init_successful: bool,
|
||||
start_render_sender: SyncSender<()>,
|
||||
web_communication_initialized: bool,
|
||||
web_communication_startup_buffer: Vec<Vec<u8>>,
|
||||
persistent_data: PersistentData,
|
||||
launch_documents: Vec<PathBuf>,
|
||||
cli: Cli,
|
||||
startup_time: Option<Instant>,
|
||||
exiting: Arc<AtomicBool>,
|
||||
exit_reason: ExitReason,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -54,28 +63,37 @@ impl App {
|
||||
wgpu_context: WgpuContext,
|
||||
app_event_receiver: Receiver<AppEvent>,
|
||||
app_event_scheduler: AppEventScheduler,
|
||||
launch_documents: Vec<PathBuf>,
|
||||
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::CloseWindow);
|
||||
ctrlc_app_event_scheduler.schedule(AppEvent::Exit);
|
||||
})
|
||||
.expect("Error setting Ctrl-C handler");
|
||||
|
||||
let exiting = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let rendering_app_event_scheduler = app_event_scheduler.clone();
|
||||
let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1);
|
||||
let exiting_clone = exiting.clone();
|
||||
std::thread::spawn(move || {
|
||||
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||
loop {
|
||||
let result = futures::executor::block_on(DesktopWrapper::execute_node_graph());
|
||||
let result = runtime.block_on(DesktopWrapper::execute_node_graph());
|
||||
rendering_app_event_scheduler.schedule(AppEvent::NodeGraphExecutionResult(result));
|
||||
let _ = start_render_receiver.recv();
|
||||
let _ = start_render_receiver.recv_timeout(Duration::from_millis(10));
|
||||
if exiting_clone.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut persistent_data = PersistentData::default();
|
||||
persistent_data.load_from_disk();
|
||||
|
||||
let desktop_wrapper = DesktopWrapper::new(rand::rng().random());
|
||||
|
||||
Self {
|
||||
render_state: None,
|
||||
wgpu_context,
|
||||
@@ -84,23 +102,43 @@ impl App {
|
||||
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: DesktopWrapper::new(),
|
||||
last_ui_update: Instant::now(),
|
||||
desktop_wrapper,
|
||||
cef_context,
|
||||
cef_schedule: Some(Instant::now()),
|
||||
cef_view_info_sender,
|
||||
avg_frame_time: 0.,
|
||||
cef_init_successful: false,
|
||||
start_render_sender,
|
||||
web_communication_initialized: false,
|
||||
web_communication_startup_buffer: Vec::new(),
|
||||
persistent_data,
|
||||
launch_documents,
|
||||
cli,
|
||||
startup_time: None,
|
||||
exiting,
|
||||
exit_reason: ExitReason::Shutdown,
|
||||
}
|
||||
}
|
||||
|
||||
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 self.exiting.swap(true, Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let _ = self.start_render_sender.send(());
|
||||
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");
|
||||
@@ -173,7 +211,7 @@ impl App {
|
||||
if let Some(path) = futures::executor::block_on(show_dialog)
|
||||
&& let Ok(content) = fs::read(&path)
|
||||
{
|
||||
let message = DesktopWrapperMessage::OpenFileDialogResult { path, content, context };
|
||||
let message = DesktopWrapperMessage::FileDialogResult { path, content, context };
|
||||
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
});
|
||||
@@ -238,6 +276,9 @@ impl App {
|
||||
if let Some(render_state) = &mut self.render_state {
|
||||
render_state.set_overlays_scene(scene);
|
||||
}
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceWriteDocument { id, document } => {
|
||||
self.persistent_data.write_document(id, document);
|
||||
@@ -252,10 +293,10 @@ impl App {
|
||||
self.persistent_data.set_document_order(ids);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceWritePreferences { preferences } => {
|
||||
self.persistent_data.write_preferences(preferences);
|
||||
preferences::write(preferences);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceLoadPreferences => {
|
||||
let preferences = self.persistent_data.load_preferences();
|
||||
let preferences = preferences::read();
|
||||
let message = DesktopWrapperMessage::LoadPreferences { preferences };
|
||||
responses.push(message);
|
||||
}
|
||||
@@ -295,11 +336,11 @@ impl App {
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::OpenLaunchDocuments => {
|
||||
if self.launch_documents.is_empty() {
|
||||
if self.cli.files.is_empty() {
|
||||
return;
|
||||
}
|
||||
let app_event_scheduler = self.app_event_scheduler.clone();
|
||||
let launch_documents = std::mem::take(&mut self.launch_documents);
|
||||
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());
|
||||
@@ -329,8 +370,14 @@ impl App {
|
||||
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::CloseWindow);
|
||||
self.app_event_scheduler.schedule(AppEvent::Exit);
|
||||
}
|
||||
DesktopFrontendMessage::WindowMinimize => {
|
||||
if let Some(window) = &self.window {
|
||||
@@ -342,6 +389,11 @@ impl App {
|
||||
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();
|
||||
@@ -362,6 +414,21 @@ impl App {
|
||||
window.show_all();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::Restart => {
|
||||
self.exit(Some(ExitReason::Restart));
|
||||
}
|
||||
DesktopFrontendMessage::LoadThirdPartyLicenses => {
|
||||
let compressed = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/third-party-licenses.txt.xz"));
|
||||
let mut reader = lzma_rust2::XzReader::new(compressed.as_slice(), false);
|
||||
let mut text = String::new();
|
||||
if let Err(e) = reader.read_to_string(&mut text) {
|
||||
tracing::error!("Failed to decompress third-party licenses: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
let message = DesktopWrapperMessage::LoadThirdPartyLicenses { text };
|
||||
responses.push(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,15 +480,13 @@ impl App {
|
||||
AppEvent::UiUpdate(texture) => {
|
||||
if let Some(render_state) = self.render_state.as_mut() {
|
||||
render_state.bind_ui_texture(texture);
|
||||
let elapsed = self.last_ui_update.elapsed().as_secs_f32();
|
||||
self.last_ui_update = Instant::now();
|
||||
if elapsed < 0.5 {
|
||||
self.avg_frame_time = (self.avg_frame_time * 3. + elapsed) / 4.;
|
||||
}
|
||||
}
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
if !self.cef_init_successful {
|
||||
self.cef_init_successful = true;
|
||||
}
|
||||
}
|
||||
AppEvent::ScheduleBrowserWork(instant) => {
|
||||
if instant <= Instant::now() {
|
||||
@@ -435,9 +500,7 @@ impl App {
|
||||
window.set_cursor(event_loop, cursor);
|
||||
}
|
||||
}
|
||||
AppEvent::CloseWindow => {
|
||||
// TODO: Implement graceful shutdown
|
||||
|
||||
AppEvent::Exit => {
|
||||
tracing::info!("Exiting main event loop");
|
||||
event_loop.exit();
|
||||
}
|
||||
@@ -464,13 +527,7 @@ impl ApplicationHandler for App {
|
||||
|
||||
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));
|
||||
self.startup_time = Some(Instant::now());
|
||||
}
|
||||
|
||||
fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) {
|
||||
@@ -479,12 +536,32 @@ impl ApplicationHandler for App {
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
|
||||
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::CloseWindow);
|
||||
self.app_event_scheduler.schedule(AppEvent::Exit);
|
||||
}
|
||||
WindowEvent::SurfaceResized(_) | WindowEvent::ScaleFactorChanged { .. } => {
|
||||
self.resize();
|
||||
@@ -509,18 +586,28 @@ impl ApplicationHandler for App {
|
||||
}
|
||||
Err(RenderError::SurfaceError(wgpu::SurfaceError::OutOfMemory)) => {
|
||||
tracing::error!("GPU out of memory");
|
||||
event_loop.exit();
|
||||
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::OpenFile { path, content };
|
||||
let message = DesktopWrapperMessage::ImportFile { path, content };
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -556,6 +643,13 @@ impl ApplicationHandler for App {
|
||||
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;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -563,10 +657,26 @@ impl ApplicationHandler for App {
|
||||
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()
|
||||
{
|
||||
@@ -575,11 +685,15 @@ impl ApplicationHandler for App {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum ExitReason {
|
||||
Shutdown,
|
||||
Restart,
|
||||
UiAccelerationFailure,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use cef::args::Args;
|
||||
use cef::sys::{CEF_API_VERSION_LAST, cef_resultcode_t};
|
||||
use cef::sys::{CEF_API_VERSION_LAST, cef_log_severity_t};
|
||||
use cef::{
|
||||
App, BrowserSettings, CefString, Client, DictionaryValue, ImplCommandLine, ImplRequestContext, RequestContextSettings, SchemeHandlerFactory, Settings, WindowInfo, api_hash,
|
||||
App, BrowserSettings, CefString, Client, DictionaryValue, ImplCommandLine, ImplRequestContext, LogSeverity, RequestContextSettings, SchemeHandlerFactory, Settings, WindowInfo, api_hash,
|
||||
browser_host_create_browser_sync, execute_process,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::CefContext;
|
||||
use super::singlethreaded::SingleThreadedCefContext;
|
||||
@@ -74,11 +73,24 @@ impl<H: CefEventHandler> CefContextBuilder<H> {
|
||||
}
|
||||
|
||||
fn common_settings(instance_dir: &Path) -> Settings {
|
||||
let log_severity = match std::env::var("GRAPHITE_BROWSER_LOG") {
|
||||
Ok(level) => match level.to_lowercase().as_str() {
|
||||
"debug" => LogSeverity::from(cef_log_severity_t::LOGSEVERITY_VERBOSE),
|
||||
"info" => LogSeverity::from(cef_log_severity_t::LOGSEVERITY_INFO),
|
||||
"warn" => LogSeverity::from(cef_log_severity_t::LOGSEVERITY_WARNING),
|
||||
"error" => LogSeverity::from(cef_log_severity_t::LOGSEVERITY_ERROR),
|
||||
"none" => LogSeverity::from(cef_log_severity_t::LOGSEVERITY_DISABLE),
|
||||
_ => LogSeverity::from(cef_log_severity_t::LOGSEVERITY_FATAL),
|
||||
},
|
||||
Err(_) => LogSeverity::from(cef_log_severity_t::LOGSEVERITY_FATAL),
|
||||
};
|
||||
|
||||
Settings {
|
||||
windowless_rendering_enabled: 1,
|
||||
root_cache_path: instance_dir.to_str().map(CefString::from).unwrap(),
|
||||
cache_path: CefString::from(""),
|
||||
disable_signal_handlers: 1,
|
||||
log_severity,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -111,6 +123,8 @@ impl<H: CefEventHandler> CefContextBuilder<H> {
|
||||
|
||||
let settings = Settings {
|
||||
multi_threaded_message_loop: 1,
|
||||
#[cfg(target_os = "linux")]
|
||||
no_sandbox: 1,
|
||||
..Self::common_settings(&instance_dir)
|
||||
};
|
||||
|
||||
@@ -123,8 +137,7 @@ impl<H: CefEventHandler> CefContextBuilder<H> {
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to initialize CEF context: {:?}", e);
|
||||
std::process::exit(1);
|
||||
panic!("Failed to initialize CEF context: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -138,10 +151,7 @@ impl<H: CefEventHandler> CefContextBuilder<H> {
|
||||
let result = cef::initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut());
|
||||
if result != 1 {
|
||||
let cef_exit_code = cef::get_exit_code() as u32;
|
||||
if cef_exit_code == cef_resultcode_t::CEF_RESULT_CODE_NORMAL_EXIT_PROCESS_NOTIFIED as u32 {
|
||||
return Err(InitError::AlreadyRunning);
|
||||
}
|
||||
return Err(InitError::InitializationFailed(cef_exit_code));
|
||||
return Err(InitError::InitializationFailureCode(cef_exit_code));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -213,18 +223,16 @@ fn create_browser<H: CefEventHandler>(event_handler: H, instance_dir: PathBuf, d
|
||||
pub(crate) enum SetupError {
|
||||
#[error("This is the sub process should exit immediately")]
|
||||
Subprocess,
|
||||
#[error("Subprocess returned non zero exit code")]
|
||||
#[error("Subprocess returned non zero exit code: {0}")]
|
||||
SubprocessFailed(String),
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub(crate) enum InitError {
|
||||
#[error("Initialization failed")]
|
||||
InitializationFailed(u32),
|
||||
#[error("Initialization failed with code: {0}")]
|
||||
InitializationFailureCode(u32),
|
||||
#[error("Browser creation failed")]
|
||||
BrowserCreationFailed,
|
||||
#[error("Request context creation failed")]
|
||||
RequestContextCreationFailed,
|
||||
#[error("Another instance is already running")]
|
||||
AlreadyRunning,
|
||||
}
|
||||
|
||||
@@ -53,7 +53,8 @@ impl CefContext for MultiThreadedCefContextProxy {
|
||||
|
||||
impl Drop for MultiThreadedCefContextProxy {
|
||||
fn drop(&mut self) {
|
||||
cef::shutdown();
|
||||
// Force dropping underlying context on the UI thread
|
||||
run_on_ui_thread(move || drop(CONTEXT.take()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,10 @@ impl CefContext for SingleThreadedCefContext {
|
||||
|
||||
impl Drop for SingleThreadedCefContext {
|
||||
fn drop(&mut self) {
|
||||
tracing::debug!("Shutting down CEF");
|
||||
|
||||
// CEF wants us to close the browser before shutting down, otherwise it may run longer that necessary.
|
||||
self.browser.host().unwrap().close_browser(1);
|
||||
cef::shutdown();
|
||||
|
||||
// Sometimes some CEF processes still linger at this point and hold file handles to the cache directory.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use cef::sys::{cef_event_flags_t, cef_key_event_type_t, cef_mouse_button_type_t};
|
||||
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};
|
||||
use winit::keyboard::Key;
|
||||
|
||||
mod keymap;
|
||||
use keymap::{ToCharRepresentation, ToNativeKeycode, ToVKBits};
|
||||
@@ -70,6 +69,8 @@ pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputStat
|
||||
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
|
||||
let Some(host) = browser.host() else { return };
|
||||
|
||||
input_state.modifiers_apply_key_event(&event.logical_key, &event.state);
|
||||
|
||||
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,
|
||||
@@ -82,35 +83,6 @@ pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputStat
|
||||
|
||||
key_event.modifiers = input_state.cef_modifiers(&event.location, event.repeat).into();
|
||||
|
||||
match (&event.logical_key, event.state) {
|
||||
(Key::Named(winit::keyboard::NamedKey::Control), ElementState::Pressed) => {
|
||||
key_event.modifiers |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0;
|
||||
}
|
||||
(Key::Named(winit::keyboard::NamedKey::Control), ElementState::Released) => {
|
||||
key_event.modifiers &= !(cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0);
|
||||
}
|
||||
(Key::Named(winit::keyboard::NamedKey::Shift), ElementState::Pressed) => {
|
||||
key_event.modifiers |= cef_event_flags_t::EVENTFLAG_SHIFT_DOWN.0;
|
||||
}
|
||||
(Key::Named(winit::keyboard::NamedKey::Shift), ElementState::Released) => {
|
||||
key_event.modifiers &= !(cef_event_flags_t::EVENTFLAG_SHIFT_DOWN.0);
|
||||
}
|
||||
(Key::Named(winit::keyboard::NamedKey::Alt), ElementState::Pressed) => {
|
||||
key_event.modifiers |= cef_event_flags_t::EVENTFLAG_ALT_DOWN.0;
|
||||
}
|
||||
(Key::Named(winit::keyboard::NamedKey::Alt), ElementState::Released) => {
|
||||
key_event.modifiers &= !(cef_event_flags_t::EVENTFLAG_ALT_DOWN.0);
|
||||
}
|
||||
(Key::Named(winit::keyboard::NamedKey::Meta), ElementState::Pressed) => {
|
||||
key_event.modifiers |= cef_event_flags_t::EVENTFLAG_COMMAND_DOWN.0;
|
||||
}
|
||||
(Key::Named(winit::keyboard::NamedKey::Meta), ElementState::Released) => {
|
||||
key_event.modifiers &= !(cef_event_flags_t::EVENTFLAG_COMMAND_DOWN.0);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
key_event.windows_key_code = match &event.logical_key {
|
||||
winit::keyboard::Key::Named(named) => named.to_vk_bits(),
|
||||
winit::keyboard::Key::Character(char) => char.chars().next().unwrap_or_default().to_vk_bits(),
|
||||
|
||||
@@ -3,7 +3,7 @@ use cef::sys::cef_event_flags_t;
|
||||
use std::time::Instant;
|
||||
use winit::dpi::PhysicalPosition;
|
||||
use winit::event::{ElementState, MouseButton};
|
||||
use winit::keyboard::{KeyLocation, ModifiersState};
|
||||
use winit::keyboard::{Key, KeyLocation, ModifiersState, NamedKey};
|
||||
|
||||
use crate::cef::consts::{MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT};
|
||||
|
||||
@@ -19,6 +19,18 @@ impl InputState {
|
||||
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 {
|
||||
@@ -127,25 +139,26 @@ impl ClickTracker {
|
||||
|
||||
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::Double => {
|
||||
ElementState::Pressed if record.down_count == ClickCount::Triple => {
|
||||
*record = ClickRecord {
|
||||
down_count: ClickCount::Single,
|
||||
down_count: ClickCount::Double,
|
||||
..*record
|
||||
};
|
||||
return ClickCount::Single;
|
||||
return ClickCount::Double;
|
||||
}
|
||||
ElementState::Released if record.up_count == ClickCount::Double => {
|
||||
ElementState::Released if record.up_count == ClickCount::Triple => {
|
||||
*record = ClickRecord {
|
||||
up_count: ClickCount::Single,
|
||||
up_count: ClickCount::Double,
|
||||
..*record
|
||||
};
|
||||
return ClickCount::Single;
|
||||
return ClickCount::Double;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -155,7 +168,11 @@ impl ClickTracker {
|
||||
let within_dist = dx <= MULTICLICK_ALLOWED_TRAVEL && dy <= MULTICLICK_ALLOWED_TRAVEL;
|
||||
let within_time = now.saturating_duration_since(prev_time) <= MULTICLICK_TIMEOUT;
|
||||
|
||||
let count = if within_time && within_dist { ClickCount::Double } else { ClickCount::Single };
|
||||
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 },
|
||||
@@ -170,12 +187,14 @@ 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +34,33 @@ impl<H: CefEventHandler> ImplApp for BrowserProcessAppImpl<H> {
|
||||
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("no-default-browser-check")));
|
||||
cmd.append_switch(Some(&CefString::from("disable-component-update")));
|
||||
cmd.append_switch(Some(&CefString::from("disable-geolocation")));
|
||||
cmd.append_switch(Some(&CefString::from("disable-notifications")));
|
||||
cmd.append_switch(Some(&CefString::from("disable-audio-input")));
|
||||
cmd.append_switch(Some(&CefString::from("disable-audio-output")));
|
||||
cmd.append_switch(Some(&CefString::from("disable-sync")));
|
||||
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")));
|
||||
cmd.append_switch(Some(&CefString::from("disable-default-apps")));
|
||||
cmd.append_switch(Some(&CefString::from("disable-breakpad")));
|
||||
cmd.append_switch_with_value(Some(&CefString::from("disable-blink-features")), Some(&CefString::from("WebBluetooth,WebUSB,Serial")));
|
||||
|
||||
let extra_disabled_features = ["OptimizationHints", "OnDeviceModelService", "TranslateUI"];
|
||||
let disabled_features_switch = Some(&CefString::from("disable-features"));
|
||||
let disabled_features: String = CefString::from(&cmd.switch_value(disabled_features_switch))
|
||||
.to_string()
|
||||
.split(',')
|
||||
.chain(extra_disabled_features)
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
cmd.append_switch_with_value(disabled_features_switch, Some(&CefString::from(disabled_features.as_str())));
|
||||
|
||||
#[cfg(not(feature = "accelerated_paint"))]
|
||||
{
|
||||
|
||||
@@ -78,7 +78,6 @@ impl<H: CefEventHandler> ImplDisplayHandler for DisplayHandlerImpl<H> {
|
||||
CT_PROGRESS => CursorIcon::Progress,
|
||||
CT_NODROP => CursorIcon::NoDrop,
|
||||
CT_COPY => CursorIcon::Copy,
|
||||
CT_NONE => CursorIcon::Default,
|
||||
CT_NOTALLOWED => CursorIcon::NotAllowed,
|
||||
CT_ZOOMIN => CursorIcon::ZoomIn,
|
||||
CT_ZOOMOUT => CursorIcon::ZoomOut,
|
||||
@@ -91,6 +90,10 @@ impl<H: CefEventHandler> ImplDisplayHandler for DisplayHandlerImpl<H> {
|
||||
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,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ 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";
|
||||
|
||||
@@ -8,7 +8,7 @@ pub(crate) enum AppEvent {
|
||||
WebCommunicationInitialized,
|
||||
DesktopWrapperMessage(DesktopWrapperMessage),
|
||||
NodeGraphExecutionResult(NodeGraphExecutionResult),
|
||||
CloseWindow,
|
||||
Exit,
|
||||
#[cfg(target_os = "macos")]
|
||||
MenuEvent {
|
||||
id: String,
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
use crate::app::App;
|
||||
use crate::cef::CefHandler;
|
||||
use crate::cli::Cli;
|
||||
use crate::consts::APP_LOCK_FILE_NAME;
|
||||
use crate::event::CreateAppEventSchedulerEventLoopExt;
|
||||
use clap::Parser;
|
||||
use std::process::exit;
|
||||
use std::io::Write;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use winit::event_loop::EventLoop;
|
||||
|
||||
pub(crate) mod consts;
|
||||
pub(crate) use graphite_desktop_wrapper as wrapper;
|
||||
|
||||
mod app;
|
||||
mod cef;
|
||||
mod cli;
|
||||
mod dirs;
|
||||
mod event;
|
||||
mod gpu_context;
|
||||
mod persist;
|
||||
mod preferences;
|
||||
mod render;
|
||||
mod window;
|
||||
|
||||
mod gpu_context;
|
||||
|
||||
pub(crate) use graphite_desktop_wrapper as wrapper;
|
||||
|
||||
use app::App;
|
||||
use cef::CefHandler;
|
||||
use cli::Cli;
|
||||
use event::CreateAppEventSchedulerEventLoopExt;
|
||||
|
||||
use crate::consts::APP_LOCK_FILE_NAME;
|
||||
pub(crate) mod consts;
|
||||
|
||||
pub fn start() {
|
||||
tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init();
|
||||
@@ -38,26 +36,35 @@ pub fn start() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut lock = pidlock::Pidlock::new_validated(dirs::app_data_dir().join(APP_LOCK_FILE_NAME)).unwrap();
|
||||
match lock.acquire() {
|
||||
Ok(lock) => {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let Ok(lock_file) = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(dirs::app_data_dir().join(APP_LOCK_FILE_NAME))
|
||||
else {
|
||||
panic!("Failed to open lock file.")
|
||||
};
|
||||
let mut lock = fd_lock::RwLock::new(lock_file);
|
||||
let lock = match lock.try_write() {
|
||||
Ok(mut guard) => {
|
||||
tracing::info!("Acquired application lock");
|
||||
lock
|
||||
let _ = guard.set_len(0);
|
||||
let _ = write!(guard, "{}", std::process::id());
|
||||
let _ = guard.sync_all();
|
||||
guard
|
||||
}
|
||||
Err(pidlock::PidlockError::LockExists) => {
|
||||
Err(_) => {
|
||||
tracing::error!("Another instance is already running, Exiting.");
|
||||
exit(0);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to acquire application lock: {err}");
|
||||
exit(1);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Must be called before event loop initialization or native window integrations will break
|
||||
App::init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
let wgpu_context = futures::executor::block_on(gpu_context::create_wgpu_context());
|
||||
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
@@ -66,33 +73,64 @@ pub fn start() {
|
||||
|
||||
let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel();
|
||||
|
||||
let disable_ui_acceleration = preferences::read().disable_ui_acceleration || cli.disable_ui_acceleration;
|
||||
if disable_ui_acceleration {
|
||||
println!("UI acceleration is disabled");
|
||||
}
|
||||
|
||||
let cef_handler = cef::CefHandler::new(wgpu_context.clone(), app_event_scheduler.clone(), cef_view_info_receiver);
|
||||
let cef_context = match cef_context_builder.initialize(cef_handler, cli.disable_ui_acceleration) {
|
||||
Ok(c) => {
|
||||
let cef_context = match cef_context_builder.initialize(cef_handler, disable_ui_acceleration) {
|
||||
Ok(context) => {
|
||||
tracing::info!("CEF initialized successfully");
|
||||
c
|
||||
context
|
||||
}
|
||||
Err(cef::InitError::AlreadyRunning) => {
|
||||
tracing::error!("Another instance is already running, Exiting.");
|
||||
exit(1);
|
||||
}
|
||||
Err(cef::InitError::InitializationFailed(code)) => {
|
||||
tracing::error!("Cef initialization failed with code: {code}");
|
||||
exit(1);
|
||||
Err(cef::InitError::InitializationFailureCode(code)) => {
|
||||
panic!("CEF initialization failed with code: {code}");
|
||||
}
|
||||
Err(cef::InitError::BrowserCreationFailed) => {
|
||||
tracing::error!("Failed to create CEF browser");
|
||||
exit(1);
|
||||
panic!("Failed to create CEF browser");
|
||||
}
|
||||
Err(cef::InitError::RequestContextCreationFailed) => {
|
||||
tracing::error!("Failed to create CEF request context");
|
||||
exit(1);
|
||||
panic!("Failed to create CEF request context");
|
||||
}
|
||||
};
|
||||
|
||||
let mut app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, cli.files);
|
||||
let app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, cli);
|
||||
|
||||
event_loop.run_app(&mut app).unwrap();
|
||||
let exit_reason = app.run(event_loop);
|
||||
|
||||
// If exiting due to a UI acceleration failure, update preferences to disable it for next launch
|
||||
if matches!(exit_reason, app::ExitReason::UiAccelerationFailure) {
|
||||
tracing::error!("Disabling UI acceleration");
|
||||
preferences::modify(|prefs| {
|
||||
prefs.disable_ui_acceleration = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Explicitly drop the instance lock
|
||||
drop(lock);
|
||||
|
||||
match exit_reason {
|
||||
#[cfg(target_os = "linux")]
|
||||
app::ExitReason::Restart | app::ExitReason::UiAccelerationFailure => {
|
||||
tracing::error!("Restarting application");
|
||||
let mut command = std::process::Command::new(std::env::current_exe().unwrap());
|
||||
#[cfg(target_family = "unix")]
|
||||
let _ = std::os::unix::process::CommandExt::exec(&mut command);
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
let _ = command.spawn();
|
||||
tracing::error!("Failed to restart application");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Workaround for a Windows-specific exception that occurs when `app` is dropped.
|
||||
// The issue causes the window to hang for a few seconds before closing.
|
||||
// Appears to be related to CEF object destruction order.
|
||||
// Calling `exit` bypasses rust teardown and lets Windows perform process cleanup.
|
||||
// TODO: Identify and fix the underlying CEF shutdown issue so this workaround can be removed.
|
||||
#[cfg(target_os = "windows")]
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
pub fn start_helper() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::wrapper::messages::{Document, DocumentId, Preferences};
|
||||
use crate::wrapper::messages::{Document, DocumentId};
|
||||
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct PersistentData {
|
||||
@@ -72,22 +72,6 @@ impl PersistentData {
|
||||
self.flush();
|
||||
}
|
||||
|
||||
pub(crate) fn write_preferences(&mut self, preferences: Preferences) {
|
||||
let Ok(preferences) = ron::ser::to_string_pretty(&preferences, Default::default()) else {
|
||||
tracing::error!("Failed to serialize preferences");
|
||||
return;
|
||||
};
|
||||
std::fs::write(Self::preferences_file_path(), &preferences).unwrap_or_else(|e| {
|
||||
tracing::error!("Failed to write preferences to disk: {e}");
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn load_preferences(&self) -> Option<Preferences> {
|
||||
let data = std::fs::read_to_string(Self::preferences_file_path()).ok()?;
|
||||
let preferences = ron::from_str(&data).ok()?;
|
||||
Some(preferences)
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
let data = match ron::ser::to_string_pretty(self, Default::default()) {
|
||||
Ok(d) => d,
|
||||
@@ -129,12 +113,6 @@ impl PersistentData {
|
||||
path.push(crate::consts::APP_STATE_FILE_NAME);
|
||||
path
|
||||
}
|
||||
|
||||
fn preferences_file_path() -> std::path::PathBuf {
|
||||
let mut path = crate::dirs::app_data_dir();
|
||||
path.push(crate::consts::APP_PREFERENCES_FILE_NAME);
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
@@ -190,7 +168,7 @@ impl DocumentStore {
|
||||
|
||||
fn document_path(id: &DocumentId) -> std::path::PathBuf {
|
||||
let mut path = crate::dirs::app_autosave_documents_dir();
|
||||
path.push(format!("{:x}.graphite", id.0));
|
||||
path.push(format!("{:x}.{}", id.0, graphite_desktop_wrapper::FILE_EXTENSION));
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
33
desktop/src/preferences.rs
Normal file
33
desktop/src/preferences.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use graphite_desktop_wrapper::messages::Preferences;
|
||||
|
||||
pub(crate) fn write(preferences: Preferences) {
|
||||
let Ok(preferences) = ron::ser::to_string_pretty(&preferences, Default::default()) else {
|
||||
tracing::error!("Failed to serialize preferences");
|
||||
return;
|
||||
};
|
||||
std::fs::write(file_path(), &preferences).unwrap_or_else(|e| {
|
||||
tracing::error!("Failed to write preferences to disk: {e}");
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn read() -> Preferences {
|
||||
let Ok(data) = std::fs::read_to_string(file_path()) else {
|
||||
return Preferences::default();
|
||||
};
|
||||
let Ok(preferences) = ron::from_str(&data) else {
|
||||
return Preferences::default();
|
||||
};
|
||||
preferences
|
||||
}
|
||||
|
||||
pub(crate) fn modify(f: impl FnOnce(&mut Preferences)) {
|
||||
let mut preferences = read();
|
||||
f(&mut preferences);
|
||||
write(preferences);
|
||||
}
|
||||
|
||||
fn file_path() -> std::path::PathBuf {
|
||||
let mut path = crate::dirs::app_data_dir();
|
||||
path.push(crate::consts::APP_PREFERENCES_FILE_NAME);
|
||||
path
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::window::Window;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::wrapper::{Color, WgpuContext, WgpuExecutor};
|
||||
use crate::window::Window;
|
||||
use crate::wrapper::{TargetTexture, WgpuContext, WgpuExecutor};
|
||||
|
||||
#[derive(derivative::Derivative)]
|
||||
#[derivative(Debug)]
|
||||
@@ -17,11 +18,12 @@ pub(crate) struct RenderState {
|
||||
viewport_scale: [f32; 2],
|
||||
viewport_offset: [f32; 2],
|
||||
viewport_texture: Option<wgpu::Texture>,
|
||||
overlays_texture: Option<wgpu::Texture>,
|
||||
overlays_texture: Option<TargetTexture>,
|
||||
ui_texture: Option<wgpu::Texture>,
|
||||
bind_group: Option<wgpu::BindGroup>,
|
||||
#[derivative(Debug = "ignore")]
|
||||
overlays_scene: Option<vello::Scene>,
|
||||
surface_outdated: bool,
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
@@ -185,6 +187,7 @@ impl RenderState {
|
||||
ui_texture: None,
|
||||
bind_group: None,
|
||||
overlays_scene: None,
|
||||
surface_outdated: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +198,7 @@ impl RenderState {
|
||||
|
||||
self.desired_width = width;
|
||||
self.desired_height = height;
|
||||
self.surface_outdated = true;
|
||||
|
||||
if width > 0 && height > 0 && (self.config.width != width || self.config.height != height) {
|
||||
self.config.width = width;
|
||||
@@ -208,25 +212,23 @@ impl RenderState {
|
||||
self.update_bindgroup();
|
||||
}
|
||||
|
||||
pub(crate) fn bind_overlays_texture(&mut self, overlays_texture: wgpu::Texture) {
|
||||
self.overlays_texture = Some(overlays_texture);
|
||||
self.update_bindgroup();
|
||||
}
|
||||
|
||||
pub(crate) fn bind_ui_texture(&mut self, bind_ui_texture: wgpu::Texture) {
|
||||
self.ui_texture = Some(bind_ui_texture);
|
||||
self.update_bindgroup();
|
||||
}
|
||||
|
||||
pub(crate) fn set_viewport_scale(&mut self, scale: [f32; 2]) {
|
||||
self.surface_outdated = true;
|
||||
self.viewport_scale = scale;
|
||||
}
|
||||
|
||||
pub(crate) fn set_viewport_offset(&mut self, offset: [f32; 2]) {
|
||||
self.surface_outdated = true;
|
||||
self.viewport_offset = offset;
|
||||
}
|
||||
|
||||
pub(crate) fn set_overlays_scene(&mut self, scene: vello::Scene) {
|
||||
self.surface_outdated = true;
|
||||
self.overlays_scene = Some(scene);
|
||||
}
|
||||
|
||||
@@ -236,15 +238,18 @@ impl RenderState {
|
||||
return;
|
||||
};
|
||||
let size = glam::UVec2::new(viewport_texture.width(), viewport_texture.height());
|
||||
let texture = futures::executor::block_on(self.executor.render_vello_scene_to_texture(&scene, size, &Default::default(), Color::TRANSPARENT));
|
||||
let Ok(texture) = texture else {
|
||||
tracing::error!("Error rendering overlays");
|
||||
let result = futures::executor::block_on(self.executor.render_vello_scene_to_target_texture(&scene, size, &Default::default(), None, &mut self.overlays_texture));
|
||||
if let Err(e) = result {
|
||||
tracing::error!("Error rendering overlays: {:?}", e);
|
||||
return;
|
||||
};
|
||||
self.bind_overlays_texture(texture);
|
||||
}
|
||||
self.update_bindgroup();
|
||||
}
|
||||
|
||||
pub(crate) fn render(&mut self, window: &Window) -> Result<(), RenderError> {
|
||||
if !self.surface_outdated {
|
||||
return Ok(());
|
||||
}
|
||||
let ui_scale = if let Some(ui_texture) = &self.ui_texture
|
||||
&& (self.desired_width != ui_texture.width() || self.desired_height != ui_texture.height())
|
||||
{
|
||||
@@ -306,13 +311,19 @@ impl RenderState {
|
||||
if ui_scale.is_some() {
|
||||
return Err(RenderError::OutdatedUITextureError);
|
||||
}
|
||||
self.surface_outdated = false;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_bindgroup(&mut self) {
|
||||
self.surface_outdated = true;
|
||||
let viewport_texture_view = self.viewport_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let overlays_texture_view = self.overlays_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let overlays_texture_view = self
|
||||
.overlays_texture
|
||||
.as_ref()
|
||||
.map(|target| Cow::Borrowed(target.view()))
|
||||
.unwrap_or_else(|| Cow::Owned(self.transparent_texture.create_view(&wgpu::TextureViewDescriptor::default())));
|
||||
let ui_texture_view = self.ui_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let bind_group = self.context.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
@@ -324,7 +335,7 @@ impl RenderState {
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(&overlays_texture_view),
|
||||
resource: wgpu::BindingResource::TextureView(overlays_texture_view.as_ref()),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::consts::APP_NAME;
|
||||
use crate::event::AppEventScheduler;
|
||||
use crate::wrapper::messages::MenuItem;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use winit::cursor::{CursorIcon, CustomCursor, CustomCursorSource};
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
use winit::monitor::Fullscreen;
|
||||
use winit::window::{Window as WinitWindow, WindowAttributes};
|
||||
|
||||
use crate::consts::APP_NAME;
|
||||
use crate::event::AppEventScheduler;
|
||||
use crate::wrapper::messages::MenuItem;
|
||||
|
||||
pub(crate) trait NativeWindow {
|
||||
fn init() {}
|
||||
fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes;
|
||||
@@ -41,7 +41,13 @@ pub(crate) struct Window {
|
||||
#[allow(dead_code)]
|
||||
native_handle: native::NativeWindowImpl,
|
||||
custom_cursors: HashMap<CustomCursorSource, CustomCursor>,
|
||||
clipboard: window_clipboard::Clipboard,
|
||||
clipboard: Option<window_clipboard::Clipboard>,
|
||||
}
|
||||
impl Drop for Window {
|
||||
fn drop(&mut self) {
|
||||
// Clipboard must be dropped before `winit_window`
|
||||
drop(self.clipboard.take());
|
||||
}
|
||||
}
|
||||
|
||||
impl Window {
|
||||
@@ -62,7 +68,7 @@ impl Window {
|
||||
|
||||
let winit_window = event_loop.create_window(attributes).unwrap();
|
||||
let native_handle = native::NativeWindowImpl::new(winit_window.as_ref(), app_event_scheduler);
|
||||
let clipboard = unsafe { window_clipboard::Clipboard::connect(&winit_window) }.expect("failed to create clipboard");
|
||||
let clipboard = unsafe { window_clipboard::Clipboard::connect(&winit_window) }.ok();
|
||||
Self {
|
||||
winit_window: winit_window.into(),
|
||||
native_handle,
|
||||
@@ -105,6 +111,9 @@ impl Window {
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_maximize(&self) {
|
||||
if self.is_fullscreen() {
|
||||
return;
|
||||
}
|
||||
self.winit_window.set_maximized(!self.winit_window.is_maximized());
|
||||
}
|
||||
|
||||
@@ -112,11 +121,22 @@ impl Window {
|
||||
self.winit_window.is_maximized()
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_fullscreen(&mut self) {
|
||||
if self.is_fullscreen() {
|
||||
self.winit_window.set_fullscreen(None);
|
||||
} else {
|
||||
self.winit_window.set_fullscreen(Some(Fullscreen::Borderless(None)));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_fullscreen(&self) -> bool {
|
||||
self.winit_window.fullscreen().is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn start_drag(&self) {
|
||||
if self.is_fullscreen() {
|
||||
return;
|
||||
}
|
||||
let _ = self.winit_window.drag_window();
|
||||
}
|
||||
|
||||
@@ -149,16 +169,35 @@ impl Window {
|
||||
};
|
||||
custom_cursor.into()
|
||||
}
|
||||
Cursor::None => {
|
||||
self.winit_window.set_cursor_visible(false);
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.winit_window.set_cursor_visible(true);
|
||||
self.winit_window.set_cursor(cursor);
|
||||
}
|
||||
|
||||
pub(crate) fn start_pointer_lock(&self) {
|
||||
let _ = self.winit_window.set_cursor_grab(winit::window::CursorGrabMode::Locked);
|
||||
self.winit_window.set_cursor_visible(false);
|
||||
}
|
||||
|
||||
pub(crate) fn end_pointer_lock(&self) {
|
||||
let _ = self.winit_window.set_cursor_grab(winit::window::CursorGrabMode::None);
|
||||
self.winit_window.set_cursor_visible(true);
|
||||
}
|
||||
|
||||
pub(crate) fn update_menu(&self, entries: Vec<MenuItem>) {
|
||||
self.native_handle.update_menu(entries);
|
||||
}
|
||||
|
||||
pub(crate) fn clipboard_read(&self) -> Option<String> {
|
||||
match self.clipboard.read() {
|
||||
let Some(clipboard) = &self.clipboard else {
|
||||
tracing::error!("Clipboard not available");
|
||||
return None;
|
||||
};
|
||||
match clipboard.read() {
|
||||
Ok(data) => Some(data),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to read from clipboard: {e}");
|
||||
@@ -168,7 +207,11 @@ impl Window {
|
||||
}
|
||||
|
||||
pub(crate) fn clipboard_write(&mut self, data: String) {
|
||||
if let Err(e) = self.clipboard.write(data) {
|
||||
let Some(clipboard) = &mut self.clipboard else {
|
||||
tracing::error!("Clipboard not available");
|
||||
return;
|
||||
};
|
||||
if let Err(e) = clipboard.write(data) {
|
||||
tracing::error!("Failed to write to clipboard: {e}")
|
||||
}
|
||||
}
|
||||
@@ -177,6 +220,7 @@ impl Window {
|
||||
pub(crate) enum Cursor {
|
||||
Icon(CursorIcon),
|
||||
Custom(CustomCursorSource),
|
||||
None,
|
||||
}
|
||||
impl From<CursorIcon> for Cursor {
|
||||
fn from(icon: CursorIcon) -> Self {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use windows::Win32::System::Com::{COINIT_APARTMENTTHREADED, CoInitializeEx};
|
||||
use windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole};
|
||||
use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
|
||||
use windows::core::HSTRING;
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
@@ -13,6 +14,12 @@ pub(super) struct NativeWindowImpl {
|
||||
|
||||
impl super::NativeWindow for NativeWindowImpl {
|
||||
fn init() {
|
||||
// Attach to parent console if launched from a terminal (no-op otherwise)
|
||||
unsafe {
|
||||
let _ = AttachConsole(ATTACH_PARENT_PROCESS);
|
||||
}
|
||||
|
||||
// Set stable app ID
|
||||
let app_id = HSTRING::from(APP_ID);
|
||||
unsafe {
|
||||
let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok();
|
||||
|
||||
@@ -61,7 +61,7 @@ impl NativeWindowHandle {
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
None,
|
||||
main,
|
||||
None,
|
||||
HINSTANCE(std::ptr::null_mut()),
|
||||
// Pass the main window's HWND to WM_NCCREATE so the helper can store it.
|
||||
@@ -118,7 +118,7 @@ impl NativeWindowHandle {
|
||||
}
|
||||
|
||||
// Force window update
|
||||
let _ = unsafe { SetWindowPos(main, None, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER) };
|
||||
let _ = unsafe { SetWindowPos(main, None, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE) };
|
||||
|
||||
native_handle
|
||||
}
|
||||
@@ -210,10 +210,10 @@ unsafe fn ensure_helper_class() {
|
||||
// Main window message handler, called on the UI thread for every message the main window receives.
|
||||
unsafe extern "system" fn main_window_handle_message(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||
if msg == WM_NCCALCSIZE && wparam.0 != 0 {
|
||||
// When maximized, shrink to visible frame so content doesn't extend beyond it.
|
||||
if unsafe { IsZoomed(hwnd).as_bool() } {
|
||||
let params = unsafe { &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS) };
|
||||
let params = unsafe { &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS) };
|
||||
|
||||
// When maximized, shrink to visible frame so content doesn't extend beyond it.
|
||||
if unsafe { IsZoomed(hwnd).as_bool() } && !is_effectively_fullscreen(params.rgrc[0]) {
|
||||
let dpi = unsafe { GetDpiForWindow(hwnd) };
|
||||
let size = unsafe { GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) };
|
||||
let pad = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) };
|
||||
@@ -325,20 +325,20 @@ unsafe fn position_helper(main: HWND, helper: HWND) {
|
||||
let w = (r.right - r.left) + RESIZE_BAND_THICKNESS * 2;
|
||||
let h = (r.bottom - r.top) + RESIZE_BAND_THICKNESS * 2;
|
||||
|
||||
let _ = unsafe { SetWindowPos(helper, main, x, y, w, h, SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOSENDCHANGING) };
|
||||
let _ = unsafe { SetWindowPos(helper, main, x, y, w, h, SWP_NOACTIVATE | SWP_NOSENDCHANGING) };
|
||||
}
|
||||
|
||||
unsafe fn calculate_hit(helper: HWND, lparam: LPARAM) -> u32 {
|
||||
let x = (lparam.0 & 0xFFFF) as i16 as u32;
|
||||
let y = ((lparam.0 >> 16) & 0xFFFF) as i16 as u32;
|
||||
let x = (lparam.0 & 0xFFFF) as i16 as i32;
|
||||
let y = ((lparam.0 >> 16) & 0xFFFF) as i16 as i32;
|
||||
|
||||
let mut r = RECT::default();
|
||||
let _ = unsafe { GetWindowRect(helper, &mut r) };
|
||||
|
||||
let on_top = y < (r.top + RESIZE_BAND_THICKNESS) as u32;
|
||||
let on_right = x >= (r.right - RESIZE_BAND_THICKNESS) as u32;
|
||||
let on_bottom = y >= (r.bottom - RESIZE_BAND_THICKNESS) as u32;
|
||||
let on_left = x < (r.left + RESIZE_BAND_THICKNESS) as u32;
|
||||
let on_top = y < (r.top + RESIZE_BAND_THICKNESS) as i32;
|
||||
let on_right = x >= (r.right - RESIZE_BAND_THICKNESS) as i32;
|
||||
let on_bottom = y >= (r.bottom - RESIZE_BAND_THICKNESS) as i32;
|
||||
let on_left = x < (r.left + RESIZE_BAND_THICKNESS) as i32;
|
||||
|
||||
match (on_top, on_right, on_bottom, on_left) {
|
||||
(true, _, _, true) => HTTOPLEFT,
|
||||
@@ -366,3 +366,27 @@ unsafe fn calculate_resize_direction(helper: HWND, lparam: LPARAM) -> Option<u32
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the rect is effectively fullscreen, meaning it would cover the entire monitor.
|
||||
// We need to use this heuristic because Windows doesn't provide a way to check for fullscreen state.
|
||||
fn is_effectively_fullscreen(rect: RECT) -> bool {
|
||||
let hmon = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST) };
|
||||
if hmon.is_invalid() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut monitor_info = MONITORINFO {
|
||||
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
if !unsafe { GetMonitorInfoW(hmon, &mut monitor_info) }.as_bool() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow a tiny tolerance for DPI / rounding issues
|
||||
const EPS: i32 = 1;
|
||||
(rect.left - monitor_info.rcMonitor.left).abs() <= EPS
|
||||
&& (rect.top - monitor_info.rcMonitor.top).abs() <= EPS
|
||||
&& (rect.right - monitor_info.rcMonitor.right).abs() <= EPS
|
||||
&& (rect.bottom - monitor_info.rcMonitor.bottom).abs() <= EPS
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
use graphene_std::Color;
|
||||
use graphene_std::raster::Image;
|
||||
use graphite_editor::messages::app_window::app_window_message_handler::AppWindowPlatform;
|
||||
use graphite_editor::messages::clipboard::utility_types::ClipboardContentRaw;
|
||||
use graphite_editor::messages::prelude::*;
|
||||
|
||||
use super::DesktopWrapperMessageDispatcher;
|
||||
use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, EditorMessage, OpenFileDialogContext, Platform, SaveFileDialogContext};
|
||||
use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, EditorMessage, OpenFileDialogContext, SaveFileDialogContext};
|
||||
|
||||
pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: DesktopWrapperMessage) {
|
||||
match message {
|
||||
@@ -15,9 +12,9 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
|
||||
DesktopWrapperMessage::Input(message) => {
|
||||
dispatcher.queue_editor_message(EditorMessage::InputPreprocessor(message));
|
||||
}
|
||||
DesktopWrapperMessage::OpenFileDialogResult { path, content, context } => match context {
|
||||
OpenFileDialogContext::Document => {
|
||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { path, content });
|
||||
DesktopWrapperMessage::FileDialogResult { path, content, context } => match context {
|
||||
OpenFileDialogContext::Open => {
|
||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenFile { path, content });
|
||||
}
|
||||
OpenFileDialogContext::Import => {
|
||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportFile { path, content });
|
||||
@@ -36,90 +33,14 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
|
||||
}
|
||||
},
|
||||
DesktopWrapperMessage::OpenFile { path, content } => {
|
||||
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase();
|
||||
match extension.as_str() {
|
||||
"graphite" => {
|
||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { path, content });
|
||||
}
|
||||
_ => {
|
||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportFile { path, content });
|
||||
}
|
||||
}
|
||||
}
|
||||
DesktopWrapperMessage::OpenDocument { path, content } => {
|
||||
let Ok(content) = String::from_utf8(content) else {
|
||||
tracing::warn!("Document file is invalid: {}", path.display());
|
||||
return;
|
||||
};
|
||||
|
||||
let message = PortfolioMessage::OpenDocumentFile {
|
||||
document_name: None,
|
||||
document_path: Some(path),
|
||||
document_serialized_content: content,
|
||||
};
|
||||
let message = PortfolioMessage::OpenFile { path, content };
|
||||
dispatcher.queue_editor_message(message);
|
||||
}
|
||||
DesktopWrapperMessage::ImportFile { path, content } => {
|
||||
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase();
|
||||
match extension.as_str() {
|
||||
"svg" => {
|
||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportSvg { path, content });
|
||||
}
|
||||
_ => {
|
||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportImage { path, content });
|
||||
}
|
||||
}
|
||||
}
|
||||
DesktopWrapperMessage::ImportSvg { path, content } => {
|
||||
let Ok(content) = String::from_utf8(content) else {
|
||||
tracing::warn!("Svg file is invalid: {}", path.display());
|
||||
return;
|
||||
};
|
||||
|
||||
let message = PortfolioMessage::PasteSvg {
|
||||
name: path.file_stem().map(|s| s.to_string_lossy().to_string()),
|
||||
svg: content,
|
||||
mouse: None,
|
||||
parent_and_insert_index: None,
|
||||
};
|
||||
dispatcher.queue_editor_message(message);
|
||||
}
|
||||
DesktopWrapperMessage::ImportImage { path, content } => {
|
||||
let name = path.file_stem().and_then(|s| s.to_str()).map(|s| s.to_string());
|
||||
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase();
|
||||
let Some(image_format) = image::ImageFormat::from_extension(&extension) else {
|
||||
tracing::warn!("Unsupported file type: {}", path.display());
|
||||
return;
|
||||
};
|
||||
let reader = image::ImageReader::with_format(std::io::Cursor::new(content), image_format);
|
||||
let Ok(image) = reader.decode() else {
|
||||
tracing::error!("Failed to decode image: {}", path.display());
|
||||
return;
|
||||
};
|
||||
let width = image.width();
|
||||
let height = image.height();
|
||||
|
||||
// TODO: Handle Image formats with more than 8 bits per channel
|
||||
let image_data = image.to_rgba8();
|
||||
let image = Image::<Color>::from_image_data(image_data.as_raw(), width, height);
|
||||
let message = PortfolioMessage::PasteImage {
|
||||
name,
|
||||
image,
|
||||
mouse: None,
|
||||
parent_and_insert_index: None,
|
||||
};
|
||||
let message = PortfolioMessage::ImportFile { path, content };
|
||||
dispatcher.queue_editor_message(message);
|
||||
}
|
||||
DesktopWrapperMessage::PollNodeGraphEvaluation => dispatcher.poll_node_graph_evaluation(),
|
||||
DesktopWrapperMessage::UpdatePlatform(platform) => {
|
||||
let platform = match platform {
|
||||
Platform::Windows => AppWindowPlatform::Windows,
|
||||
Platform::Mac => AppWindowPlatform::Mac,
|
||||
Platform::Linux => AppWindowPlatform::Linux,
|
||||
};
|
||||
let message = AppWindowMessage::UpdatePlatform { platform };
|
||||
dispatcher.queue_editor_message(message);
|
||||
}
|
||||
DesktopWrapperMessage::UpdateMaximized { maximized } => {
|
||||
let message = FrontendMessage::UpdateMaximized { maximized };
|
||||
dispatcher.queue_editor_message(message);
|
||||
@@ -172,5 +93,13 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
|
||||
dispatcher.queue_editor_message(message);
|
||||
}
|
||||
}
|
||||
DesktopWrapperMessage::PointerLockMove { x, y } => {
|
||||
let message = AppWindowMessage::PointerLockMove { x, y };
|
||||
dispatcher.queue_editor_message(message);
|
||||
}
|
||||
DesktopWrapperMessage::LoadThirdPartyLicenses { text } => {
|
||||
let message = DialogMessage::RequestLicensesThirdPartyDialogWithLicenseText { license_text: text };
|
||||
dispatcher.queue_editor_message(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use graphite_editor::messages::layout::utility_types::layout_widget::LayoutTarget;
|
||||
use graphite_editor::messages::prelude::FrontendMessage;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::DesktopWrapperMessageDispatcher;
|
||||
use super::messages::{DesktopFrontendMessage, Document, FileFilter, OpenFileDialogContext, SaveFileDialogContext};
|
||||
@@ -10,29 +11,17 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
||||
FrontendMessage::RenderOverlays { context } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene()));
|
||||
}
|
||||
FrontendMessage::TriggerOpenDocument => {
|
||||
FrontendMessage::TriggerOpen => {
|
||||
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
||||
title: "Open Document".to_string(),
|
||||
filters: vec![FileFilter {
|
||||
name: "Graphite".to_string(),
|
||||
extensions: vec!["graphite".to_string()],
|
||||
}],
|
||||
context: OpenFileDialogContext::Document,
|
||||
filters: vec![],
|
||||
context: OpenFileDialogContext::Open,
|
||||
});
|
||||
}
|
||||
FrontendMessage::TriggerImport => {
|
||||
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
||||
title: "Import File".to_string(),
|
||||
filters: vec![
|
||||
FileFilter {
|
||||
name: "Svg".to_string(),
|
||||
extensions: vec!["svg".to_string()],
|
||||
},
|
||||
FileFilter {
|
||||
name: "Image".to_string(),
|
||||
extensions: vec!["png".to_string(), "jpg".to_string(), "jpeg".to_string(), "bmp".to_string()],
|
||||
},
|
||||
],
|
||||
filters: vec![],
|
||||
context: OpenFileDialogContext::Import,
|
||||
});
|
||||
}
|
||||
@@ -115,7 +104,10 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
||||
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
FrontendMessage::UpdateMenuBarLayout { diff } => {
|
||||
FrontendMessage::UpdateLayout {
|
||||
layout_target: LayoutTarget::MenuBar,
|
||||
diff,
|
||||
} => {
|
||||
use graphite_editor::messages::tool::tool_messages::tool_prelude::{DiffUpdate, WidgetDiff};
|
||||
match diff.as_slice() {
|
||||
[
|
||||
@@ -136,6 +128,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
||||
FrontendMessage::TriggerClipboardWrite { content } => {
|
||||
dispatcher.respond(DesktopFrontendMessage::ClipboardWrite { content });
|
||||
}
|
||||
FrontendMessage::WindowPointerLock => {
|
||||
dispatcher.respond(DesktopFrontendMessage::PointerLock);
|
||||
}
|
||||
FrontendMessage::WindowClose => {
|
||||
dispatcher.respond(DesktopFrontendMessage::WindowClose);
|
||||
}
|
||||
@@ -145,6 +140,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
||||
FrontendMessage::WindowMaximize => {
|
||||
dispatcher.respond(DesktopFrontendMessage::WindowMaximize);
|
||||
}
|
||||
FrontendMessage::WindowFullscreen => {
|
||||
dispatcher.respond(DesktopFrontendMessage::WindowFullscreen);
|
||||
}
|
||||
FrontendMessage::WindowDrag => {
|
||||
dispatcher.respond(DesktopFrontendMessage::WindowDrag);
|
||||
}
|
||||
@@ -157,6 +155,12 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
||||
FrontendMessage::WindowShowAll => {
|
||||
dispatcher.respond(DesktopFrontendMessage::WindowShowAll);
|
||||
}
|
||||
FrontendMessage::WindowRestart => {
|
||||
dispatcher.respond(DesktopFrontendMessage::Restart);
|
||||
}
|
||||
FrontendMessage::TriggerDisplayThirdPartyLicensesDialog => {
|
||||
dispatcher.respond(DesktopFrontendMessage::LoadThirdPartyLicenses);
|
||||
}
|
||||
m => return Some(m),
|
||||
}
|
||||
None
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
use graph_craft::wasm_application_io::WasmApplicationIo;
|
||||
use graphite_editor::application::Editor;
|
||||
use graphite_editor::application::{Editor, Environment, Host, Platform};
|
||||
use graphite_editor::messages::prelude::{FrontendMessage, Message};
|
||||
use message_dispatcher::DesktopWrapperMessageDispatcher;
|
||||
use messages::{DesktopFrontendMessage, DesktopWrapperMessage};
|
||||
|
||||
// TODO: Remove usage of this reexport in desktop create and remove this line
|
||||
pub use graphene_std::Color;
|
||||
|
||||
pub use graphite_editor::consts::FILE_EXTENSION;
|
||||
pub use wgpu_executor::TargetTexture;
|
||||
pub use wgpu_executor::WgpuContext;
|
||||
pub use wgpu_executor::WgpuContextBuilder;
|
||||
pub use wgpu_executor::WgpuExecutor;
|
||||
pub use wgpu_executor::WgpuFeatures;
|
||||
|
||||
pub mod messages;
|
||||
use messages::{DesktopFrontendMessage, DesktopWrapperMessage};
|
||||
|
||||
mod message_dispatcher;
|
||||
use message_dispatcher::DesktopWrapperMessageDispatcher;
|
||||
|
||||
mod handle_desktop_wrapper_message;
|
||||
mod intercept_editor_message;
|
||||
mod intercept_frontend_message;
|
||||
|
||||
mod message_dispatcher;
|
||||
pub mod messages;
|
||||
pub(crate) mod utils;
|
||||
|
||||
pub struct DesktopWrapper {
|
||||
@@ -27,8 +23,18 @@ pub struct DesktopWrapper {
|
||||
}
|
||||
|
||||
impl DesktopWrapper {
|
||||
pub fn new() -> Self {
|
||||
Self { editor: Editor::new() }
|
||||
pub fn new(uuid_random_seed: u64) -> Self {
|
||||
#[cfg(target_os = "windows")]
|
||||
let host = Host::Windows;
|
||||
#[cfg(target_os = "macos")]
|
||||
let host = Host::Mac;
|
||||
#[cfg(target_os = "linux")]
|
||||
let host = Host::Linux;
|
||||
let env = Environment { platform: Platform::Desktop, host };
|
||||
|
||||
Self {
|
||||
editor: Editor::new(env, uuid_random_seed),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&self, wgpu_context: WgpuContext) {
|
||||
@@ -51,12 +57,6 @@ impl DesktopWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DesktopWrapper {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum NodeGraphExecutionResult {
|
||||
HasRun(Option<wgpu::Texture>),
|
||||
NotRun,
|
||||
|
||||
@@ -65,19 +65,23 @@ pub enum DesktopFrontendMessage {
|
||||
ClipboardWrite {
|
||||
content: String,
|
||||
},
|
||||
PointerLock,
|
||||
WindowClose,
|
||||
WindowMinimize,
|
||||
WindowMaximize,
|
||||
WindowFullscreen,
|
||||
WindowDrag,
|
||||
WindowHide,
|
||||
WindowHideOthers,
|
||||
WindowShowAll,
|
||||
Restart,
|
||||
LoadThirdPartyLicenses,
|
||||
}
|
||||
|
||||
pub enum DesktopWrapperMessage {
|
||||
FromWeb(Box<EditorMessage>),
|
||||
Input(InputMessage),
|
||||
OpenFileDialogResult {
|
||||
FileDialogResult {
|
||||
path: PathBuf,
|
||||
content: Vec<u8>,
|
||||
context: OpenFileDialogContext,
|
||||
@@ -86,10 +90,6 @@ pub enum DesktopWrapperMessage {
|
||||
path: PathBuf,
|
||||
context: SaveFileDialogContext,
|
||||
},
|
||||
OpenDocument {
|
||||
path: PathBuf,
|
||||
content: Vec<u8>,
|
||||
},
|
||||
OpenFile {
|
||||
path: PathBuf,
|
||||
content: Vec<u8>,
|
||||
@@ -98,16 +98,7 @@ pub enum DesktopWrapperMessage {
|
||||
path: PathBuf,
|
||||
content: Vec<u8>,
|
||||
},
|
||||
ImportSvg {
|
||||
path: PathBuf,
|
||||
content: Vec<u8>,
|
||||
},
|
||||
ImportImage {
|
||||
path: PathBuf,
|
||||
content: Vec<u8>,
|
||||
},
|
||||
PollNodeGraphEvaluation,
|
||||
UpdatePlatform(Platform),
|
||||
UpdateMaximized {
|
||||
maximized: bool,
|
||||
},
|
||||
@@ -124,7 +115,7 @@ pub enum DesktopWrapperMessage {
|
||||
id: DocumentId,
|
||||
},
|
||||
LoadPreferences {
|
||||
preferences: Option<Preferences>,
|
||||
preferences: Preferences,
|
||||
},
|
||||
MenuEvent {
|
||||
id: String,
|
||||
@@ -132,6 +123,13 @@ pub enum DesktopWrapperMessage {
|
||||
ClipboardReadResult {
|
||||
content: Option<String>,
|
||||
},
|
||||
PointerLockMove {
|
||||
x: f64,
|
||||
y: f64,
|
||||
},
|
||||
LoadThirdPartyLicenses {
|
||||
text: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]
|
||||
@@ -148,7 +146,7 @@ pub struct FileFilter {
|
||||
}
|
||||
|
||||
pub enum OpenFileDialogContext {
|
||||
Document,
|
||||
Open,
|
||||
Import,
|
||||
}
|
||||
|
||||
@@ -157,12 +155,6 @@ pub enum SaveFileDialogContext {
|
||||
File { content: Vec<u8> },
|
||||
}
|
||||
|
||||
pub enum Platform {
|
||||
Windows,
|
||||
Mac,
|
||||
Linux,
|
||||
}
|
||||
|
||||
pub enum MenuItem {
|
||||
Action {
|
||||
id: String,
|
||||
|
||||
@@ -21,7 +21,7 @@ pub(crate) mod menu {
|
||||
widgets
|
||||
.into_iter()
|
||||
.map(|widget| {
|
||||
let text_button = match &widget.widget {
|
||||
let text_button = match widget.widget.as_ref() {
|
||||
Widget::TextButton(text_button) => text_button,
|
||||
_ => panic!("Menu bar layout top-level widgets are supposed to be text buttons"),
|
||||
};
|
||||
|
||||
@@ -18,16 +18,18 @@ fn main() {
|
||||
if !gh.trim().is_empty() {
|
||||
gh.trim().to_string()
|
||||
} else {
|
||||
git_or_unknown(&["rev-parse", "--abbrev-ref", "HEAD"])
|
||||
git(&["rev-parse", "--abbrev-ref", "HEAD"]).unwrap_or_default()
|
||||
}
|
||||
});
|
||||
|
||||
// Instruct Cargo to set environment variables for compile time.
|
||||
// They are accessed with the `env!("GRAPHITE_*")` macro in the codebase.
|
||||
println!("cargo:rustc-env=GRAPHITE_GIT_COMMIT_DATE={commit_date}");
|
||||
println!("cargo:rustc-env=GRAPHITE_GIT_COMMIT_HASH={commit_hash}");
|
||||
println!("cargo:rustc-env=GRAPHITE_GIT_COMMIT_BRANCH={commit_branch}");
|
||||
println!("cargo:rustc-env=GRAPHITE_RELEASE_SERIES={GRAPHITE_RELEASE_SERIES}");
|
||||
if !commit_branch.is_empty() {
|
||||
println!("cargo:rustc-env=GRAPHITE_GIT_COMMIT_BRANCH={commit_branch}");
|
||||
}
|
||||
println!("cargo:rustc-env=GRAPHITE_GIT_COMMIT_HASH={commit_hash}");
|
||||
println!("cargo:rustc-env=GRAPHITE_GIT_COMMIT_DATE={commit_date}");
|
||||
}
|
||||
|
||||
/// Get an environment variable, or if it is not set or empty, use the provided fallback function. Returns a string with trimmed whitespace.
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
use crate::dispatcher::Dispatcher;
|
||||
use crate::messages::prelude::*;
|
||||
pub use graphene_std::uuid::*;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
// TODO: serialize with serde to save the current editor state
|
||||
pub struct Editor {
|
||||
pub dispatcher: Dispatcher,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
/// Construct the editor.
|
||||
/// Remember to provide a random seed with `editor::set_uuid_seed(seed)` before any editors can be used.
|
||||
pub fn new() -> Self {
|
||||
pub fn new(environment: Environment, uuid_random_seed: u64) -> Self {
|
||||
ENVIRONMENT.set(environment).expect("Editor shoud only be initialized once");
|
||||
graphene_std::uuid::set_uuid_seed(uuid_random_seed);
|
||||
|
||||
Self { dispatcher: Dispatcher::new() }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new_local_executor() -> (Self, crate::node_graph_executor::NodeRuntime) {
|
||||
let _ = ENVIRONMENT.set(*Editor::environment());
|
||||
graphene_std::uuid::set_uuid_seed(0);
|
||||
|
||||
let (runtime, executor) = crate::node_graph_executor::NodeGraphExecutor::new_with_local_runtime();
|
||||
let dispatcher = Dispatcher::with_executor(executor);
|
||||
(Self { dispatcher }, runtime)
|
||||
let editor = Self {
|
||||
dispatcher: Dispatcher::with_executor(executor),
|
||||
};
|
||||
|
||||
(editor, runtime)
|
||||
}
|
||||
|
||||
pub fn handle_message<T: Into<Message>>(&mut self, message: T) -> Vec<FrontendMessage> {
|
||||
@@ -32,26 +39,68 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Editor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
static ENVIRONMENT: OnceLock<Environment> = OnceLock::new();
|
||||
impl Editor {
|
||||
#[cfg(not(test))]
|
||||
pub fn environment() -> &'static Environment {
|
||||
ENVIRONMENT.get().expect("Editor environment accessed before initialization")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn environment() -> &'static Environment {
|
||||
&Environment {
|
||||
platform: Platform::Desktop,
|
||||
host: Host::Linux,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Environment {
|
||||
pub platform: Platform,
|
||||
pub host: Host,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Platform {
|
||||
Desktop,
|
||||
Web,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Host {
|
||||
Windows,
|
||||
Mac,
|
||||
Linux,
|
||||
}
|
||||
impl Environment {
|
||||
pub fn is_desktop(&self) -> bool {
|
||||
matches!(self.platform, Platform::Desktop)
|
||||
}
|
||||
pub fn is_web(&self) -> bool {
|
||||
matches!(self.platform, Platform::Web)
|
||||
}
|
||||
pub fn is_windows(&self) -> bool {
|
||||
matches!(self.host, Host::Windows)
|
||||
}
|
||||
pub fn is_mac(&self) -> bool {
|
||||
matches!(self.host, Host::Mac)
|
||||
}
|
||||
pub fn is_linux(&self) -> bool {
|
||||
matches!(self.host, Host::Linux)
|
||||
}
|
||||
}
|
||||
|
||||
pub const GRAPHITE_RELEASE_SERIES: &str = env!("GRAPHITE_RELEASE_SERIES");
|
||||
pub const GRAPHITE_GIT_COMMIT_DATE: &str = env!("GRAPHITE_GIT_COMMIT_DATE");
|
||||
pub const GRAPHITE_GIT_COMMIT_BRANCH: Option<&str> = option_env!("GRAPHITE_GIT_COMMIT_BRANCH");
|
||||
pub const GRAPHITE_GIT_COMMIT_HASH: &str = env!("GRAPHITE_GIT_COMMIT_HASH");
|
||||
pub const GRAPHITE_GIT_COMMIT_BRANCH: &str = env!("GRAPHITE_GIT_COMMIT_BRANCH");
|
||||
pub const GRAPHITE_GIT_COMMIT_DATE: &str = env!("GRAPHITE_GIT_COMMIT_DATE");
|
||||
|
||||
pub fn commit_info_localized(localized_commit_date: &str) -> String {
|
||||
format!(
|
||||
"Release Series: {}\n\
|
||||
Branch: {}\n\
|
||||
Commit: {}\n\
|
||||
{}",
|
||||
GRAPHITE_RELEASE_SERIES,
|
||||
GRAPHITE_GIT_COMMIT_BRANCH,
|
||||
GRAPHITE_GIT_COMMIT_HASH.get(..8).unwrap_or(GRAPHITE_GIT_COMMIT_HASH),
|
||||
localized_commit_date
|
||||
)
|
||||
let mut info = String::new();
|
||||
info.push_str(&format!("Release Series: {GRAPHITE_RELEASE_SERIES}\n"));
|
||||
if let Some(branch) = GRAPHITE_GIT_COMMIT_BRANCH {
|
||||
info.push_str(&format!("Branch: {branch}\n"));
|
||||
}
|
||||
info.push_str(&format!("Commit: {}\n", GRAPHITE_GIT_COMMIT_HASH.get(..8).unwrap_or(GRAPHITE_GIT_COMMIT_HASH)));
|
||||
info.push_str(localized_commit_date);
|
||||
info
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ pub const DEFAULT_STROKE_WIDTH: f64 = 2.;
|
||||
pub const SELECTION_TOLERANCE: f64 = 5.;
|
||||
pub const DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD: f64 = 15.;
|
||||
pub const SELECTION_DRAG_ANGLE: f64 = 90.;
|
||||
pub const LAYER_ORIGIN_CROSS_DIAMETER: f64 = 10.;
|
||||
pub const LAYER_ORIGIN_CROSS_THICKNESS: f64 = 1.;
|
||||
|
||||
// PIVOT
|
||||
pub const PIVOT_CROSSHAIR_THICKNESS: f64 = 1.;
|
||||
@@ -109,6 +111,12 @@ pub const SEGMENT_OVERLAY_SIZE: f64 = 10.;
|
||||
pub const SEGMENT_SELECTED_THICKNESS: f64 = 3.;
|
||||
pub const HANDLE_LENGTH_FACTOR: f64 = 0.5;
|
||||
|
||||
// GRADIENT TOOL
|
||||
pub const GRADIENT_MIDPOINT_DIAMOND_RADIUS: f64 = 4.;
|
||||
pub const GRADIENT_MIDPOINT_MIN: f64 = 0.01;
|
||||
pub const GRADIENT_MIDPOINT_MAX: f64 = 0.99;
|
||||
pub const GRADIENT_STOP_MIN_VIEWPORT_GAP: f64 = 10.;
|
||||
|
||||
// PEN TOOL
|
||||
pub const CREATE_CURVE_THRESHOLD: f64 = 5.;
|
||||
|
||||
@@ -122,6 +130,9 @@ pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||
pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
|
||||
pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
|
||||
|
||||
// EYEDROPPER TOOL
|
||||
pub const EYEDROPPER_PREVIEW_AREA_RESOLUTION: u32 = 11;
|
||||
|
||||
// GIZMOS
|
||||
pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.;
|
||||
pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9;
|
||||
@@ -141,13 +152,19 @@ pub const SCALE_EFFECT: f64 = 0.5;
|
||||
// COLORS
|
||||
pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff";
|
||||
pub const COLOR_OVERLAY_BLUE_50: &str = "#00a8ff80";
|
||||
pub const COLOR_OVERLAY_BLUE_25: &str = "#00a8ff40";
|
||||
pub const COLOR_OVERLAY_BLUE_05: &str = "#00a8ff0d";
|
||||
pub const COLOR_OVERLAY_YELLOW: &str = "#ffc848";
|
||||
pub const COLOR_OVERLAY_YELLOW_DULL: &str = "#d7ba8b";
|
||||
pub const COLOR_OVERLAY_GREEN: &str = "#63ce63";
|
||||
pub const COLOR_OVERLAY_GREEN_25: &str = "#63ce6340";
|
||||
pub const COLOR_OVERLAY_RED: &str = "#ef5454";
|
||||
pub const COLOR_OVERLAY_RED_25: &str = "#ef545440";
|
||||
pub const COLOR_OVERLAY_GRAY: &str = "#cccccc";
|
||||
pub const COLOR_OVERLAY_GRAY_25: &str = "#cccccc40";
|
||||
pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
|
||||
pub const COLOR_OVERLAY_WHITE_05: &str = "#ffffff0d";
|
||||
pub const COLOR_OVERLAY_BLACK: &str = "#000000";
|
||||
pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf";
|
||||
|
||||
// DOCUMENT
|
||||
|
||||
@@ -23,12 +23,11 @@ pub struct DispatcherMessageHandlers {
|
||||
debug_message_handler: DebugMessageHandler,
|
||||
defer_message_handler: DeferMessageHandler,
|
||||
dialog_message_handler: DialogMessageHandler,
|
||||
globals_message_handler: GlobalsMessageHandler,
|
||||
input_preprocessor_message_handler: InputPreprocessorMessageHandler,
|
||||
key_mapping_message_handler: KeyMappingMessageHandler,
|
||||
layout_message_handler: LayoutMessageHandler,
|
||||
menu_bar_message_handler: MenuBarMessageHandler,
|
||||
pub portfolio_message_handler: PortfolioMessageHandler,
|
||||
pub(crate) portfolio_message_handler: PortfolioMessageHandler,
|
||||
preferences_message_handler: PreferencesMessageHandler,
|
||||
tool_message_handler: ToolMessageHandler,
|
||||
viewport_message_handler: ViewportMessageHandler,
|
||||
@@ -52,6 +51,7 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
|
||||
NodeGraphMessageDiscriminant::RunDocumentGraph,
|
||||
))),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitEyedropperPreviewRender),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontDataLoad),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateUIScale),
|
||||
];
|
||||
@@ -192,17 +192,11 @@ impl Dispatcher {
|
||||
self.responses.push(message);
|
||||
}
|
||||
}
|
||||
Message::Globals(message) => {
|
||||
self.message_handlers.globals_message_handler.process_message(message, &mut queue, ());
|
||||
}
|
||||
Message::InputPreprocessor(message) => {
|
||||
let keyboard_platform = GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout();
|
||||
|
||||
self.message_handlers.input_preprocessor_message_handler.process_message(
|
||||
message,
|
||||
&mut queue,
|
||||
InputPreprocessorMessageContext {
|
||||
keyboard_platform,
|
||||
viewport: &self.message_handlers.viewport_message_handler,
|
||||
},
|
||||
);
|
||||
@@ -239,6 +233,7 @@ impl Dispatcher {
|
||||
Message::MenuBar(message) => {
|
||||
let menu_bar_message_handler = &mut self.message_handlers.menu_bar_message_handler;
|
||||
|
||||
menu_bar_message_handler.focus_document = self.message_handlers.portfolio_message_handler.focus_document;
|
||||
menu_bar_message_handler.data_panel_open = self.message_handlers.portfolio_message_handler.data_panel_open;
|
||||
menu_bar_message_handler.layers_panel_open = self.message_handlers.portfolio_message_handler.layers_panel_open;
|
||||
menu_bar_message_handler.properties_panel_open = self.message_handlers.portfolio_message_handler.properties_panel_open;
|
||||
@@ -364,10 +359,15 @@ impl Dispatcher {
|
||||
/// with a discriminant or the entire payload (depending on settings)
|
||||
fn log_message(&self, message: &Message, queues: &[VecDeque<Message>], message_logging_verbosity: MessageLoggingVerbosity) {
|
||||
let discriminant = MessageDiscriminant::from(message);
|
||||
let is_blocked = DEBUG_MESSAGE_BLOCK_LIST.contains(&discriminant) || DEBUG_MESSAGE_ENDING_BLOCK_LIST.iter().any(|blocked_name| discriminant.local_name().ends_with(blocked_name));
|
||||
let is_empty_batched = if let Message::Batched { messages } = message { messages.is_empty() } else { false };
|
||||
let is_blocked =
|
||||
|discriminant| DEBUG_MESSAGE_BLOCK_LIST.contains(&discriminant) || DEBUG_MESSAGE_ENDING_BLOCK_LIST.iter().any(|blocked_name| discriminant.local_name().ends_with(blocked_name));
|
||||
let is_batch_all_blocked = if let Message::Batched { messages } = message {
|
||||
messages.iter().all(|message| is_blocked(MessageDiscriminant::from(message)))
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if !is_blocked && !is_empty_batched {
|
||||
if !is_blocked(discriminant) && !is_batch_all_blocked {
|
||||
match message_logging_verbosity {
|
||||
MessageLoggingVerbosity::Off => {}
|
||||
MessageLoggingVerbosity::Names => {
|
||||
@@ -578,10 +578,9 @@ mod test {
|
||||
"Demo artwork '{document_name}' has more than 1 line (remember to open and re-save it in Graphite)",
|
||||
);
|
||||
|
||||
let responses = editor.editor.handle_message(PortfolioMessage::OpenDocumentFile {
|
||||
document_name: Some(document_name.to_string()),
|
||||
document_path: None,
|
||||
document_serialized_content,
|
||||
let responses = editor.editor.handle_message(PortfolioMessage::OpenFile {
|
||||
path: file_name.into(),
|
||||
content: document_serialized_content.bytes().collect(),
|
||||
});
|
||||
|
||||
// Check if the graph renders
|
||||
@@ -591,10 +590,14 @@ mod test {
|
||||
|
||||
for response in responses {
|
||||
// Check for the existence of the file format incompatibility warning dialog after opening the test file
|
||||
if let FrontendMessage::UpdateDialogColumn1 { diff } = response {
|
||||
if let FrontendMessage::UpdateLayout {
|
||||
layout_target: LayoutTarget::DialogColumn1,
|
||||
diff,
|
||||
} = response
|
||||
{
|
||||
if let DiffUpdate::Layout(sub_layout) = &diff[0].new_value {
|
||||
if let LayoutGroup::Row { widgets } = &sub_layout.0[0] {
|
||||
if let Widget::TextLabel(TextLabel { value, .. }) = &widgets[0].widget {
|
||||
if let Widget::TextLabel(TextLabel { value, .. }) = &*widgets[0].widget {
|
||||
print_problem_to_terminal_on_failure(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use super::app_window_message_handler::AppWindowPlatform;
|
||||
|
||||
#[impl_message(Message, AppWindow)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum AppWindowMessage {
|
||||
UpdatePlatform { platform: AppWindowPlatform },
|
||||
PointerLock,
|
||||
PointerLockMove { x: f64, y: f64 },
|
||||
Restart,
|
||||
Close,
|
||||
Minimize,
|
||||
Maximize,
|
||||
Fullscreen,
|
||||
Drag,
|
||||
Hide,
|
||||
HideOthers,
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use crate::messages::app_window::AppWindowMessage;
|
||||
use crate::application::{Environment, Platform};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::{application::Host, messages::app_window::AppWindowMessage};
|
||||
use graphite_proc_macros::{ExtractField, message_handler_data};
|
||||
|
||||
#[derive(Debug, Clone, Default, ExtractField)]
|
||||
pub struct AppWindowMessageHandler {
|
||||
platform: AppWindowPlatform,
|
||||
}
|
||||
pub struct AppWindowMessageHandler {}
|
||||
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
|
||||
fn process_message(&mut self, message: AppWindowMessage, responses: &mut std::collections::VecDeque<Message>, _: ()) {
|
||||
match message {
|
||||
AppWindowMessage::UpdatePlatform { platform } => {
|
||||
self.platform = platform;
|
||||
responses.add(FrontendMessage::UpdatePlatform { platform: self.platform });
|
||||
AppWindowMessage::PointerLock => {
|
||||
responses.add(FrontendMessage::WindowPointerLock);
|
||||
}
|
||||
AppWindowMessage::PointerLockMove { x, y } => {
|
||||
responses.add(FrontendMessage::WindowPointerLockMove { x, y });
|
||||
}
|
||||
AppWindowMessage::Close => {
|
||||
responses.add(FrontendMessage::WindowClose);
|
||||
@@ -24,6 +25,9 @@ impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
|
||||
AppWindowMessage::Maximize => {
|
||||
responses.add(FrontendMessage::WindowMaximize);
|
||||
}
|
||||
AppWindowMessage::Fullscreen => {
|
||||
responses.add(FrontendMessage::WindowFullscreen);
|
||||
}
|
||||
AppWindowMessage::Drag => {
|
||||
responses.add(FrontendMessage::WindowDrag);
|
||||
}
|
||||
@@ -36,12 +40,17 @@ impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
|
||||
AppWindowMessage::ShowAll => {
|
||||
responses.add(FrontendMessage::WindowShowAll);
|
||||
}
|
||||
AppWindowMessage::Restart => {
|
||||
responses.add(PortfolioMessage::AutoSaveAllDocuments);
|
||||
responses.add(FrontendMessage::WindowRestart);
|
||||
}
|
||||
}
|
||||
}
|
||||
advertise_actions!(AppWindowMessageDiscriminant;
|
||||
Close,
|
||||
Minimize,
|
||||
Maximize,
|
||||
Fullscreen,
|
||||
Drag,
|
||||
Hide,
|
||||
HideOthers,
|
||||
@@ -56,3 +65,14 @@ pub enum AppWindowPlatform {
|
||||
Mac,
|
||||
Linux,
|
||||
}
|
||||
|
||||
impl From<&Environment> for AppWindowPlatform {
|
||||
fn from(environment: &Environment) -> Self {
|
||||
match (environment.platform, environment.host) {
|
||||
(Platform::Web, _) => AppWindowPlatform::Web,
|
||||
(Platform::Desktop, Host::Linux) => AppWindowPlatform::Linux,
|
||||
(Platform::Desktop, Host::Mac) => AppWindowPlatform::Mac,
|
||||
(Platform::Desktop, Host::Windows) => AppWindowPlatform::Windows,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,12 @@ pub enum DialogMessage {
|
||||
PreferencesDialog(PreferencesDialogMessage),
|
||||
|
||||
// Messages
|
||||
CloseAllDocumentsWithConfirmation,
|
||||
CloseDialogAndThen {
|
||||
Dismiss,
|
||||
Close,
|
||||
CloseAndThen {
|
||||
followups: Vec<Message>,
|
||||
},
|
||||
CloseAllDocumentsWithConfirmation,
|
||||
DisplayDialogError {
|
||||
title: String,
|
||||
description: String,
|
||||
@@ -35,4 +37,5 @@ pub enum DialogMessage {
|
||||
},
|
||||
RequestNewDocumentDialog,
|
||||
RequestPreferencesDialog,
|
||||
RequestConfirmRestartDialog,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::simple_dialogs::{self, AboutGraphiteDialog, DemoArtworkDialog, LicensesDialog};
|
||||
use crate::messages::dialog::simple_dialogs::LicensesThirdPartyDialog;
|
||||
use crate::application::GRAPHITE_GIT_COMMIT_DATE;
|
||||
use crate::messages::dialog::simple_dialogs::{ConfirmRestartDialog, LicensesThirdPartyDialog};
|
||||
use crate::messages::frontend::utility_types::ExportBounds;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
@@ -12,6 +14,7 @@ pub struct DialogMessageContext<'a> {
|
||||
/// Stores the dialogs which require state. These are the ones that have their own message handlers, and are not the ones defined in `simple_dialogs`.
|
||||
#[derive(Debug, Default, Clone, ExtractField)]
|
||||
pub struct DialogMessageHandler {
|
||||
on_dismiss: Option<Message>,
|
||||
export_dialog: ExportDialogMessageHandler,
|
||||
new_document_dialog: NewDocumentDialogMessageHandler,
|
||||
preferences_dialog: PreferencesDialogMessageHandler,
|
||||
@@ -27,34 +30,47 @@ impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHa
|
||||
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, responses, ()),
|
||||
DialogMessage::PreferencesDialog(message) => self.preferences_dialog.process_message(message, responses, PreferencesDialogMessageContext { preferences }),
|
||||
|
||||
DialogMessage::CloseAllDocumentsWithConfirmation => {
|
||||
let dialog = simple_dialogs::CloseAllDocumentsDialog {
|
||||
unsaved_document_names: portfolio.unsaved_document_names(),
|
||||
};
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
DialogMessage::Dismiss => {
|
||||
if let Some(message) = self.on_dismiss.take() {
|
||||
responses.add(message);
|
||||
}
|
||||
}
|
||||
DialogMessage::CloseDialogAndThen { followups } => {
|
||||
DialogMessage::Close => {
|
||||
self.on_dismiss = None;
|
||||
responses.add(FrontendMessage::DialogClose)
|
||||
}
|
||||
DialogMessage::CloseAndThen { followups } => {
|
||||
for message in followups.into_iter() {
|
||||
responses.add(message);
|
||||
}
|
||||
|
||||
// This come after followups, so that the followups (which can cause the dialog to open) happen first, then we close it afterwards.
|
||||
// If it comes before, the dialog reopens (and appears to not close at all).
|
||||
responses.add(FrontendMessage::DisplayDialogDismiss);
|
||||
responses.add(DialogMessage::Close);
|
||||
}
|
||||
DialogMessage::CloseAllDocumentsWithConfirmation => {
|
||||
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||
let dialog = simple_dialogs::CloseAllDocumentsDialog {
|
||||
unsaved_document_names: portfolio.unsaved_document_names(),
|
||||
};
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::DisplayDialogError { title, description } => {
|
||||
self.on_dismiss = None;
|
||||
let dialog = simple_dialogs::ErrorDialog { title, description };
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestAboutGraphiteDialog => {
|
||||
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||
responses.add(FrontendMessage::TriggerAboutGraphiteLocalizedCommitDate {
|
||||
commit_date: env!("GRAPHITE_GIT_COMMIT_DATE").into(),
|
||||
commit_date: GRAPHITE_GIT_COMMIT_DATE.into(),
|
||||
});
|
||||
}
|
||||
DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate {
|
||||
localized_commit_date,
|
||||
localized_commit_year,
|
||||
} => {
|
||||
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||
let dialog = AboutGraphiteDialog {
|
||||
localized_commit_date,
|
||||
localized_commit_year,
|
||||
@@ -63,10 +79,12 @@ impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHa
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestDemoArtworkDialog => {
|
||||
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||
let dialog = DemoArtworkDialog;
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestExportDialog => {
|
||||
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||
if let Some(document) = portfolio.active_document() {
|
||||
let artboards = document
|
||||
.metadata()
|
||||
@@ -84,20 +102,29 @@ impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHa
|
||||
.collect();
|
||||
|
||||
self.export_dialog.artboards = artboards;
|
||||
|
||||
if let ExportBounds::Artboard(layer) = self.export_dialog.bounds
|
||||
&& !self.export_dialog.artboards.contains_key(&layer)
|
||||
{
|
||||
self.export_dialog.bounds = ExportBounds::AllArtwork;
|
||||
}
|
||||
|
||||
self.export_dialog.has_selection = document.network_interface.selected_nodes().selected_layers(document.metadata()).next().is_some();
|
||||
self.export_dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
}
|
||||
DialogMessage::RequestLicensesDialogWithLocalizedCommitDate { localized_commit_year } => {
|
||||
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||
let dialog = LicensesDialog { localized_commit_year };
|
||||
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestLicensesThirdPartyDialogWithLicenseText { license_text } => {
|
||||
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||
let dialog = LicensesThirdPartyDialog { license_text };
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestNewDocumentDialog => {
|
||||
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||
self.new_document_dialog = NewDocumentDialogMessageHandler {
|
||||
name: portfolio.generate_new_document_name(),
|
||||
infinite: false,
|
||||
@@ -106,9 +133,16 @@ impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHa
|
||||
self.new_document_dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestPreferencesDialog => {
|
||||
self.preferences_dialog = PreferencesDialogMessageHandler {};
|
||||
self.on_dismiss = Some(PreferencesDialogMessage::Confirm.into());
|
||||
self.preferences_dialog.send_dialog_to_frontend(responses, preferences);
|
||||
}
|
||||
DialogMessage::RequestConfirmRestartDialog => {
|
||||
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||
let dialog = ConfirmRestartDialog {
|
||||
changed_settings: vec!["Disable UI Acceleration".into()],
|
||||
};
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,19 +43,28 @@ impl MessageHandler<ExportDialogMessage, ExportDialogMessageContext<'_>> for Exp
|
||||
ExportDialogMessage::TransparentBackground { transparent } => self.transparent_background = transparent,
|
||||
ExportDialogMessage::ExportBounds { bounds } => self.bounds = bounds,
|
||||
|
||||
ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport {
|
||||
name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
|
||||
file_type: self.file_type,
|
||||
scale_factor: self.scale_factor,
|
||||
bounds: self.bounds,
|
||||
transparent_background: self.file_type != FileType::Jpg && self.transparent_background,
|
||||
}),
|
||||
ExportDialogMessage::Submit => {
|
||||
let artboard_name = match self.bounds {
|
||||
ExportBounds::Artboard(layer) => self.artboards.get(&layer).cloned(),
|
||||
_ => None,
|
||||
};
|
||||
responses.add_front(PortfolioMessage::SubmitDocumentExport {
|
||||
name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
|
||||
file_type: self.file_type,
|
||||
scale_factor: self.scale_factor,
|
||||
bounds: self.bounds,
|
||||
transparent_background: self.file_type != FileType::Jpg && self.transparent_background,
|
||||
artboard_name,
|
||||
artboard_count: self.artboards.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
self.send_dialog_to_frontend(responses);
|
||||
}
|
||||
|
||||
advertise_actions! {ExportDialogUpdate;}
|
||||
advertise_actions!(ExportDialogUpdate;
|
||||
);
|
||||
}
|
||||
|
||||
impl DialogLayoutHolder for ExportDialogMessageHandler {
|
||||
@@ -67,13 +76,13 @@ impl DialogLayoutHolder for ExportDialogMessageHandler {
|
||||
TextButton::new("Export")
|
||||
.emphasized(true)
|
||||
.on_update(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
DialogMessage::CloseAndThen {
|
||||
followups: vec![ExportDialogMessage::Submit.into()],
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance(),
|
||||
TextButton::new("Cancel").on_update(|_| FrontendMessage::DialogClose.into()).widget_instance(),
|
||||
];
|
||||
|
||||
Layout(vec![LayoutGroup::Row { widgets }])
|
||||
@@ -92,14 +101,14 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
||||
.collect();
|
||||
|
||||
let export_type = vec![
|
||||
TextLabel::new("File Type").table_align(true).min_width("100px").widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
TextLabel::new("File Type").table_align(true).min_width(100).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
RadioInput::new(entries).selected_index(Some(self.file_type as u32)).widget_instance(),
|
||||
];
|
||||
|
||||
let resolution = vec![
|
||||
TextLabel::new("Scale Factor").table_align(true).min_width("100px").widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
TextLabel::new("Scale Factor").table_align(true).min_width(100).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
NumberInput::new(Some(self.scale_factor))
|
||||
.unit("")
|
||||
.min(0.)
|
||||
@@ -122,7 +131,7 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
||||
} else {
|
||||
self.bounds
|
||||
};
|
||||
let index = choices.iter().flatten().position(|(bounds, _, _)| *bounds == current_bounds).unwrap();
|
||||
let index = choices.iter().flatten().position(|(bounds, _, _)| *bounds == current_bounds).unwrap_or(0);
|
||||
|
||||
let mut entries = choices
|
||||
.into_iter()
|
||||
@@ -144,15 +153,15 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
||||
}
|
||||
|
||||
let export_area = vec![
|
||||
TextLabel::new("Bounds").table_align(true).min_width("100px").widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
TextLabel::new("Bounds").table_align(true).min_width(100).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_instance(),
|
||||
];
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let transparent_background = vec![
|
||||
TextLabel::new("Transparency").table_align(true).min_width("100px").for_checkbox(checkbox_id).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
TextLabel::new("Transparency").table_align(true).min_width(100).for_checkbox(checkbox_id).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(self.transparent_background)
|
||||
.disabled(self.file_type == FileType::Jpg)
|
||||
.on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground { transparent: value.checked }.into())
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct NewDocumentDialogMessageHandler {
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
impl<'a> MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHandler {
|
||||
impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHandler {
|
||||
fn process_message(&mut self, message: NewDocumentDialogMessage, responses: &mut VecDeque<Message>, _: ()) {
|
||||
match message {
|
||||
NewDocumentDialogMessage::Name { name } => self.name = name,
|
||||
@@ -34,7 +34,11 @@ impl<'a> MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessa
|
||||
responses.add(ViewportMessage::RepropagateUpdate);
|
||||
|
||||
responses.add(DeferMessage::AfterNavigationReady {
|
||||
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into(), DocumentMessage::DeselectAllLayers.into()],
|
||||
messages: vec![
|
||||
DocumentMessage::ZoomCanvasToFitAll.into(),
|
||||
DocumentMessage::DeselectAllLayers.into(),
|
||||
PortfolioMessage::AutoSaveActiveDocument.into(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,7 +49,8 @@ impl<'a> MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessa
|
||||
self.send_dialog_to_frontend(responses);
|
||||
}
|
||||
|
||||
advertise_actions! {NewDocumentDialogUpdate;}
|
||||
advertise_actions!(NewDocumentDialogUpdate;
|
||||
);
|
||||
}
|
||||
|
||||
impl DialogLayoutHolder for NewDocumentDialogMessageHandler {
|
||||
@@ -57,13 +62,13 @@ impl DialogLayoutHolder for NewDocumentDialogMessageHandler {
|
||||
TextButton::new("OK")
|
||||
.emphasized(true)
|
||||
.on_update(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
DialogMessage::CloseAndThen {
|
||||
followups: vec![NewDocumentDialogMessage::Submit.into()],
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance(),
|
||||
TextButton::new("Cancel").on_update(|_| FrontendMessage::DialogClose.into()).widget_instance(),
|
||||
];
|
||||
|
||||
Layout(vec![LayoutGroup::Row { widgets }])
|
||||
@@ -73,8 +78,8 @@ impl DialogLayoutHolder for NewDocumentDialogMessageHandler {
|
||||
impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||
fn layout(&self) -> Layout {
|
||||
let name = vec![
|
||||
TextLabel::new("Name").table_align(true).min_width("90px").widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
TextLabel::new("Name").table_align(true).min_width(90).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
TextInput::new(&self.name)
|
||||
.on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name { name: text_input.value.clone() }.into())
|
||||
.min_width(204) // Matches the 100px of both NumberInputs below + the 4px of the Unrelated-type separator
|
||||
@@ -83,8 +88,8 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let infinite = vec![
|
||||
TextLabel::new("Infinite Canvas").table_align(true).min_width("90px").for_checkbox(checkbox_id).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
TextLabel::new("Infinite Canvas").table_align(true).min_width(90).for_checkbox(checkbox_id).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(self.infinite)
|
||||
.on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite { infinite: checkbox_input.checked }.into())
|
||||
.for_label(checkbox_id)
|
||||
@@ -92,8 +97,8 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||
];
|
||||
|
||||
let scale = vec![
|
||||
TextLabel::new("Dimensions").table_align(true).min_width("90px").widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
TextLabel::new("Dimensions").table_align(true).min_width(90).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
NumberInput::new(Some(self.dimensions.x as f64))
|
||||
.label("W")
|
||||
.unit(" px")
|
||||
@@ -104,7 +109,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||
.min_width(100)
|
||||
.on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsX { width: number_input.value.unwrap() }.into())
|
||||
.widget_instance(),
|
||||
Separator::new(SeparatorType::Related).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
NumberInput::new(Some(self.dimensions.y as f64))
|
||||
.label("H")
|
||||
.unit(" px")
|
||||
|
||||
@@ -3,5 +3,7 @@ use crate::messages::prelude::*;
|
||||
#[impl_message(Message, DialogMessage, PreferencesDialog)]
|
||||
#[derive(Eq, PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum PreferencesDialogMessage {
|
||||
MayRequireRestart,
|
||||
Confirm,
|
||||
Update,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle;
|
||||
use crate::messages::preferences::SelectionMode;
|
||||
use crate::messages::prelude::*;
|
||||
use graphene_std::render_node::{EditorPreferences, wgpu_available};
|
||||
|
||||
#[derive(ExtractField)]
|
||||
pub struct PreferencesDialogMessageContext<'a> {
|
||||
@@ -11,21 +12,35 @@ pub struct PreferencesDialogMessageContext<'a> {
|
||||
|
||||
/// A dialog to allow users to customize Graphite editor options
|
||||
#[derive(Debug, Clone, Default, ExtractField)]
|
||||
pub struct PreferencesDialogMessageHandler {}
|
||||
pub struct PreferencesDialogMessageHandler {
|
||||
unmodified_preferences: Option<PreferencesMessageHandler>,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<PreferencesDialogMessage, PreferencesDialogMessageContext<'_>> for PreferencesDialogMessageHandler {
|
||||
fn process_message(&mut self, message: PreferencesDialogMessage, responses: &mut VecDeque<Message>, context: PreferencesDialogMessageContext) {
|
||||
let PreferencesDialogMessageContext { preferences } = context;
|
||||
|
||||
match message {
|
||||
PreferencesDialogMessage::Confirm => {}
|
||||
PreferencesDialogMessage::MayRequireRestart => {
|
||||
if self.unmodified_preferences.is_none() {
|
||||
self.unmodified_preferences = Some(preferences.clone());
|
||||
}
|
||||
}
|
||||
PreferencesDialogMessage::Confirm => {
|
||||
if let Some(unmodified_preferences) = &self.unmodified_preferences
|
||||
&& unmodified_preferences.needs_restart(preferences)
|
||||
{
|
||||
responses.add(DialogMessage::RequestConfirmRestartDialog);
|
||||
} else {
|
||||
responses.add(DialogMessage::Close);
|
||||
}
|
||||
}
|
||||
PreferencesDialogMessage::Update => {}
|
||||
}
|
||||
|
||||
self.send_dialog_to_frontend(responses, preferences);
|
||||
}
|
||||
|
||||
advertise_actions! {PreferencesDialogUpdate;}
|
||||
advertise_actions!(PreferencesDialogUpdate;
|
||||
);
|
||||
}
|
||||
|
||||
// This doesn't actually implement the `DialogLayoutHolder` trait like the other dialog message handlers.
|
||||
@@ -44,15 +59,20 @@ impl PreferencesDialogMessageHandler {
|
||||
{
|
||||
let header = vec![TextLabel::new("Navigation").italic(true).widget_instance()];
|
||||
|
||||
let zoom_rate_description = "Adjust how fast zooming occurs when using the scroll wheel or pinch gesture (relative to a default of 50).";
|
||||
let zoom_rate_description = "
|
||||
Adjust how fast zooming occurs when using the scroll wheel or pinch gesture.\n\
|
||||
\n\
|
||||
*Default: 50.*
|
||||
"
|
||||
.trim();
|
||||
let zoom_rate_label = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
TextLabel::new("Zoom Rate").tooltip_label("Zoom Rate").tooltip_description(zoom_rate_description).widget_instance(),
|
||||
];
|
||||
let zoom_rate = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
NumberInput::new(Some(map_zoom_rate_to_display(preferences.viewport_zoom_wheel_rate)))
|
||||
.tooltip_label("Zoom Rate")
|
||||
.tooltip_description(zoom_rate_description)
|
||||
@@ -72,10 +92,15 @@ impl PreferencesDialogMessageHandler {
|
||||
];
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let zoom_with_scroll_description = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads).";
|
||||
let zoom_with_scroll_description = "
|
||||
Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads).\n\
|
||||
\n\
|
||||
*Default: Off.*
|
||||
"
|
||||
.trim();
|
||||
let zoom_with_scroll = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(preferences.zoom_with_scroll)
|
||||
.tooltip_label("Zoom with Scroll")
|
||||
.tooltip_description(zoom_with_scroll_description)
|
||||
@@ -103,12 +128,18 @@ impl PreferencesDialogMessageHandler {
|
||||
{
|
||||
let header = vec![TextLabel::new("Editing").italic(true).widget_instance()];
|
||||
|
||||
let selection_label_description = "
|
||||
Choose how targets are selected within dragged rectangular and lasso areas.\n\
|
||||
\n\
|
||||
*Default: Touched.*
|
||||
"
|
||||
.trim();
|
||||
let selection_label = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
TextLabel::new("Selection")
|
||||
.tooltip_label("Selection")
|
||||
.tooltip_description("Choose how targets are selected within dragged rectangular and lasso areas.")
|
||||
.tooltip_description(selection_label_description)
|
||||
.widget_instance(),
|
||||
];
|
||||
|
||||
@@ -147,8 +178,8 @@ impl PreferencesDialogMessageHandler {
|
||||
.selected_index(Some(preferences.selection_mode as u32))
|
||||
.widget_instance();
|
||||
let selection_mode = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
selection_mode,
|
||||
];
|
||||
|
||||
@@ -162,15 +193,20 @@ impl PreferencesDialogMessageHandler {
|
||||
{
|
||||
let header = vec![TextLabel::new("Interface").italic(true).widget_instance()];
|
||||
|
||||
let scale_description = "Adjust the scale of the entire user interface (100% is default).";
|
||||
let scale_description = "
|
||||
Adjust the scale of the entire user interface.\n\
|
||||
\n\
|
||||
*Default: 100%.*
|
||||
"
|
||||
.trim();
|
||||
let scale_label = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
TextLabel::new("Scale").tooltip_label("Scale").tooltip_description(scale_description).widget_instance(),
|
||||
];
|
||||
let scale = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
NumberInput::new(Some(ui_scale_to_display(preferences.ui_scale)))
|
||||
.tooltip_label("Scale")
|
||||
.tooltip_description(scale_description)
|
||||
@@ -202,10 +238,15 @@ impl PreferencesDialogMessageHandler {
|
||||
{
|
||||
let header = vec![TextLabel::new("Experimental").italic(true).widget_instance()];
|
||||
|
||||
let node_graph_section_description = "Configure the appearance of the wires running between node connections in the graph.";
|
||||
let node_graph_section_description = "
|
||||
Configure the appearance of the wires running between node connections in the graph.\n\
|
||||
\n\
|
||||
*Default: Direct.*
|
||||
"
|
||||
.trim();
|
||||
let node_graph_wires_label = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
TextLabel::new("Node Graph Wires")
|
||||
.tooltip_label("Node Graph Wires")
|
||||
.tooltip_description(node_graph_section_description)
|
||||
@@ -226,48 +267,25 @@ impl PreferencesDialogMessageHandler {
|
||||
.selected_index(Some(preferences.graph_wire_style as u32))
|
||||
.widget_instance();
|
||||
let graph_wire_style = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
graph_wire_style,
|
||||
];
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let vello_description = "Use the experimental Vello renderer instead of SVG-based rendering.".to_string();
|
||||
#[cfg(target_family = "wasm")]
|
||||
let mut vello_description = vello_description;
|
||||
#[cfg(target_family = "wasm")]
|
||||
vello_description.push_str("\n\n(Your browser must support WebGPU.)");
|
||||
|
||||
let use_vello = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(preferences.use_vello && preferences.supports_wgpu())
|
||||
.tooltip_label("Vello Renderer")
|
||||
.tooltip_description(vello_description.clone())
|
||||
.disabled(!preferences.supports_wgpu())
|
||||
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into())
|
||||
.for_label(checkbox_id)
|
||||
.widget_instance(),
|
||||
TextLabel::new("Vello Renderer")
|
||||
.tooltip_label("Vello Renderer")
|
||||
.tooltip_description(vello_description)
|
||||
.disabled(!preferences.supports_wgpu())
|
||||
.for_checkbox(checkbox_id)
|
||||
.widget_instance(),
|
||||
];
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let brush_tool_description = "
|
||||
Enable the Brush tool to support basic raster-based layer painting.\n\
|
||||
\n\
|
||||
This legacy experimental tool has performance and quality limitations and is slated for replacement in future versions of Graphite that will focus on raster graphics editing.\n\
|
||||
\n\
|
||||
Content created with the Brush tool may not be compatible with future versions of Graphite.
|
||||
"
|
||||
Enable the Brush tool to support basic raster-based layer painting.\n\
|
||||
\n\
|
||||
This legacy experimental tool has performance and quality limitations and is slated for replacement in future versions of Graphite that will have a renewed focus on raster graphics editing.\n\
|
||||
\n\
|
||||
Content created with the Brush tool may not be compatible with future versions of Graphite.\n\
|
||||
\n\
|
||||
*Default: Off.*
|
||||
"
|
||||
.trim();
|
||||
let brush_tool = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(preferences.brush_tool)
|
||||
.tooltip_label("Brush Tool")
|
||||
.tooltip_description(brush_tool_description)
|
||||
@@ -281,7 +299,94 @@ impl PreferencesDialogMessageHandler {
|
||||
.widget_instance(),
|
||||
];
|
||||
|
||||
rows.extend_from_slice(&[header, node_graph_wires_label, graph_wire_style, use_vello, brush_tool]);
|
||||
rows.extend_from_slice(&[header, node_graph_wires_label, graph_wire_style, brush_tool]);
|
||||
}
|
||||
|
||||
// =============
|
||||
// COMPATIBILITY
|
||||
// =============
|
||||
{
|
||||
let wgpu_available = wgpu_available().unwrap_or(false);
|
||||
let is_desktop = cfg!(not(target_family = "wasm"));
|
||||
if wgpu_available || is_desktop {
|
||||
let header = vec![TextLabel::new("Compatibility").italic(true).widget_instance()];
|
||||
rows.push(header);
|
||||
}
|
||||
|
||||
if wgpu_available {
|
||||
let render_tile_resolution_description = "
|
||||
Maximum X or Y resolution per render tile. Larger tiles may improve performance but can cause flickering or missing content in complex artwork if set too high.\n\
|
||||
\n\
|
||||
*Default: 1280 px.*
|
||||
"
|
||||
.trim();
|
||||
let render_tile_resolution_label = vec![
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
TextLabel::new("Render Tile Resolution")
|
||||
.tooltip_label("Render Tile Resolution")
|
||||
.tooltip_description(render_tile_resolution_description)
|
||||
.widget_instance(),
|
||||
];
|
||||
let render_tile_resolution = vec![
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
NumberInput::new(Some(preferences.max_render_region_size as f64))
|
||||
.tooltip_label("Render Tile Resolution")
|
||||
.tooltip_description(render_tile_resolution_description)
|
||||
.mode_range()
|
||||
.int()
|
||||
.min(256.)
|
||||
.max(4096.)
|
||||
.increment_step(256.)
|
||||
.unit(" px")
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
let size = number_input.value.unwrap_or(EditorPreferences::default().max_render_region_size as f64) as u32;
|
||||
PreferencesMessage::MaxRenderRegionSize { size }.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
];
|
||||
|
||||
rows.extend_from_slice(&[render_tile_resolution_label, render_tile_resolution]);
|
||||
}
|
||||
|
||||
if is_desktop {
|
||||
let ui_acceleration_description = "
|
||||
Use the CPU to draw the Graphite user interface (areas outside of the canvas) instead of the GPU. This does not affect the rendering of artwork in the canvas, which remains hardware accelerated.\n\
|
||||
\n\
|
||||
Disabling UI acceleration may slightly degrade performance, so this should be used as a workaround only if issues are observed with displaying the UI. This setting may become enabled automatically if Graphite launches, detects that it cannot draw the UI normally, and restarts in compatibility mode.\n\
|
||||
\n\
|
||||
*Default: Off.*
|
||||
"
|
||||
.trim();
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let ui_acceleration = vec![
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
CheckboxInput::new(preferences.disable_ui_acceleration)
|
||||
.tooltip_label("Disable UI Acceleration")
|
||||
.tooltip_description(ui_acceleration_description)
|
||||
.on_update(|number_input: &CheckboxInput| Message::Batched {
|
||||
messages: Box::new([
|
||||
PreferencesDialogMessage::MayRequireRestart.into(),
|
||||
PreferencesMessage::DisableUIAcceleration {
|
||||
disable_ui_acceleration: number_input.checked,
|
||||
}
|
||||
.into(),
|
||||
]),
|
||||
})
|
||||
.for_label(checkbox_id)
|
||||
.widget_instance(),
|
||||
TextLabel::new("Disable UI Acceleration")
|
||||
.tooltip_label("Disable UI Acceleration")
|
||||
.tooltip_description(ui_acceleration_description)
|
||||
.for_checkbox(checkbox_id)
|
||||
.widget_instance(),
|
||||
];
|
||||
|
||||
rows.push(ui_acceleration);
|
||||
}
|
||||
}
|
||||
|
||||
Layout(rows.into_iter().map(|r| LayoutGroup::Row { widgets: r }).collect())
|
||||
@@ -307,15 +412,7 @@ impl PreferencesDialogMessageHandler {
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![
|
||||
TextButton::new("OK")
|
||||
.emphasized(true)
|
||||
.on_update(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followups: vec![PreferencesDialogMessage::Confirm.into()],
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
TextButton::new("OK").emphasized(true).on_update(|_| PreferencesDialogMessage::Confirm.into()).widget_instance(),
|
||||
TextButton::new("Reset to Defaults").on_update(|_| PreferencesMessage::ResetToDefaults.into()).widget_instance(),
|
||||
];
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ impl DialogLayoutHolder for AboutGraphiteDialog {
|
||||
const TITLE: &'static str = "About Graphite";
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance()];
|
||||
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()];
|
||||
|
||||
Layout(vec![LayoutGroup::Row { widgets }])
|
||||
}
|
||||
|
||||
@@ -15,13 +15,13 @@ impl DialogLayoutHolder for CloseAllDocumentsDialog {
|
||||
TextButton::new("Discard All")
|
||||
.emphasized(true)
|
||||
.on_update(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
DialogMessage::CloseAndThen {
|
||||
followups: vec![PortfolioMessage::CloseAllDocuments.into()],
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance(),
|
||||
TextButton::new("Cancel").on_update(|_| FrontendMessage::DialogClose.into()).widget_instance(),
|
||||
];
|
||||
|
||||
Layout(vec![LayoutGroup::Row { widgets }])
|
||||
|
||||
@@ -18,7 +18,7 @@ impl DialogLayoutHolder for CloseDocumentDialog {
|
||||
TextButton::new("Save")
|
||||
.emphasized(true)
|
||||
.on_update(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
DialogMessage::CloseAndThen {
|
||||
followups: vec![DocumentMessage::SaveDocument.into()],
|
||||
}
|
||||
.into()
|
||||
@@ -26,13 +26,13 @@ impl DialogLayoutHolder for CloseDocumentDialog {
|
||||
.widget_instance(),
|
||||
TextButton::new("Discard")
|
||||
.on_update(move |_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
DialogMessage::CloseAndThen {
|
||||
followups: vec![EventMessage::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()],
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
TextButton::new("Cancel").on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance(),
|
||||
TextButton::new("Cancel").on_update(|_| FrontendMessage::DialogClose.into()).widget_instance(),
|
||||
];
|
||||
|
||||
Layout(vec![LayoutGroup::Row { widgets }])
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
/// A dialog for confirming the restart of the application when changing a preference that requires a restart to take effect.
|
||||
pub struct ConfirmRestartDialog {
|
||||
pub changed_settings: Vec<String>,
|
||||
}
|
||||
|
||||
impl DialogLayoutHolder for ConfirmRestartDialog {
|
||||
const ICON: &'static str = "Warning";
|
||||
const TITLE: &'static str = "Restart Required";
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![
|
||||
TextButton::new("Restart Now")
|
||||
.emphasized(true)
|
||||
.on_update(|_| {
|
||||
DialogMessage::CloseAndThen {
|
||||
followups: vec![AppWindowMessage::Restart.into()],
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
TextButton::new("Later").on_update(|_| FrontendMessage::DialogClose.into()).widget_instance(),
|
||||
];
|
||||
|
||||
Layout(vec![LayoutGroup::Row { widgets }])
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutHolder for ConfirmRestartDialog {
|
||||
fn layout(&self) -> Layout {
|
||||
let changed_settings = "• ".to_string() + &self.changed_settings.join("\n• ");
|
||||
|
||||
Layout(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Restart to apply changes?").bold(true).multiline(true).widget_instance()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new(
|
||||
format!(
|
||||
"
|
||||
Settings that only take effect on next launch:\n\
|
||||
{changed_settings}\n\
|
||||
\n\
|
||||
This only takes a few seconds. Open documents,\n\
|
||||
even unsaved ones, will be automatically restored.
|
||||
"
|
||||
)
|
||||
.trim(),
|
||||
)
|
||||
.multiline(true)
|
||||
.widget_instance(),
|
||||
],
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ impl DialogLayoutHolder for DemoArtworkDialog {
|
||||
const TITLE: &'static str = "Demo Artwork";
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![TextButton::new("Close").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance()];
|
||||
let widgets = vec![TextButton::new("Close").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()];
|
||||
|
||||
Layout(vec![LayoutGroup::Row { widgets }])
|
||||
}
|
||||
@@ -32,7 +32,7 @@ impl LayoutHolder for DemoArtworkDialog {
|
||||
.chunks(4)
|
||||
.flat_map(|chunk| {
|
||||
fn make_dialog(name: &str, filename: &str) -> Message {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
DialogMessage::CloseAndThen {
|
||||
followups: vec![
|
||||
FrontendMessage::TriggerFetchAndOpenDocument {
|
||||
name: name.to_string(),
|
||||
|
||||
@@ -12,7 +12,7 @@ impl DialogLayoutHolder for ErrorDialog {
|
||||
const TITLE: &'static str = "Error";
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance()];
|
||||
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()];
|
||||
|
||||
Layout(vec![LayoutGroup::Row { widgets }])
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ impl DialogLayoutHolder for LicensesDialog {
|
||||
const TITLE: &'static str = "Licenses";
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance()];
|
||||
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()];
|
||||
|
||||
Layout(vec![LayoutGroup::Row { widgets }])
|
||||
}
|
||||
@@ -18,25 +18,19 @@ impl DialogLayoutHolder for LicensesDialog {
|
||||
fn layout_column_2(&self) -> Layout {
|
||||
#[allow(clippy::type_complexity)]
|
||||
let button_definitions: &[(&str, &str, fn() -> Message)] = &[
|
||||
("GraphiteLogo", "Graphite Logo", || {
|
||||
("Code", "Source Code License", || {
|
||||
FrontendMessage::TriggerVisitLink {
|
||||
url: "https://graphite.art/logo/".into(),
|
||||
url: "https://graphite.art/license#source-code".into(),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
("IconsGrid", "Graphite Icons", || {
|
||||
("GraphiteLogo", "Branding License", || {
|
||||
FrontendMessage::TriggerVisitLink {
|
||||
url: "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/frontend/assets/LICENSE.md".into(),
|
||||
url: "https://graphite.art/license#branding".into(),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
("License", "Graphite License", || {
|
||||
FrontendMessage::TriggerVisitLink {
|
||||
url: "https://graphite.art/license/".into(),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
("License", "Other Licenses", || FrontendMessage::TriggerDisplayThirdPartyLicensesDialog.into()),
|
||||
("IconsGrid", "Dependency Licenses", || FrontendMessage::TriggerDisplayThirdPartyLicensesDialog.into()),
|
||||
];
|
||||
let widgets = button_definitions
|
||||
.iter()
|
||||
@@ -52,13 +46,11 @@ impl LayoutHolder for LicensesDialog {
|
||||
let year = &self.localized_commit_year;
|
||||
let description = format!(
|
||||
"
|
||||
The Graphite logo and brand identity are copyright © {year}\nGraphite Labs, LLC. See \"Graphite Logo\" for usage policy.\n\
|
||||
Graphite source code is copyright © {year} Graphite contrib-\nutors and is available under the Apache License 2.0. See\n\"Source Code License\" for details.\n\
|
||||
\n\
|
||||
The Graphite editor's icons and design assets are copyright\n© {year} Graphite Labs, LLC. See \"Graphite Icons\" for details.\n\
|
||||
The Graphite logo, icons, and visual identity are copyright ©\n{year} Graphite Labs, LLC. See \"Branding License\" for details.\n\
|
||||
\n\
|
||||
Graphite code is copyright © {year} Graphite contributors\nand is made available under the Apache 2.0 license. See\n\"Graphite License\" for details.\n\
|
||||
\n\
|
||||
Graphite is distributed with third-party open source code\ndependencies. See \"Other Licenses\" for details.
|
||||
Graphite is distributed with third-party open source code\ndependencies. See \"Dependency Licenses\" for details.
|
||||
"
|
||||
);
|
||||
let description = description.trim();
|
||||
|
||||
@@ -10,7 +10,7 @@ impl DialogLayoutHolder for LicensesThirdPartyDialog {
|
||||
const TITLE: &'static str = "Third-Party Software License Notices";
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_instance()];
|
||||
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()];
|
||||
|
||||
Layout(vec![LayoutGroup::Row { widgets }])
|
||||
}
|
||||
@@ -29,14 +29,14 @@ impl LayoutHolder for LicensesThirdPartyDialog {
|
||||
};
|
||||
|
||||
// Two characters (one before, one after) the sequence of underscore characters, plus one additional column to provide a space between the text and the scrollbar
|
||||
let non_wrapping_column_width = license_text.split('\n').map(|line| line.chars().filter(|&c| c == '_').count()).max().unwrap_or(0) + 2 + 1;
|
||||
let non_wrapping_column_width = license_text.split('\n').map(|line| line.chars().filter(|&c| c == '_').count() as u32).max().unwrap_or(0) + 2 + 1;
|
||||
|
||||
Layout(vec![LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new(license_text)
|
||||
.monospace(true)
|
||||
.multiline(true)
|
||||
.min_width(format!("{non_wrapping_column_width}ch"))
|
||||
.min_width_characters(non_wrapping_column_width)
|
||||
.widget_instance(),
|
||||
],
|
||||
}])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod about_graphite_dialog;
|
||||
mod close_all_documents_dialog;
|
||||
mod close_document_dialog;
|
||||
mod confirm_restart_dialog;
|
||||
mod demo_artwork_dialog;
|
||||
mod error_dialog;
|
||||
mod licenses_dialog;
|
||||
@@ -9,6 +10,7 @@ mod licenses_third_party_dialog;
|
||||
pub use about_graphite_dialog::AboutGraphiteDialog;
|
||||
pub use close_all_documents_dialog::CloseAllDocumentsDialog;
|
||||
pub use close_document_dialog::CloseDocumentDialog;
|
||||
pub use confirm_restart_dialog::ConfirmRestartDialog;
|
||||
pub use demo_artwork_dialog::ARTWORK;
|
||||
pub use demo_artwork_dialog::DemoArtworkDialog;
|
||||
pub use error_dialog::ErrorDialog;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument};
|
||||
use crate::messages::app_window::app_window_message_handler::AppWindowPlatform;
|
||||
use crate::messages::frontend::utility_types::EyedropperPreviewImage;
|
||||
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::node_graph::utility_types::{
|
||||
BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, NodeGraphErrorDiagnostic, Transform,
|
||||
};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::{LayerPanelEntry, LayerStructureEntry};
|
||||
use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate};
|
||||
use crate::messages::prelude::*;
|
||||
use glam::IVec2;
|
||||
@@ -27,7 +28,7 @@ pub enum FrontendMessage {
|
||||
title: String,
|
||||
icon: String,
|
||||
},
|
||||
DisplayDialogDismiss,
|
||||
DialogClose,
|
||||
DisplayDialogPanic {
|
||||
#[serde(rename = "panicInfo")]
|
||||
panic_info: String,
|
||||
@@ -38,7 +39,7 @@ pub enum FrontendMessage {
|
||||
line_height_ratio: f64,
|
||||
#[serde(rename = "fontSize")]
|
||||
font_size: f64,
|
||||
color: Color,
|
||||
color: String,
|
||||
#[serde(rename = "fontData")]
|
||||
font_data: Vec<u8>,
|
||||
transform: [f64; 6],
|
||||
@@ -64,8 +65,10 @@ pub enum FrontendMessage {
|
||||
#[serde(rename = "nodeTypes")]
|
||||
node_types: Vec<FrontendNodeType>,
|
||||
},
|
||||
SendShortcutF11 {
|
||||
SendShortcutFullscreen {
|
||||
shortcut: Option<ActionShortcut>,
|
||||
#[serde(rename = "shortcutMac")]
|
||||
shortcut_mac: Option<ActionShortcut>,
|
||||
},
|
||||
SendShortcutAltClick {
|
||||
shortcut: Option<ActionShortcut>,
|
||||
@@ -105,7 +108,6 @@ pub enum FrontendMessage {
|
||||
font: Font,
|
||||
url: String,
|
||||
},
|
||||
TriggerImport,
|
||||
TriggerPersistenceRemoveDocument {
|
||||
#[serde(rename = "documentId")]
|
||||
document_id: DocumentId,
|
||||
@@ -120,7 +122,8 @@ pub enum FrontendMessage {
|
||||
TriggerLoadRestAutoSaveDocuments,
|
||||
TriggerOpenLaunchDocuments,
|
||||
TriggerLoadPreferences,
|
||||
TriggerOpenDocument,
|
||||
TriggerOpen,
|
||||
TriggerImport,
|
||||
TriggerSavePreferences {
|
||||
preferences: PreferencesMessageHandler,
|
||||
},
|
||||
@@ -148,6 +151,11 @@ pub enum FrontendMessage {
|
||||
#[serde(rename = "documentId")]
|
||||
document_id: DocumentId,
|
||||
},
|
||||
UpdateGradientStopColorPickerPosition {
|
||||
color: Color,
|
||||
x: f64,
|
||||
y: f64,
|
||||
},
|
||||
UpdateImportsExports {
|
||||
/// If the primary import is not visible, then it is None.
|
||||
imports: Vec<Option<FrontendGraphOutput>>,
|
||||
@@ -191,7 +199,9 @@ pub enum FrontendMessage {
|
||||
UpdateLayersPanelState {
|
||||
open: bool,
|
||||
},
|
||||
UpdateDataPanelLayout {
|
||||
UpdateLayout {
|
||||
#[serde(rename = "layoutTarget")]
|
||||
layout_target: LayoutTarget,
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateImportReorderIndex {
|
||||
@@ -210,34 +220,18 @@ pub enum FrontendMessage {
|
||||
#[serde(rename = "hasLeftInputWire")]
|
||||
has_left_input_wire: HashMap<NodeId, bool>,
|
||||
},
|
||||
UpdateDialogButtons {
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateDialogColumn1 {
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateDialogColumn2 {
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateDocumentArtwork {
|
||||
svg: String,
|
||||
},
|
||||
UpdateImageData {
|
||||
image_data: Vec<(u64, Image<Color>)>,
|
||||
},
|
||||
UpdateDocumentBarLayout {
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateDocumentLayerDetails {
|
||||
data: LayerPanelEntry,
|
||||
},
|
||||
UpdateDocumentLayerStructure {
|
||||
#[serde(rename = "dataBuffer")]
|
||||
data_buffer: RawBuffer,
|
||||
},
|
||||
UpdateDocumentLayerStructureJs {
|
||||
#[serde(rename = "dataBuffer")]
|
||||
data_buffer: JsRawBuffer,
|
||||
#[serde(rename = "layerStructure")]
|
||||
layer_structure: Vec<LayerStructureEntry>,
|
||||
},
|
||||
UpdateDocumentRulers {
|
||||
origin: (f64, f64),
|
||||
@@ -251,6 +245,7 @@ pub enum FrontendMessage {
|
||||
multiplier: (f64, f64),
|
||||
},
|
||||
UpdateEyedropperSamplingState {
|
||||
image: Option<EyedropperPreviewImage>,
|
||||
#[serde(rename = "mousePosition")]
|
||||
mouse_position: Option<(f64, f64)>,
|
||||
#[serde(rename = "primaryColor")]
|
||||
@@ -263,18 +258,6 @@ pub enum FrontendMessage {
|
||||
UpdateGraphFadeArtwork {
|
||||
percentage: f64,
|
||||
},
|
||||
UpdateLayersPanelControlBarLeftLayout {
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateLayersPanelControlBarRightLayout {
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateLayersPanelBottomBarLayout {
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateMenuBarLayout {
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateMouseCursor {
|
||||
cursor: MouseCursorIcon,
|
||||
},
|
||||
@@ -291,9 +274,6 @@ pub enum FrontendMessage {
|
||||
wires: Vec<WirePathUpdate>,
|
||||
},
|
||||
ClearAllNodeGraphWires,
|
||||
UpdateNodeGraphControlBarLayout {
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateNodeGraphSelection {
|
||||
selected: Vec<NodeId>,
|
||||
},
|
||||
@@ -308,28 +288,10 @@ pub enum FrontendMessage {
|
||||
#[serde(rename = "openDocuments")]
|
||||
open_documents: Vec<OpenDocument>,
|
||||
},
|
||||
UpdatePropertiesPanelLayout {
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateToolOptionsLayout {
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateToolShelfLayout {
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateWirePathInProgress {
|
||||
#[serde(rename = "wirePath")]
|
||||
wire_path: Option<WirePath>,
|
||||
},
|
||||
UpdateWelcomeScreenButtonsLayout {
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateStatusBarHintsLayout {
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateWorkingColorsLayout {
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdatePlatform {
|
||||
platform: AppWindowPlatform,
|
||||
},
|
||||
@@ -360,11 +322,18 @@ pub enum FrontendMessage {
|
||||
},
|
||||
|
||||
// Window prefix: cause the application window to do something
|
||||
WindowPointerLock,
|
||||
WindowPointerLockMove {
|
||||
x: f64,
|
||||
y: f64,
|
||||
},
|
||||
WindowClose,
|
||||
WindowMinimize,
|
||||
WindowMaximize,
|
||||
WindowFullscreen,
|
||||
WindowDrag,
|
||||
WindowHide,
|
||||
WindowHideOthers,
|
||||
WindowShowAll,
|
||||
WindowRestart,
|
||||
}
|
||||
|
||||
@@ -62,3 +62,10 @@ pub enum ExportBounds {
|
||||
Selection,
|
||||
Artboard(LayerNodeIdentifier),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct EyedropperPreviewImage {
|
||||
pub data: Vec<u8>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
use crate::messages::portfolio::utility_types::Platform;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub static GLOBAL_PLATFORM: OnceLock<Platform> = OnceLock::new();
|
||||
@@ -1,8 +0,0 @@
|
||||
use crate::messages::portfolio::utility_types::Platform;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[impl_message(Message, Globals)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum GlobalsMessage {
|
||||
SetPlatform { platform: Platform },
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[derive(Debug, Default, ExtractField)]
|
||||
pub struct GlobalsMessageHandler {}
|
||||
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<GlobalsMessage, ()> for GlobalsMessageHandler {
|
||||
fn process_message(&mut self, message: GlobalsMessage, _responses: &mut VecDeque<Message>, _: ()) {
|
||||
match message {
|
||||
GlobalsMessage::SetPlatform { platform } => {
|
||||
if GLOBAL_PLATFORM.get() != Some(&platform) {
|
||||
GLOBAL_PLATFORM.set(platform).expect("Failed to set GLOBAL_PLATFORM");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
advertise_actions!(GlobalsMessageDiscriminant;
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
mod globals_message;
|
||||
mod globals_message_handler;
|
||||
|
||||
pub mod global_variables;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use globals_message::{GlobalsMessage, GlobalsMessageDiscriminant};
|
||||
#[doc(inline)]
|
||||
pub use globals_message_handler::GlobalsMessageHandler;
|
||||
@@ -1,10 +1,9 @@
|
||||
use super::utility_types::input_keyboard::KeysGroup;
|
||||
use super::utility_types::misc::Mapping;
|
||||
use crate::application::Editor;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{self, Key};
|
||||
use crate::messages::input_mapper::utility_types::misc::MappingEntry;
|
||||
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
|
||||
use crate::messages::prelude::*;
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(ExtractField)]
|
||||
pub struct InputMapperMessageContext<'a> {
|
||||
@@ -34,27 +33,6 @@ impl InputMapperMessageHandler {
|
||||
self.mapping = mapping;
|
||||
}
|
||||
|
||||
pub fn hints(&self, actions: ActionList) -> String {
|
||||
let mut output = String::new();
|
||||
let mut actions = actions
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|a| !matches!(*a, MessageDiscriminant::Tool(ToolMessageDiscriminant::ActivateTool) | MessageDiscriminant::Debug(_)));
|
||||
self.mapping
|
||||
.key_down
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, m)| {
|
||||
let ma = m.0.iter().find_map(|m| actions.find_map(|a| (a == m.action.to_discriminant()).then(|| m.action.to_discriminant())));
|
||||
|
||||
ma.map(|a| ((i as u8).try_into().unwrap(), a))
|
||||
})
|
||||
.for_each(|(k, a): (Key, _)| {
|
||||
let _ = write!(output, "{}: {}, ", k.to_discriminant().local_name(), a.local_name().split('.').next_back().unwrap());
|
||||
});
|
||||
output.replace("Key", "")
|
||||
}
|
||||
|
||||
pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant) -> Option<KeysGroup> {
|
||||
let all_key_mapping_entries = std::iter::empty()
|
||||
.chain(self.mapping.key_up.iter())
|
||||
@@ -70,11 +48,7 @@ impl InputMapperMessageHandler {
|
||||
let found_actions = all_mapping_entries.filter(|entry| entry.action.to_discriminant() == *action_to_find);
|
||||
|
||||
// Get the `Key` for this platform's accelerator key
|
||||
let keyboard_layout = || GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout();
|
||||
let platform_accel_key = match keyboard_layout() {
|
||||
KeyboardPlatformLayout::Standard => Key::Control,
|
||||
KeyboardPlatformLayout::Mac => Key::Command,
|
||||
};
|
||||
let platform_accel_key = if Editor::environment().is_mac() { Key::Command } else { Key::Control };
|
||||
|
||||
let entry_to_key = |entry: &MappingEntry| {
|
||||
// Get the modifier keys for the entry (and convert them to Key)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user