Merge branch 'master' into actions_menu

This commit is contained in:
Adesh Gupta
2026-03-07 19:53:12 +05:30
committed by GitHub
481 changed files with 19998 additions and 12741 deletions

View File

@@ -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

2
.envrc
View File

@@ -1 +1 @@
use flake .nix
use flake

View File

@@ -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.
-->

View 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
View 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

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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
View 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

View File

@@ -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:

View File

@@ -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) {

View File

@@ -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
View 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 }}

View 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"

View File

@@ -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
View File

@@ -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
View 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);
}

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
{ pkgs, inputs, ... }:
{ pkgs, ... }:
let
extensions = [

View File

@@ -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
View File

@@ -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
}

View File

@@ -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;
}
);
}

View 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
''

View 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";
};
};
};
};
}

View 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;
}
];
}
];
}

View File

@@ -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"
];
};

View File

@@ -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
'';
}
)

View 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";
}
)

View File

@@ -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

View File

@@ -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
View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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" }

View File

@@ -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}}
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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 }

View File

@@ -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

View File

@@ -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(())
}

View File

@@ -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(())

View File

@@ -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(())

View File

@@ -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(())
}

View File

@@ -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");

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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()));
}
}

View File

@@ -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.

View File

@@ -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(),

View File

@@ -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,
}
}
}

View File

@@ -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"))]
{

View File

@@ -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,
};

View File

@@ -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";

View File

@@ -8,7 +8,7 @@ pub(crate) enum AppEvent {
WebCommunicationInitialized,
DesktopWrapperMessage(DesktopWrapperMessage),
NodeGraphExecutionResult(NodeGraphExecutionResult),
CloseWindow,
Exit,
#[cfg(target_os = "macos")]
MenuEvent {
id: String,

View File

@@ -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() {

View File

@@ -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
}
}

View 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
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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
}

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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"),
};

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -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,
}
}
}

View File

@@ -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,
}

View File

@@ -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);
}
}
}

View File

@@ -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())

View File

@@ -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")

View File

@@ -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,
}

View File

@@ -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(),
];

View File

@@ -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 }])
}

View File

@@ -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 }])

View File

@@ -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 }])

View File

@@ -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(),
],
},
])
}
}

View File

@@ -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(),

View File

@@ -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 }])
}

View File

@@ -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();

View File

@@ -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(),
],
}])

View File

@@ -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;

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -1,4 +0,0 @@
use crate::messages::portfolio::utility_types::Platform;
use std::sync::OnceLock;
pub static GLOBAL_PLATFORM: OnceLock<Platform> = OnceLock::new();

View File

@@ -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 },
}

View File

@@ -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;
);
}

View File

@@ -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;

View File

@@ -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