mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 02:58:12 +08:00
Merge branch 'master' into actions_menu
This commit is contained in:
@@ -1,2 +1,2 @@
|
|||||||
https://github.com/Keavon/graphite-branded-assets/archive/f8b02e68c92f5bbd27626bdd7a51102303b70a40.tar.gz
|
https://github.com/Keavon/graphite-branded-assets/archive/8ae15dc9c51a3855475d8cab1d0f29d9d9bc622c.tar.gz
|
||||||
d06fd7b79fa9b7509c23072fa56745415fdc6eb98575d15214b0acc47ea4dd42
|
c19abe4ac848f3c835e43dc065c59e20e60233ae023ea0a064c5fed442be2d3d
|
||||||
|
|||||||
@@ -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.
|
||||||
|
-->
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -14,4 +14,4 @@ jobs:
|
|||||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
- name: Build Nix Package Dev
|
- name: Build Nix Package Dev
|
||||||
run: nix build .nix#graphite-dev --print-build-logs
|
run: nix build .#graphite-dev --print-build-logs
|
||||||
|
|||||||
@@ -52,9 +52,7 @@ jobs:
|
|||||||
- name: 🌐 Build Graphite web code
|
- name: 🌐 Build Graphite web code
|
||||||
env:
|
env:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
run: |
|
run: mold -run cargo run build web
|
||||||
cd frontend
|
|
||||||
mold -run npm run build
|
|
||||||
|
|
||||||
- name: 📤 Publish to Cloudflare Pages
|
- name: 📤 Publish to Cloudflare Pages
|
||||||
id: cloudflare
|
id: cloudflare
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -7,6 +7,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cargo-deny:
|
cargo-deny:
|
||||||
|
if: github.repository == 'GraphiteEditor/Graphite' # Don't run on forks by default
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
# USAGE:
|
# USAGE:
|
||||||
# After reviewing the code, core team members may comment on a PR with the exact text:
|
# 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-debug` to build with debug symbols and optimizations disabled
|
||||||
# - `!build-profiling` to build with debug symbols and optimizations enabled
|
|
||||||
# - `!build` to build without debug symbols and optimizations enabled
|
# - `!build` to build without debug symbols and optimizations enabled
|
||||||
# The comment may not contain any other text, not even whitespace or newlines.
|
# The comment may not contain any other text, not even whitespace or newlines.
|
||||||
name: "!build PR Command"
|
name: "!build PR Command"
|
||||||
@@ -21,7 +20,7 @@ jobs:
|
|||||||
if: >
|
if: >
|
||||||
github.event.issue.pull_request &&
|
github.event.issue.pull_request &&
|
||||||
github.event.comment.author_association == 'MEMBER' &&
|
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
|
runs-on: self-hosted
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -82,22 +81,43 @@ jobs:
|
|||||||
- name: ⌨ Set build command based on comment
|
- name: ⌨ Set build command based on comment
|
||||||
id: build_command
|
id: build_command
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event.comment.body }}" == "!build-dev" ]]; then
|
if [[ "${{ github.event.comment.body }}" == "!build-debug" ]]; then
|
||||||
echo "command=build-dev" >> $GITHUB_OUTPUT
|
echo "command=build web debug" >> $GITHUB_OUTPUT
|
||||||
elif [[ "${{ github.event.comment.body }}" == "!build-profiling" ]]; then
|
|
||||||
echo "command=build-profiling" >> $GITHUB_OUTPUT
|
|
||||||
elif [[ "${{ github.event.comment.body }}" == "!build" ]]; then
|
elif [[ "${{ github.event.comment.body }}" == "!build" ]]; then
|
||||||
echo "command=build" >> $GITHUB_OUTPUT
|
echo "command=build web" >> $GITHUB_OUTPUT
|
||||||
else
|
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
|
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
|
- name: 🌐 Build Graphite web code
|
||||||
env:
|
env:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
run: |
|
if: ${{ success() || failure()}}
|
||||||
cd frontend
|
run: mold -run cargo run ${{ steps.build_command.outputs.command }}
|
||||||
mold -run npm 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
|
- name: 📤 Publish to Cloudflare Pages
|
||||||
id: cloudflare
|
id: cloudflare
|
||||||
@@ -110,6 +130,18 @@ jobs:
|
|||||||
projectName: graphite-dev
|
projectName: graphite-dev
|
||||||
directory: frontend/dist
|
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
|
- name: 💬 Comment build link
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ name: Profiling Changes
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'node-graph/**'
|
- "node-graph/**"
|
||||||
- 'Cargo.toml'
|
- "Cargo.toml"
|
||||||
- 'Cargo.lock'
|
- "Cargo.lock"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
@@ -200,7 +200,7 @@ jobs:
|
|||||||
let commentBody = "";
|
let commentBody = "";
|
||||||
|
|
||||||
function formatNumber(num) {
|
function formatNumber(num) {
|
||||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPercentage(pct) {
|
function formatPercentage(pct) {
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
name: "Editor: Dev & CI"
|
name: "Deploy Master"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request: {}
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
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>
|
INDEX_HTML_HEAD_REPLACEMENT: <script defer data-domain="dev.graphite.art" data-api="https://graphite.art/visit/event" src="https://graphite.art/visit/script.hash.js"></script>
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
deploy:
|
||||||
runs-on: self-hosted
|
runs-on: [self-hosted, target/wasm]
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
deployments: write
|
deployments: write
|
||||||
pull-requests: write
|
|
||||||
actions: write
|
actions: write
|
||||||
env:
|
env:
|
||||||
RUSTC_WRAPPER: /usr/bin/sccache
|
RUSTC_WRAPPER: /usr/bin/sccache
|
||||||
@@ -41,29 +40,16 @@ jobs:
|
|||||||
|
|
||||||
- name: 🦀 Install the latest Rust
|
- name: 🦀 Install the latest Rust
|
||||||
run: |
|
run: |
|
||||||
echo "Initial system version:"
|
|
||||||
rustc --version
|
|
||||||
rustup update stable
|
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
|
- name: ✂ Replace template in <head> of index.html
|
||||||
run: |
|
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
|
sed -i "s|<!-- INDEX_HTML_HEAD_REPLACEMENT -->|$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html
|
||||||
|
|
||||||
- name: 🌐 Build Graphite web code
|
- name: 🌐 Build Graphite web code
|
||||||
env:
|
env:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
run: |
|
run: mold -run cargo run build web
|
||||||
cd frontend
|
|
||||||
mold -run npm run build
|
|
||||||
|
|
||||||
- name: 📤 Publish to Cloudflare Pages
|
- name: 📤 Publish to Cloudflare Pages
|
||||||
id: cloudflare
|
id: cloudflare
|
||||||
@@ -77,7 +63,6 @@ jobs:
|
|||||||
directory: frontend/dist
|
directory: frontend/dist
|
||||||
|
|
||||||
- name: 💬 Comment build link URL to commit hash page on GitHub
|
- name: 💬 Comment build link URL to commit hash page on GitHub
|
||||||
if: github.ref == 'refs/heads/master'
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
@@ -89,36 +74,15 @@ jobs:
|
|||||||
|-|
|
|-|
|
||||||
| ${{ steps.cloudflare.outputs.url }} |"
|
| ${{ 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
|
- name: 📃 Generate code documentation info for website
|
||||||
if: github.ref == 'refs/heads/master'
|
|
||||||
run: |
|
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
|
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
|
- 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
|
id: cache-website-code-docs
|
||||||
uses: actions/cache/restore@v3
|
uses: actions/cache/restore@v3
|
||||||
with:
|
with:
|
||||||
@@ -126,7 +90,6 @@ jobs:
|
|||||||
key: website-code-docs
|
key: website-code-docs
|
||||||
|
|
||||||
- name: 🔍 Check if auto-generated code docs artifacts changed
|
- name: 🔍 Check if auto-generated code docs artifacts changed
|
||||||
if: github.ref == 'refs/heads/master'
|
|
||||||
id: website-code-docs-changed
|
id: website-code-docs-changed
|
||||||
run: |
|
run: |
|
||||||
if ! diff --brief --recursive artifacts-generated artifacts; then
|
if ! diff --brief --recursive artifacts-generated artifacts; then
|
||||||
@@ -154,31 +117,3 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
rm -rf artifacts
|
rm -rf artifacts
|
||||||
gh workflow run website.yml --ref master
|
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
|
|
||||||
@@ -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 }}
|
||||||
@@ -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"
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: 🕸 Install Zola
|
- name: 🕸 Install Zola
|
||||||
uses: taiki-e/install-action@v2
|
uses: taiki-e/install-action@v2
|
||||||
with:
|
with:
|
||||||
tool: zola@0.20.0
|
tool: zola@0.22.0
|
||||||
|
|
||||||
- name: 🔍 Check if `website/other` directory changed
|
- name: 🔍 Check if `website/other` directory changed
|
||||||
uses: dorny/paths-filter@v3
|
uses: dorny/paths-filter@v3
|
||||||
@@ -67,19 +67,27 @@ jobs:
|
|||||||
rustup update stable
|
rustup update stable
|
||||||
echo "🦀 Latest updated version of Rust:"
|
echo "🦀 Latest updated version of Rust:"
|
||||||
rustc --version
|
rustc --version
|
||||||
cargo test --package graphite-editor --lib -- messages::message::test::generate_message_tree
|
cd tools/editor-message-tree
|
||||||
|
cargo run
|
||||||
|
cd ../..
|
||||||
mkdir artifacts
|
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: |
|
run: |
|
||||||
mv artifacts/* .
|
mkdir -p website/generated
|
||||||
|
mv artifacts/* website/generated/
|
||||||
|
|
||||||
- name: 🔧 Build auto-generated code docs artifacts into HTML
|
- name: 🔧 Build auto-generated code docs artifacts into HTML
|
||||||
run: |
|
run: |
|
||||||
cd website
|
cd website
|
||||||
npm run generate-editor-structure
|
npm run generate-editor-structure
|
||||||
|
|
||||||
|
- name: 📃 Generate node catalog documentation
|
||||||
|
run: |
|
||||||
|
cd tools/node-docs
|
||||||
|
cargo run
|
||||||
|
|
||||||
- name: 🌐 Build Graphite website with Zola
|
- name: 🌐 Build Graphite website with Zola
|
||||||
env:
|
env:
|
||||||
MODE: prod
|
MODE: prod
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,8 @@
|
|||||||
branding/
|
branding/
|
||||||
target/
|
target/
|
||||||
|
third-party-licenses.txt*
|
||||||
result/
|
result/
|
||||||
|
.flatpak-builder/
|
||||||
*.spv
|
*.spv
|
||||||
*.exrc
|
*.exrc
|
||||||
perf.data*
|
perf.data*
|
||||||
@@ -10,5 +12,3 @@ flamegraph.svg
|
|||||||
.idea/
|
.idea/
|
||||||
.direnv
|
.direnv
|
||||||
.DS_Store
|
.DS_Store
|
||||||
hierarchical_message_system_tree.txt
|
|
||||||
hierarchical_message_system_tree.html
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
+18
-17
@@ -1,25 +1,26 @@
|
|||||||
{ pkgs, inputs, ... }:
|
{ pkgs, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
cef = pkgs.cef-binary.overrideAttrs (_: _: {
|
cefPath = pkgs.cef-binary.overrideAttrs (finalAttrs: {
|
||||||
postInstall = ''
|
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
|
in
|
||||||
{
|
{
|
||||||
env.CEF_PATH = cefPath;
|
env.CEF_PATH = cefPath;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{ pkgs, inputs, ... }:
|
{ pkgs, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
extensions = [
|
extensions = [
|
||||||
|
|||||||
+51
-9
@@ -1,16 +1,58 @@
|
|||||||
{
|
{ pkgs, deps, ... }:
|
||||||
pkgs,
|
|
||||||
deps,
|
|
||||||
libs,
|
|
||||||
tools,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
|
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 (
|
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";
|
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 = ''
|
shellHook = ''
|
||||||
|
|||||||
Generated
-113
@@ -1,113 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"crane": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1763938834,
|
|
||||||
"narHash": "sha256-j8iB0Yr4zAvQLueCZ5abxfk6fnG/SJ5JnGUziETjwfg=",
|
|
||||||
"owner": "ipetkov",
|
|
||||||
"repo": "crane",
|
|
||||||
"rev": "d9e753122e51cee64eb8d2dddfe11148f339f5a2",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "ipetkov",
|
|
||||||
"repo": "crane",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-compat": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1733328505,
|
|
||||||
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
|
|
||||||
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
|
|
||||||
"revCount": 69,
|
|
||||||
"type": "tarball",
|
|
||||||
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"type": "tarball",
|
|
||||||
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1731533236,
|
|
||||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1764242076,
|
|
||||||
"narHash": "sha256-sKoIWfnijJ0+9e4wRvIgm/HgE27bzwQxcEmo2J/gNpI=",
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "2fad6eac6077f03fe109c4d4eb171cf96791faa4",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nixos",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"crane": "crane",
|
|
||||||
"flake-compat": "flake-compat",
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs",
|
|
||||||
"rust-overlay": "rust-overlay"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rust-overlay": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1764297505,
|
|
||||||
"narHash": "sha256-qrLpVu2/hA9Cu6IovMEsgh9YRyvmmWS+bSx7C1JGChA=",
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"rev": "9623580f8ce09ec444b9aca107566ec5db110e62",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
-159
@@ -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;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
''
|
||||||
@@ -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";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
self,
|
||||||
|
system,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
(pkgs.formats.json { }).generate "art.graphite.Graphite.json" {
|
||||||
|
app-id = "art.graphite.Graphite";
|
||||||
|
runtime = "org.freedesktop.Platform";
|
||||||
|
runtime-version = "25.08";
|
||||||
|
sdk = "org.freedesktop.Sdk";
|
||||||
|
command = "graphite";
|
||||||
|
finish-args = [
|
||||||
|
"--device=dri"
|
||||||
|
"--share=ipc"
|
||||||
|
"--socket=wayland"
|
||||||
|
"--socket=fallback-x11"
|
||||||
|
"--share=network"
|
||||||
|
];
|
||||||
|
modules = [
|
||||||
|
{
|
||||||
|
name = "app";
|
||||||
|
buildsystem = "simple";
|
||||||
|
build-commands = [
|
||||||
|
"mkdir -p /app"
|
||||||
|
"cp -r ./* /app/"
|
||||||
|
"chmod +x /app/bin/*"
|
||||||
|
];
|
||||||
|
sources = [
|
||||||
|
{
|
||||||
|
type = "archive";
|
||||||
|
path = self.packages.${system}.graphite-bundle.tar;
|
||||||
|
strip-components = 0;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,12 +1,4 @@
|
|||||||
{
|
{ info, deps, ... }:
|
||||||
info,
|
|
||||||
pkgs,
|
|
||||||
inputs,
|
|
||||||
deps,
|
|
||||||
libs,
|
|
||||||
tools,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
(deps.crane.lib.overrideToolchain (_: deps.rustGPU.toolchain)).buildPackage {
|
(deps.crane.lib.overrideToolchain (_: deps.rustGPU.toolchain)).buildPackage {
|
||||||
pname = "raster-nodes-shaders";
|
pname = "raster-nodes-shaders";
|
||||||
@@ -16,7 +8,7 @@
|
|||||||
inherit (deps.crane.lib.findCargoFiles (deps.crane.lib.cleanCargoSource info.src)) cargoConfigs;
|
inherit (deps.crane.lib.findCargoFiles (deps.crane.lib.cleanCargoSource info.src)) cargoConfigs;
|
||||||
cargoLockList = [
|
cargoLockList = [
|
||||||
"${info.src}/Cargo.lock"
|
"${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"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
+90
-54
@@ -1,61 +1,58 @@
|
|||||||
{
|
{
|
||||||
info,
|
info,
|
||||||
pkgs,
|
pkgs,
|
||||||
inputs,
|
self,
|
||||||
deps,
|
deps,
|
||||||
libs,
|
system,
|
||||||
tools,
|
lib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
|
||||||
{
|
{
|
||||||
embeddedResources ? true,
|
|
||||||
dev ? false,
|
dev ? false,
|
||||||
}:
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
brandingTar = pkgs.fetchurl (
|
branding = self.packages.${system}.graphite-branding;
|
||||||
let
|
cargoVendorDir = deps.crane.lib.vendorCargoDeps { inherit (info) src; };
|
||||||
lockContent = builtins.readFile "${info.src}/.branding";
|
|
||||||
lines = builtins.filter (s: s != [ ]) (builtins.split "\n" lockContent);
|
|
||||||
url = builtins.elemAt lines 0;
|
|
||||||
hash = builtins.elemAt lines 1;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
url = url;
|
|
||||||
sha256 = hash;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
branding = pkgs.runCommand "${info.pname}-branding" { } ''
|
|
||||||
mkdir -p $out
|
|
||||||
tar -xvf ${brandingTar} -C $out --strip-components 1
|
|
||||||
'';
|
|
||||||
resourcesCommon = {
|
resourcesCommon = {
|
||||||
pname = "${info.pname}-resources";
|
pname = "${info.pname}-resources";
|
||||||
inherit (info) version src;
|
inherit (info) version src;
|
||||||
|
inherit cargoVendorDir;
|
||||||
strictDeps = true;
|
strictDeps = true;
|
||||||
doCheck = false;
|
nativeBuildInputs = [
|
||||||
nativeBuildInputs = tools.frontend;
|
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";
|
env.CARGO_PROFILE = if dev then "dev" else "release";
|
||||||
cargoExtraArgs = "--target wasm32-unknown-unknown -p graphite-wasm --no-default-features --features native";
|
cargoExtraArgs = "--target wasm32-unknown-unknown -p graphite-wasm --no-default-features --features native";
|
||||||
|
doCheck = false;
|
||||||
};
|
};
|
||||||
resources = deps.crane.lib.buildPackage (
|
resources = deps.crane.lib.buildPackage (
|
||||||
resourcesCommon
|
resourcesCommon
|
||||||
// {
|
// {
|
||||||
cargoArtifacts = deps.crane.lib.buildDepsOnly 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.importNpmLock {
|
||||||
npmDeps = pkgs.fetchNpmDeps {
|
npmRoot = "${info.src}/frontend";
|
||||||
inherit (info) pname version;
|
|
||||||
src = "${info.src}/frontend";
|
|
||||||
hash = "sha256-D8VCNK+Ca3gxO+5wriBn8FszG8/x8n/zM6/MPo9E2j4=";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
npmRoot = "frontend";
|
npmRoot = "frontend";
|
||||||
npmConfigScript = "setup";
|
npmConfigScript = "setup";
|
||||||
makeCacheWritable = true;
|
makeCacheWritable = true;
|
||||||
|
|
||||||
nativeBuildInputs = tools.frontend ++ [ pkgs.npmHooks.npmConfigHook ];
|
nativeBuildInputs = [
|
||||||
|
pkgs.importNpmLock.npmConfigHook
|
||||||
|
pkgs.removeReferencesTo
|
||||||
|
]
|
||||||
|
++ resourcesCommon.nativeBuildInputs;
|
||||||
|
|
||||||
prePatch = ''
|
prePatch = ''
|
||||||
mkdir branding
|
mkdir branding
|
||||||
@@ -75,19 +72,39 @@ let
|
|||||||
mkdir -p $out
|
mkdir -p $out
|
||||||
cp -r frontend/dist/* $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 = {
|
common = {
|
||||||
inherit (info) pname version src;
|
inherit (info) pname version src;
|
||||||
|
inherit cargoVendorDir;
|
||||||
strictDeps = true;
|
strictDeps = true;
|
||||||
buildInputs = libs.desktop-all;
|
buildInputs = libs;
|
||||||
nativeBuildInputs = tools.desktop ++ [ pkgs.makeWrapper ];
|
nativeBuildInputs = [
|
||||||
|
pkgs.pkg-config
|
||||||
|
pkgs.cargo-about
|
||||||
|
pkgs.removeReferencesTo
|
||||||
|
];
|
||||||
env = deps.cef.env // {
|
env = deps.cef.env // {
|
||||||
CARGO_PROFILE = if dev then "dev" else "release";
|
CARGO_PROFILE = if dev then "dev" else "release";
|
||||||
};
|
};
|
||||||
cargoExtraArgs = "-p graphite-desktop${
|
cargoExtraArgs = "-p graphite-desktop";
|
||||||
if embeddedResources then "" else " --no-default-features --features recommended"
|
|
||||||
}";
|
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
@@ -97,24 +114,34 @@ deps.crane.lib.buildPackage (
|
|||||||
// {
|
// {
|
||||||
cargoArtifacts = deps.crane.lib.buildDepsOnly common;
|
cargoArtifacts = deps.crane.lib.buildDepsOnly common;
|
||||||
|
|
||||||
env =
|
env = common.env // {
|
||||||
common.env
|
RASTER_NODES_SHADER_PATH = self.packages.${system}.graphite-raster-nodes-shaders;
|
||||||
// {
|
EMBEDDED_RESOURCES = resources;
|
||||||
RASTER_NODES_SHADER_PATH = pkgs.raster-nodes-shaders;
|
GRAPHITE_GIT_COMMIT_HASH = self.rev or "unknown";
|
||||||
}
|
GRAPHITE_GIT_COMMIT_DATE = self.lastModified or "unknown";
|
||||||
// (
|
};
|
||||||
if embeddedResources then
|
|
||||||
{
|
|
||||||
EMBEDDED_RESOURCES = resources;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{ }
|
|
||||||
);
|
|
||||||
|
|
||||||
postUnpack = ''
|
npmDeps = pkgs.importNpmLock {
|
||||||
mkdir ./branding
|
npmRoot = "${info.src}/frontend";
|
||||||
cp -r ${branding}/* ./branding
|
};
|
||||||
'';
|
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 = ''
|
installPhase = ''
|
||||||
mkdir -p $out/bin
|
mkdir -p $out/bin
|
||||||
@@ -124,13 +151,22 @@ deps.crane.lib.buildPackage (
|
|||||||
cp $src/desktop/assets/*.desktop $out/share/applications/
|
cp $src/desktop/assets/*.desktop $out/share/applications/
|
||||||
|
|
||||||
mkdir -p $out/share/icons/hicolor/scalable/apps
|
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 = ''
|
postFixup = ''
|
||||||
wrapProgram "$out/bin/graphite" \
|
remove-references-to -t "${cargoVendorDir}" $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}"
|
patchelf \
|
||||||
|
--set-rpath "${pkgs.lib.makeLibraryPath libs}:${deps.cef.env.CEF_PATH}" \
|
||||||
|
--add-needed libGL.so \
|
||||||
|
$out/bin/graphite
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
info,
|
||||||
|
deps,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
cargoVendorDir = deps.crane.lib.vendorCargoDeps { inherit (info) src; };
|
||||||
|
common = {
|
||||||
|
pname = "third-party-licenses";
|
||||||
|
inherit (info) version src;
|
||||||
|
inherit cargoVendorDir;
|
||||||
|
nativeBuildInputs = [ pkgs.pkg-config ];
|
||||||
|
buildInputs = [ pkgs.openssl ];
|
||||||
|
strictDeps = true;
|
||||||
|
env = deps.cef.env // {
|
||||||
|
CARGO_PROFILE = "dev";
|
||||||
|
};
|
||||||
|
cargoExtraArgs = "-p third-party-licenses --features desktop";
|
||||||
|
doCheck = false;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
deps.crane.lib.buildPackage (
|
||||||
|
common
|
||||||
|
// {
|
||||||
|
inherit cargoVendorDir;
|
||||||
|
cargoArtifacts = deps.crane.lib.buildDepsOnly common;
|
||||||
|
meta.mainProgram = "third-party-licenses";
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# This is a helper file for people using NixOS as their operating system.
|
|
||||||
# If you don't know what this file does, you can safely ignore it.
|
|
||||||
|
|
||||||
# If you are using Nix as your package manager, you can run 'nix-shell .nix'
|
|
||||||
# in the root directory of the project and Nix will open a bash shell
|
|
||||||
# with all the packages needed to build and run Graphite installed.
|
|
||||||
# A shell.nix file is used in the Nix ecosystem to define a development
|
|
||||||
# environment with specific dependencies. When you enter a Nix shell using
|
|
||||||
# this file, it ensures that all the specified tools and libraries are
|
|
||||||
# available regardless of the host system's configuration. This provides
|
|
||||||
# a reproducible development environment across different machines and developers.
|
|
||||||
|
|
||||||
# You can enter the Nix shell and run Graphite like normal with:
|
|
||||||
# > npm start
|
|
||||||
# Or you can run it like this without needing to first enter the Nix shell:
|
|
||||||
# > nix-shell .nix --command "npm start"
|
|
||||||
|
|
||||||
# Uses flake compat to provide a development shell that is identical to the one defined in the flake
|
|
||||||
(import (
|
|
||||||
let
|
|
||||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
|
||||||
nodeName = lock.nodes.root.inputs.flake-compat;
|
|
||||||
in
|
|
||||||
fetchTarball {
|
|
||||||
url = lock.nodes.${nodeName}.locked.url;
|
|
||||||
sha256 = lock.nodes.${nodeName}.locked.narHash;
|
|
||||||
}
|
|
||||||
) { src = ./.; }).shellNix
|
|
||||||
Vendored
+2
-1
@@ -11,9 +11,10 @@
|
|||||||
// Code quality
|
// Code quality
|
||||||
"wayou.vscode-todo-highlight",
|
"wayou.vscode-todo-highlight",
|
||||||
"streetsidesoftware.code-spell-checker",
|
"streetsidesoftware.code-spell-checker",
|
||||||
// Helpful
|
// Git
|
||||||
"mhutchie.git-graph",
|
"mhutchie.git-graph",
|
||||||
"qezhu.gitlink",
|
"qezhu.gitlink",
|
||||||
|
// Helpful
|
||||||
"wmaurer.change-case"
|
"wmaurer.change-case"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+30
-2
@@ -26,12 +26,17 @@
|
|||||||
// Configured in `.prettierrc`
|
// Configured in `.prettierrc`
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"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
|
// Handlebars: don't save on format
|
||||||
// (`about.hbs` is used by Cargo About to encode license information)
|
// (`about.hbs` is used by Cargo About to encode license information)
|
||||||
"[handlebars]": {
|
"[handlebars]": {
|
||||||
"editor.formatOnSave": false
|
"editor.formatOnSave": false
|
||||||
},
|
},
|
||||||
// Rust Analyzer config
|
// Rust Analyzer config
|
||||||
|
"rust-analyzer.check.command": "clippy",
|
||||||
"rust-analyzer.cargo.allTargets": false,
|
"rust-analyzer.cargo.allTargets": false,
|
||||||
"rust-analyzer.procMacro.ignored": {
|
"rust-analyzer.procMacro.ignored": {
|
||||||
"serde_derive": ["Serialize", "Deserialize"],
|
"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`
|
"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-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-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
|
// VS Code config
|
||||||
"html.format.wrapLineLength": 200,
|
"html.format.wrapLineLength": 200,
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"files.insertFinalNewline": true,
|
"files.insertFinalNewline": true,
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.graphite": "json"
|
"*.graphite": "json"
|
||||||
}
|
},
|
||||||
|
"editor.renderWhitespace": "boundary",
|
||||||
|
"editor.minimap.markSectionHeaderRegex": "// ===+\\n\\s*//\\s*(?<label>[^\\n]{1,18})[^\\n]*(\\n\\s*//[^\\n]*)*\\n\\s*// ===+"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+717
-556
File diff suppressed because it is too large
Load Diff
+29
-57
@@ -12,26 +12,10 @@ members = [
|
|||||||
"libraries/dyn-any",
|
"libraries/dyn-any",
|
||||||
"libraries/path-bool",
|
"libraries/path-bool",
|
||||||
"libraries/math-parser",
|
"libraries/math-parser",
|
||||||
"node-graph/libraries/application-io",
|
"node-graph/libraries/*",
|
||||||
"node-graph/libraries/core-types",
|
"node-graph/nodes/*",
|
||||||
"node-graph/libraries/no-std-types",
|
|
||||||
"node-graph/libraries/raster-types",
|
|
||||||
"node-graph/libraries/vector-types",
|
|
||||||
"node-graph/libraries/graphic-types",
|
|
||||||
"node-graph/libraries/rendering",
|
|
||||||
"node-graph/libraries/wgpu-executor",
|
|
||||||
"node-graph/nodes/blending",
|
|
||||||
"node-graph/nodes/brush",
|
|
||||||
"node-graph/nodes/gcore",
|
|
||||||
"node-graph/nodes/graphic",
|
|
||||||
"node-graph/nodes/math",
|
|
||||||
"node-graph/nodes/path-bool",
|
|
||||||
"node-graph/nodes/raster",
|
|
||||||
"node-graph/nodes/raster/shaders",
|
"node-graph/nodes/raster/shaders",
|
||||||
"node-graph/nodes/raster/shaders/entrypoint",
|
"node-graph/nodes/raster/shaders/entrypoint",
|
||||||
"node-graph/nodes/text",
|
|
||||||
"node-graph/nodes/transform",
|
|
||||||
"node-graph/nodes/vector",
|
|
||||||
"node-graph/graph-craft",
|
"node-graph/graph-craft",
|
||||||
"node-graph/graphene-cli",
|
"node-graph/graphene-cli",
|
||||||
"node-graph/nodes/gstd",
|
"node-graph/nodes/gstd",
|
||||||
@@ -39,7 +23,11 @@ members = [
|
|||||||
"node-graph/node-macro",
|
"node-graph/node-macro",
|
||||||
"node-graph/preprocessor",
|
"node-graph/preprocessor",
|
||||||
"proc-macros",
|
"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 = [
|
default-members = [
|
||||||
"editor",
|
"editor",
|
||||||
@@ -47,33 +35,13 @@ default-members = [
|
|||||||
"libraries/dyn-any",
|
"libraries/dyn-any",
|
||||||
"libraries/path-bool",
|
"libraries/path-bool",
|
||||||
"libraries/math-parser",
|
"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/graph-craft",
|
||||||
"node-graph/graphene-cli",
|
|
||||||
"node-graph/nodes/gstd",
|
|
||||||
"node-graph/interpreted-executor",
|
"node-graph/interpreted-executor",
|
||||||
"node-graph/node-macro",
|
"node-graph/node-macro",
|
||||||
"node-graph/preprocessor",
|
"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",
|
# "proc-macros",
|
||||||
|
"tools/cargo-run",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
@@ -114,6 +82,7 @@ graphic-nodes = { path = "node-graph/nodes/graphic" }
|
|||||||
text-nodes = { path = "node-graph/nodes/text" }
|
text-nodes = { path = "node-graph/nodes/text" }
|
||||||
transform-nodes = { path = "node-graph/nodes/transform" }
|
transform-nodes = { path = "node-graph/nodes/transform" }
|
||||||
vector-nodes = { path = "node-graph/nodes/vector" }
|
vector-nodes = { path = "node-graph/nodes/vector" }
|
||||||
|
repeat-nodes = { path = "node-graph/nodes/repeat" }
|
||||||
math-nodes = { path = "node-graph/nodes/math" }
|
math-nodes = { path = "node-graph/nodes/math" }
|
||||||
path-bool-nodes = { path = "node-graph/nodes/path-bool" }
|
path-bool-nodes = { path = "node-graph/nodes/path-bool" }
|
||||||
graph-craft = { path = "node-graph/graph-craft" }
|
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 = { version = "1.0", features = ["derive", "rc"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde-wasm-bindgen = "0.6"
|
serde-wasm-bindgen = "0.6"
|
||||||
reqwest = { version = "0.12", features = ["blocking", "rustls-tls", "json"] }
|
reqwest = { version = "0.13", features = ["blocking", "json"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
bitflags = { version = "2.4", features = ["serde"] }
|
bitflags = { version = "2.4", features = ["serde"] }
|
||||||
ctor = "0.2"
|
ctor = "0.2"
|
||||||
convert_case = "0.8"
|
convert_case = "0.8"
|
||||||
|
indoc = "2.0.5"
|
||||||
derivative = "2.2"
|
derivative = "2.2"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
proc-macro2 = { version = "1", features = ["span-locations"] }
|
proc-macro2 = { version = "1", features = ["span-locations"] }
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
ron = "0.11"
|
ron = "0.12"
|
||||||
fastnoise-lite = "1.1"
|
fastnoise-lite = "1.1"
|
||||||
wgpu = { version = "27.0", features = [
|
wgpu = { version = "27.0", features = [
|
||||||
# We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm
|
# We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm
|
||||||
@@ -151,7 +121,7 @@ wgpu = { version = "27.0", features = [
|
|||||||
"spirv",
|
"spirv",
|
||||||
"strict_asserts",
|
"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 = "=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"
|
wasm-bindgen-futures = "0.4"
|
||||||
js-sys = "=0.3.77"
|
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" }
|
winit = { git = "https://github.com/rust-windowing/winit.git" }
|
||||||
keyboard-types = "0.8"
|
keyboard-types = "0.8"
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt"] }
|
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt", "rt-multi-thread"] }
|
||||||
vello = { git = "https://github.com/linebender/vello" }
|
# Linebender ecosystem (BEGIN)
|
||||||
vello_encoding = { git = "https://github.com/linebender/vello" }
|
kurbo = { version = "0.13", features = ["serde"] }
|
||||||
resvg = "0.45"
|
vello = "0.7"
|
||||||
usvg = "0.45"
|
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 = { version = "0.9", default-features = false, features = ["std_rng"] }
|
||||||
rand_chacha = "0.9"
|
rand_chacha = "0.9"
|
||||||
glam = { version = "0.29", default-features = false, features = [
|
glam = { version = "0.29", default-features = false, features = [
|
||||||
@@ -194,8 +170,6 @@ image = { version = "0.25", default-features = false, features = [
|
|||||||
"jpeg",
|
"jpeg",
|
||||||
"bmp",
|
"bmp",
|
||||||
] }
|
] }
|
||||||
parley = "0.6"
|
|
||||||
skrifa = "0.36"
|
|
||||||
pretty_assertions = "1.4"
|
pretty_assertions = "1.4"
|
||||||
fern = { version = "0.7", features = ["colored"] }
|
fern = { version = "0.7", features = ["colored"] }
|
||||||
num_enum = { version = "0.7", default-features = false }
|
num_enum = { version = "0.7", default-features = false }
|
||||||
@@ -217,7 +191,6 @@ syn = { version = "2.0", default-features = false, features = [
|
|||||||
"extra-traits",
|
"extra-traits",
|
||||||
"proc-macro",
|
"proc-macro",
|
||||||
] }
|
] }
|
||||||
kurbo = { version = "0.12", features = ["serde"] }
|
|
||||||
lyon_geom = "1.0"
|
lyon_geom = "1.0"
|
||||||
petgraph = { version = "0.7", default-features = false, features = ["graphmap"] }
|
petgraph = { version = "0.7", default-features = false, features = ["graphmap"] }
|
||||||
half = { version = "2.4", default-features = false, features = ["bytemuck"] }
|
half = { version = "2.4", default-features = false, features = ["bytemuck"] }
|
||||||
@@ -234,11 +207,13 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
rfd = "0.15"
|
rfd = "0.15"
|
||||||
open = "5.3"
|
open = "5.3"
|
||||||
polycool = "0.4"
|
|
||||||
spin = "0.10"
|
spin = "0.10"
|
||||||
clap = "4.5"
|
clap = "4.5"
|
||||||
spirv-std = { git = "https://github.com/Firestar99/rust-gpu-new", rev = "c12f216121820580731440ee79ebc7403d6ea04f", features = ["bytemuck"] }
|
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 }
|
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]
|
[workspace.lints.rust]
|
||||||
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(target_arch, values("spirv"))'] }
|
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(target_arch, values("spirv"))'] }
|
||||||
@@ -262,10 +237,7 @@ node-macro = { opt-level = 2 }
|
|||||||
lto = "thin"
|
lto = "thin"
|
||||||
debug = true
|
debug = true
|
||||||
|
|
||||||
[profile.profiling]
|
|
||||||
inherits = "release"
|
|
||||||
debug = true
|
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Force cargo to use only one version of the dpi crate (vendoring breaks without this)
|
# Force cargo to use only one version of the dpi crate (vendoring breaks without this)
|
||||||
dpi = { git = "https://github.com/rust-windowing/winit.git" }
|
dpi = { git = "https://github.com/rust-windowing/winit.git" }
|
||||||
|
download-cef = { git = "https://github.com/timon-schelling/cef-rs.git", branch = "graphite" }
|
||||||
|
|||||||
@@ -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}}
|
|
||||||
]
|
|
||||||
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
+10
-4
@@ -25,9 +25,13 @@ graphite-desktop-wrapper = { path = "wrapper" }
|
|||||||
graphite-desktop-embedded-resources = { path = "embedded-resources", optional = true }
|
graphite-desktop-embedded-resources = { path = "embedded-resources", optional = true }
|
||||||
|
|
||||||
wgpu = { workspace = 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 }
|
thiserror = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
cef = { workspace = true }
|
cef = { workspace = true }
|
||||||
cef-dll-sys = { workspace = true }
|
cef-dll-sys = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
@@ -40,10 +44,11 @@ vello = { workspace = true }
|
|||||||
derivative = { workspace = true }
|
derivative = { workspace = true }
|
||||||
rfd = { workspace = true }
|
rfd = { workspace = true }
|
||||||
open = { workspace = true }
|
open = { workspace = true }
|
||||||
rand = { workspace = true, features = ["thread_rng"] }
|
lzma-rust2 = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
rand = { workspace = true, features = ["thread_rng"] }
|
||||||
clap = { workspace = true, features = ["derive"] }
|
clap = { workspace = true, features = ["derive"] }
|
||||||
pidlock = "0.2.2"
|
fd-lock = "4.0.4"
|
||||||
ctrlc = "3.5.1"
|
ctrlc = "3.5.1"
|
||||||
window_clipboard = "0.5"
|
window_clipboard = "0.5"
|
||||||
|
|
||||||
@@ -55,6 +60,7 @@ windows = { version = "0.58.0", features = [
|
|||||||
"Win32_Graphics_Gdi",
|
"Win32_Graphics_Gdi",
|
||||||
"Win32_System_LibraryLoader",
|
"Win32_System_LibraryLoader",
|
||||||
"Win32_System_Com",
|
"Win32_System_Com",
|
||||||
|
"Win32_System_Console",
|
||||||
"Win32_UI_Controls",
|
"Win32_UI_Controls",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
"Win32_UI_HiDpi",
|
"Win32_UI_HiDpi",
|
||||||
@@ -66,4 +72,4 @@ windows = { version = "0.58.0", features = [
|
|||||||
objc2 = { version = "0.6.1", default-features = false }
|
objc2 = { version = "0.6.1", default-features = false }
|
||||||
objc2-foundation = { version = "0.3.2", default-features = false }
|
objc2-foundation = { version = "0.3.2", default-features = false }
|
||||||
objc2-app-kit = { version = "0.3.2", default-features = false }
|
objc2-app-kit = { version = "0.3.2", default-features = false }
|
||||||
muda = { git = "https://github.com/tauri-apps/muda.git", rev = "3f460b8fbaed59cda6d95ceea6904f000f093f15", default-features = false }
|
muda = { git = "https://github.com/timon-schelling/muda.git", rev = "e5bc28bbd6781b18afbfc237981f9ef47eddf863", default-features = false }
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Comment=Open-source vector & raster graphics editor. Featuring node based proced
|
|||||||
Exec=graphite
|
Exec=graphite
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Icon=graphite
|
Icon=art.graphite.Graphite
|
||||||
Categories=Graphics;VectorGraphics;RasterGraphics;
|
Categories=Graphics;VectorGraphics;RasterGraphics;
|
||||||
Keywords=graphite;editor;vector;raster;procedural;design;
|
Keywords=graphite;editor;vector;raster;procedural;design;
|
||||||
StartupWMClass=art.graphite.Graphite
|
StartupWMClass=art.graphite.Graphite
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![cfg_attr(target_os = "linux", allow(unused))] // TODO: Remove this when bundling for linux is implemented
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
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>> {
|
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_name()];
|
||||||
let mut args = vec!["build", "--package", package, "--profile", profile];
|
|
||||||
if let Some(bin) = bin {
|
if let Some(bin) = bin {
|
||||||
args.push("--bin");
|
args.push("--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>> {
|
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()?;
|
let status = Command::new(program).args(args).stdout(Stdio::inherit()).stderr(Stdio::inherit()).status()?;
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
std::process::exit(1);
|
return Err(format!("Command '{}' with args {:?} failed with status: {}", program, args, status).into());
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
use std::error::Error;
|
|
||||||
|
|
||||||
use crate::common::*;
|
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)?;
|
let app_bin = build_bin("graphite-desktop-platform-linux", None)?;
|
||||||
|
|
||||||
// TODO: Implement bundling for linux
|
// TODO: Implement bundling for linux
|
||||||
|
|
||||||
// TODO: Consider adding more useful cli
|
// TODO: Consider adding more useful cli
|
||||||
if std::env::args().any(|a| a == "open") {
|
let args: Vec<String> = std::env::args().collect();
|
||||||
run_command(&app_bin.to_string_lossy(), &[]).expect("failed to open app");
|
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 {
|
} 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!("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`");
|
eprintln!("You can still start the app with the `open` subcommand. `cargo run -p graphite-desktop-bundle -- open`");
|
||||||
std::process::exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ pub fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let app_dir = bundle(&profile_path, &app_bin, &helper_bin);
|
let app_dir = bundle(&profile_path, &app_bin, &helper_bin);
|
||||||
|
|
||||||
// TODO: Consider adding more useful cli
|
// TODO: Consider adding more useful cli
|
||||||
if std::env::args().any(|a| a == "open") {
|
let args: Vec<String> = std::env::args().collect();
|
||||||
let executable_path = app_dir.join(EXEC_PATH).join(APP_NAME);
|
if let Some(pos) = args.iter().position(|a| a == "open") {
|
||||||
run_command(&executable_path.to_string_lossy(), &[]).expect("failed to open app");
|
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(())
|
Ok(())
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ pub fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let executable = bundle(&profile_path(), &app_bin);
|
let executable = bundle(&profile_path(), &app_bin);
|
||||||
|
|
||||||
// TODO: Consider adding more useful cli
|
// TODO: Consider adding more useful cli
|
||||||
if std::env::args().any(|a| a == "open") {
|
let args: Vec<String> = std::env::args().collect();
|
||||||
let executable_path = executable.to_string_lossy();
|
if let Some(pos) = args.iter().position(|a| a == "open") {
|
||||||
run_command(&executable_path, &[]).expect("failed to open app")
|
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(())
|
Ok(())
|
||||||
@@ -27,8 +28,33 @@ fn bundle(out_dir: &Path, app_bin: &Path) -> PathBuf {
|
|||||||
|
|
||||||
copy_dir(&cef_path(), &app_dir);
|
copy_dir(&cef_path(), &app_dir);
|
||||||
|
|
||||||
|
if let Err(e) = remove_unnecessary_cef_files(&app_dir) {
|
||||||
|
eprintln!("Failed to remove unnecessary CEF files: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
let bin_path = app_dir.join(EXECUTABLE);
|
let bin_path = app_dir.join(EXECUTABLE);
|
||||||
fs::copy(app_bin, &bin_path).unwrap();
|
fs::copy(app_bin, &bin_path).unwrap();
|
||||||
|
|
||||||
bin_path
|
bin_path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_unnecessary_cef_files(app_dir: &Path) -> Result<(), Box<dyn Error>> {
|
||||||
|
fs::remove_dir_all(app_dir.join("cmake"))?;
|
||||||
|
fs::remove_dir_all(app_dir.join("include"))?;
|
||||||
|
fs::remove_dir_all(app_dir.join("libcef_dll"))?;
|
||||||
|
|
||||||
|
for entry in fs::read_dir(app_dir.join("locales"))? {
|
||||||
|
let path = entry?.path();
|
||||||
|
if path.is_file() && path.file_name() != Some("en-US.pak".as_ref()) {
|
||||||
|
fs::remove_file(path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::remove_file(app_dir.join("archive.json"))?;
|
||||||
|
fs::remove_file(app_dir.join("CMakeLists.txt"))?;
|
||||||
|
fs::remove_file(app_dir.join("bootstrapc.exe"))?;
|
||||||
|
fs::remove_file(app_dir.join("bootstrap.exe"))?;
|
||||||
|
fs::remove_file(app_dir.join("libcef.lib"))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ fn main() {
|
|||||||
res.set("FileDescription", "Graphite");
|
res.set("FileDescription", "Graphite");
|
||||||
res.set("ProductName", "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.set("CompanyName", "Graphite Labs, LLC");
|
||||||
|
|
||||||
res.compile().expect("Failed to compile Windows resources");
|
res.compile().expect("Failed to compile Windows resources");
|
||||||
|
|||||||
+161
-47
@@ -1,22 +1,27 @@
|
|||||||
|
use rand::Rng;
|
||||||
use rfd::AsyncFileDialog;
|
use rfd::AsyncFileDialog;
|
||||||
use std::fs;
|
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::sync::mpsc::{Receiver, Sender, SyncSender};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
use winit::dpi::PhysicalSize;
|
use winit::dpi::{PhysicalPosition, PhysicalSize};
|
||||||
use winit::event::{ButtonSource, ElementState, MouseButton, WindowEvent};
|
use winit::event::{ButtonSource, ElementState, MouseButton, StartCause, WindowEvent};
|
||||||
use winit::event_loop::{ActiveEventLoop, ControlFlow};
|
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||||
use winit::window::WindowId;
|
use winit::window::WindowId;
|
||||||
|
|
||||||
use crate::cef;
|
use crate::cef;
|
||||||
|
use crate::cli::Cli;
|
||||||
use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS;
|
use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS;
|
||||||
use crate::event::{AppEvent, AppEventScheduler};
|
use crate::event::{AppEvent, AppEventScheduler};
|
||||||
use crate::persist::PersistentData;
|
use crate::persist::PersistentData;
|
||||||
|
use crate::preferences;
|
||||||
use crate::render::{RenderError, RenderState};
|
use crate::render::{RenderError, RenderState};
|
||||||
use crate::window::Window;
|
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};
|
use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
||||||
|
|
||||||
pub(crate) struct App {
|
pub(crate) struct App {
|
||||||
@@ -27,6 +32,8 @@ pub(crate) struct App {
|
|||||||
window_size: PhysicalSize<u32>,
|
window_size: PhysicalSize<u32>,
|
||||||
window_maximized: bool,
|
window_maximized: bool,
|
||||||
window_fullscreen: bool,
|
window_fullscreen: bool,
|
||||||
|
pointer_position: PhysicalPosition<f64>,
|
||||||
|
pointer_lock_position: Option<PhysicalPosition<f64>>,
|
||||||
ui_scale: f64,
|
ui_scale: f64,
|
||||||
app_event_receiver: Receiver<AppEvent>,
|
app_event_receiver: Receiver<AppEvent>,
|
||||||
app_event_scheduler: AppEventScheduler,
|
app_event_scheduler: AppEventScheduler,
|
||||||
@@ -34,13 +41,15 @@ pub(crate) struct App {
|
|||||||
cef_context: Box<dyn cef::CefContext>,
|
cef_context: Box<dyn cef::CefContext>,
|
||||||
cef_schedule: Option<Instant>,
|
cef_schedule: Option<Instant>,
|
||||||
cef_view_info_sender: Sender<cef::ViewInfoUpdate>,
|
cef_view_info_sender: Sender<cef::ViewInfoUpdate>,
|
||||||
last_ui_update: Instant,
|
cef_init_successful: bool,
|
||||||
avg_frame_time: f32,
|
|
||||||
start_render_sender: SyncSender<()>,
|
start_render_sender: SyncSender<()>,
|
||||||
web_communication_initialized: bool,
|
web_communication_initialized: bool,
|
||||||
web_communication_startup_buffer: Vec<Vec<u8>>,
|
web_communication_startup_buffer: Vec<Vec<u8>>,
|
||||||
persistent_data: PersistentData,
|
persistent_data: PersistentData,
|
||||||
launch_documents: Vec<PathBuf>,
|
cli: Cli,
|
||||||
|
startup_time: Option<Instant>,
|
||||||
|
exiting: Arc<AtomicBool>,
|
||||||
|
exit_reason: ExitReason,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@@ -54,28 +63,37 @@ impl App {
|
|||||||
wgpu_context: WgpuContext,
|
wgpu_context: WgpuContext,
|
||||||
app_event_receiver: Receiver<AppEvent>,
|
app_event_receiver: Receiver<AppEvent>,
|
||||||
app_event_scheduler: AppEventScheduler,
|
app_event_scheduler: AppEventScheduler,
|
||||||
launch_documents: Vec<PathBuf>,
|
cli: Cli,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let ctrlc_app_event_scheduler = app_event_scheduler.clone();
|
let ctrlc_app_event_scheduler = app_event_scheduler.clone();
|
||||||
ctrlc::set_handler(move || {
|
ctrlc::set_handler(move || {
|
||||||
tracing::info!("Termination signal received, exiting...");
|
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");
|
.expect("Error setting Ctrl-C handler");
|
||||||
|
|
||||||
|
let exiting = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
let rendering_app_event_scheduler = app_event_scheduler.clone();
|
let rendering_app_event_scheduler = app_event_scheduler.clone();
|
||||||
let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1);
|
let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1);
|
||||||
|
let exiting_clone = exiting.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
|
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||||
loop {
|
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));
|
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();
|
let mut persistent_data = PersistentData::default();
|
||||||
persistent_data.load_from_disk();
|
persistent_data.load_from_disk();
|
||||||
|
|
||||||
|
let desktop_wrapper = DesktopWrapper::new(rand::rng().random());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
render_state: None,
|
render_state: None,
|
||||||
wgpu_context,
|
wgpu_context,
|
||||||
@@ -84,23 +102,43 @@ impl App {
|
|||||||
window_size: PhysicalSize { width: 0, height: 0 },
|
window_size: PhysicalSize { width: 0, height: 0 },
|
||||||
window_maximized: false,
|
window_maximized: false,
|
||||||
window_fullscreen: false,
|
window_fullscreen: false,
|
||||||
|
pointer_position: Default::default(),
|
||||||
|
pointer_lock_position: Default::default(),
|
||||||
ui_scale: 1.,
|
ui_scale: 1.,
|
||||||
app_event_receiver,
|
app_event_receiver,
|
||||||
app_event_scheduler,
|
app_event_scheduler,
|
||||||
desktop_wrapper: DesktopWrapper::new(),
|
desktop_wrapper,
|
||||||
last_ui_update: Instant::now(),
|
|
||||||
cef_context,
|
cef_context,
|
||||||
cef_schedule: Some(Instant::now()),
|
cef_schedule: Some(Instant::now()),
|
||||||
cef_view_info_sender,
|
cef_view_info_sender,
|
||||||
avg_frame_time: 0.,
|
cef_init_successful: false,
|
||||||
start_render_sender,
|
start_render_sender,
|
||||||
web_communication_initialized: false,
|
web_communication_initialized: false,
|
||||||
web_communication_startup_buffer: Vec::new(),
|
web_communication_startup_buffer: Vec::new(),
|
||||||
persistent_data,
|
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) {
|
fn resize(&mut self) {
|
||||||
let Some(window) = &self.window else {
|
let Some(window) = &self.window else {
|
||||||
tracing::error!("Resize failed due to missing window");
|
tracing::error!("Resize failed due to missing window");
|
||||||
@@ -173,7 +211,7 @@ impl App {
|
|||||||
if let Some(path) = futures::executor::block_on(show_dialog)
|
if let Some(path) = futures::executor::block_on(show_dialog)
|
||||||
&& let Ok(content) = fs::read(&path)
|
&& 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));
|
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -238,6 +276,9 @@ impl App {
|
|||||||
if let Some(render_state) = &mut self.render_state {
|
if let Some(render_state) = &mut self.render_state {
|
||||||
render_state.set_overlays_scene(scene);
|
render_state.set_overlays_scene(scene);
|
||||||
}
|
}
|
||||||
|
if let Some(window) = &self.window {
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DesktopFrontendMessage::PersistenceWriteDocument { id, document } => {
|
DesktopFrontendMessage::PersistenceWriteDocument { id, document } => {
|
||||||
self.persistent_data.write_document(id, document);
|
self.persistent_data.write_document(id, document);
|
||||||
@@ -252,10 +293,10 @@ impl App {
|
|||||||
self.persistent_data.set_document_order(ids);
|
self.persistent_data.set_document_order(ids);
|
||||||
}
|
}
|
||||||
DesktopFrontendMessage::PersistenceWritePreferences { preferences } => {
|
DesktopFrontendMessage::PersistenceWritePreferences { preferences } => {
|
||||||
self.persistent_data.write_preferences(preferences);
|
preferences::write(preferences);
|
||||||
}
|
}
|
||||||
DesktopFrontendMessage::PersistenceLoadPreferences => {
|
DesktopFrontendMessage::PersistenceLoadPreferences => {
|
||||||
let preferences = self.persistent_data.load_preferences();
|
let preferences = preferences::read();
|
||||||
let message = DesktopWrapperMessage::LoadPreferences { preferences };
|
let message = DesktopWrapperMessage::LoadPreferences { preferences };
|
||||||
responses.push(message);
|
responses.push(message);
|
||||||
}
|
}
|
||||||
@@ -295,11 +336,11 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
DesktopFrontendMessage::OpenLaunchDocuments => {
|
DesktopFrontendMessage::OpenLaunchDocuments => {
|
||||||
if self.launch_documents.is_empty() {
|
if self.cli.files.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let app_event_scheduler = self.app_event_scheduler.clone();
|
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 || {
|
let _ = thread::spawn(move || {
|
||||||
for path in launch_documents {
|
for path in launch_documents {
|
||||||
tracing::info!("Opening file from command line: {}", path.display());
|
tracing::info!("Opening file from command line: {}", path.display());
|
||||||
@@ -329,8 +370,14 @@ impl App {
|
|||||||
window.clipboard_write(content);
|
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 => {
|
DesktopFrontendMessage::WindowClose => {
|
||||||
self.app_event_scheduler.schedule(AppEvent::CloseWindow);
|
self.app_event_scheduler.schedule(AppEvent::Exit);
|
||||||
}
|
}
|
||||||
DesktopFrontendMessage::WindowMinimize => {
|
DesktopFrontendMessage::WindowMinimize => {
|
||||||
if let Some(window) = &self.window {
|
if let Some(window) = &self.window {
|
||||||
@@ -342,6 +389,11 @@ impl App {
|
|||||||
window.toggle_maximize();
|
window.toggle_maximize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DesktopFrontendMessage::WindowFullscreen => {
|
||||||
|
if let Some(window) = &mut self.window {
|
||||||
|
window.toggle_fullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
DesktopFrontendMessage::WindowDrag => {
|
DesktopFrontendMessage::WindowDrag => {
|
||||||
if let Some(window) = &self.window {
|
if let Some(window) = &self.window {
|
||||||
window.start_drag();
|
window.start_drag();
|
||||||
@@ -362,6 +414,21 @@ impl App {
|
|||||||
window.show_all();
|
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) => {
|
AppEvent::UiUpdate(texture) => {
|
||||||
if let Some(render_state) = self.render_state.as_mut() {
|
if let Some(render_state) = self.render_state.as_mut() {
|
||||||
render_state.bind_ui_texture(texture);
|
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 {
|
if let Some(window) = &self.window {
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
if !self.cef_init_successful {
|
||||||
|
self.cef_init_successful = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AppEvent::ScheduleBrowserWork(instant) => {
|
AppEvent::ScheduleBrowserWork(instant) => {
|
||||||
if instant <= Instant::now() {
|
if instant <= Instant::now() {
|
||||||
@@ -435,9 +500,7 @@ impl App {
|
|||||||
window.set_cursor(event_loop, cursor);
|
window.set_cursor(event_loop, cursor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppEvent::CloseWindow => {
|
AppEvent::Exit => {
|
||||||
// TODO: Implement graceful shutdown
|
|
||||||
|
|
||||||
tracing::info!("Exiting main event loop");
|
tracing::info!("Exiting main event loop");
|
||||||
event_loop.exit();
|
event_loop.exit();
|
||||||
}
|
}
|
||||||
@@ -464,13 +527,7 @@ impl ApplicationHandler for App {
|
|||||||
|
|
||||||
self.desktop_wrapper.init(self.wgpu_context.clone());
|
self.desktop_wrapper.init(self.wgpu_context.clone());
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
self.startup_time = Some(Instant::now());
|
||||||
let platform = Platform::Windows;
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
let platform = Platform::Mac;
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
let platform = Platform::Linux;
|
|
||||||
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::UpdatePlatform(platform));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) {
|
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);
|
self.cef_context.handle_window_event(&event);
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
WindowEvent::CloseRequested => {
|
WindowEvent::CloseRequested => {
|
||||||
self.app_event_scheduler.schedule(AppEvent::CloseWindow);
|
self.app_event_scheduler.schedule(AppEvent::Exit);
|
||||||
}
|
}
|
||||||
WindowEvent::SurfaceResized(_) | WindowEvent::ScaleFactorChanged { .. } => {
|
WindowEvent::SurfaceResized(_) | WindowEvent::ScaleFactorChanged { .. } => {
|
||||||
self.resize();
|
self.resize();
|
||||||
@@ -509,18 +586,28 @@ impl ApplicationHandler for App {
|
|||||||
}
|
}
|
||||||
Err(RenderError::SurfaceError(wgpu::SurfaceError::OutOfMemory)) => {
|
Err(RenderError::SurfaceError(wgpu::SurfaceError::OutOfMemory)) => {
|
||||||
tracing::error!("GPU out of memory");
|
tracing::error!("GPU out of memory");
|
||||||
event_loop.exit();
|
self.exit(None);
|
||||||
}
|
}
|
||||||
Err(RenderError::SurfaceError(e)) => tracing::error!("Render error: {:?}", e),
|
Err(RenderError::SurfaceError(e)) => tracing::error!("Render error: {:?}", e),
|
||||||
}
|
}
|
||||||
let _ = self.start_render_sender.try_send(());
|
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, .. } => {
|
WindowEvent::DragDropped { paths, .. } => {
|
||||||
for path in paths {
|
for path in paths {
|
||||||
match fs::read(&path) {
|
match fs::read(&path) {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
let message = DesktopWrapperMessage::OpenFile { path, content };
|
let message = DesktopWrapperMessage::ImportFile { path, content };
|
||||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -556,6 +643,13 @@ impl ApplicationHandler for App {
|
|||||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
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();
|
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) {
|
fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) {
|
||||||
// Set a timeout in case we miss any cef schedule requests
|
// Set a timeout in case we miss any cef schedule requests
|
||||||
let timeout = Instant::now() + Duration::from_millis(10);
|
let mut wait_until = Instant::now() + Duration::from_millis(10);
|
||||||
let wait_until = timeout.min(self.cef_schedule.unwrap_or(timeout));
|
|
||||||
if let Some(schedule) = self.cef_schedule
|
if let Some(schedule) = self.cef_schedule
|
||||||
&& schedule < Instant::now()
|
&& schedule < Instant::now()
|
||||||
{
|
{
|
||||||
@@ -575,11 +685,15 @@ impl ApplicationHandler for App {
|
|||||||
for _ in 0..CEF_MESSAGE_LOOP_MAX_ITERATIONS {
|
for _ in 0..CEF_MESSAGE_LOOP_MAX_ITERATIONS {
|
||||||
self.cef_context.work();
|
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));
|
event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) enum ExitReason {
|
||||||
|
Shutdown,
|
||||||
|
Restart,
|
||||||
|
UiAccelerationFailure,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use cef::args::Args;
|
use cef::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::{
|
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,
|
browser_host_create_browser_sync, execute_process,
|
||||||
};
|
};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use super::CefContext;
|
use super::CefContext;
|
||||||
use super::singlethreaded::SingleThreadedCefContext;
|
use super::singlethreaded::SingleThreadedCefContext;
|
||||||
@@ -74,11 +73,24 @@ impl<H: CefEventHandler> CefContextBuilder<H> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn common_settings(instance_dir: &Path) -> Settings {
|
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 {
|
Settings {
|
||||||
windowless_rendering_enabled: 1,
|
windowless_rendering_enabled: 1,
|
||||||
root_cache_path: instance_dir.to_str().map(CefString::from).unwrap(),
|
root_cache_path: instance_dir.to_str().map(CefString::from).unwrap(),
|
||||||
cache_path: CefString::from(""),
|
cache_path: CefString::from(""),
|
||||||
disable_signal_handlers: 1,
|
disable_signal_handlers: 1,
|
||||||
|
log_severity,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,6 +123,8 @@ impl<H: CefEventHandler> CefContextBuilder<H> {
|
|||||||
|
|
||||||
let settings = Settings {
|
let settings = Settings {
|
||||||
multi_threaded_message_loop: 1,
|
multi_threaded_message_loop: 1,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
no_sandbox: 1,
|
||||||
..Self::common_settings(&instance_dir)
|
..Self::common_settings(&instance_dir)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,8 +137,7 @@ impl<H: CefEventHandler> CefContextBuilder<H> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to initialize CEF context: {:?}", e);
|
panic!("Failed to initialize CEF context: {:?}", e);
|
||||||
std::process::exit(1);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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());
|
let result = cef::initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut());
|
||||||
if result != 1 {
|
if result != 1 {
|
||||||
let cef_exit_code = cef::get_exit_code() as u32;
|
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::InitializationFailureCode(cef_exit_code));
|
||||||
return Err(InitError::AlreadyRunning);
|
|
||||||
}
|
|
||||||
return Err(InitError::InitializationFailed(cef_exit_code));
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -213,18 +223,16 @@ fn create_browser<H: CefEventHandler>(event_handler: H, instance_dir: PathBuf, d
|
|||||||
pub(crate) enum SetupError {
|
pub(crate) enum SetupError {
|
||||||
#[error("This is the sub process should exit immediately")]
|
#[error("This is the sub process should exit immediately")]
|
||||||
Subprocess,
|
Subprocess,
|
||||||
#[error("Subprocess returned non zero exit code")]
|
#[error("Subprocess returned non zero exit code: {0}")]
|
||||||
SubprocessFailed(String),
|
SubprocessFailed(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub(crate) enum InitError {
|
pub(crate) enum InitError {
|
||||||
#[error("Initialization failed")]
|
#[error("Initialization failed with code: {0}")]
|
||||||
InitializationFailed(u32),
|
InitializationFailureCode(u32),
|
||||||
#[error("Browser creation failed")]
|
#[error("Browser creation failed")]
|
||||||
BrowserCreationFailed,
|
BrowserCreationFailed,
|
||||||
#[error("Request context creation failed")]
|
#[error("Request context creation failed")]
|
||||||
RequestContextCreationFailed,
|
RequestContextCreationFailed,
|
||||||
#[error("Another instance is already running")]
|
|
||||||
AlreadyRunning,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ impl CefContext for MultiThreadedCefContextProxy {
|
|||||||
|
|
||||||
impl Drop for MultiThreadedCefContextProxy {
|
impl Drop for MultiThreadedCefContextProxy {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
cef::shutdown();
|
// Force dropping underlying context on the UI thread
|
||||||
|
run_on_ui_thread(move || drop(CONTEXT.take()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ impl CefContext for SingleThreadedCefContext {
|
|||||||
|
|
||||||
impl Drop for SingleThreadedCefContext {
|
impl Drop for SingleThreadedCefContext {
|
||||||
fn drop(&mut self) {
|
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();
|
cef::shutdown();
|
||||||
|
|
||||||
// Sometimes some CEF processes still linger at this point and hold file handles to the cache directory.
|
// Sometimes some CEF processes still linger at this point and hold file handles to the cache directory.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use cef::sys::{cef_event_flags_t, cef_key_event_type_t, cef_mouse_button_type_t};
|
use cef::sys::{cef_key_event_type_t, cef_mouse_button_type_t};
|
||||||
use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, MouseEvent};
|
use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, MouseEvent};
|
||||||
use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
||||||
use winit::keyboard::Key;
|
|
||||||
|
|
||||||
mod keymap;
|
mod keymap;
|
||||||
use keymap::{ToCharRepresentation, ToNativeKeycode, ToVKBits};
|
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: _ } => {
|
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
|
||||||
let Some(host) = browser.host() else { return };
|
let Some(host) = browser.host() else { return };
|
||||||
|
|
||||||
|
input_state.modifiers_apply_key_event(&event.logical_key, &event.state);
|
||||||
|
|
||||||
let mut key_event = KeyEvent {
|
let mut key_event = KeyEvent {
|
||||||
type_: match (event.state, &event.logical_key) {
|
type_: match (event.state, &event.logical_key) {
|
||||||
(ElementState::Pressed, winit::keyboard::Key::Character(_)) => cef_key_event_type_t::KEYEVENT_CHAR,
|
(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();
|
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 {
|
key_event.windows_key_code = match &event.logical_key {
|
||||||
winit::keyboard::Key::Named(named) => named.to_vk_bits(),
|
winit::keyboard::Key::Named(named) => named.to_vk_bits(),
|
||||||
winit::keyboard::Key::Character(char) => char.chars().next().unwrap_or_default().to_vk_bits(),
|
winit::keyboard::Key::Character(char) => char.chars().next().unwrap_or_default().to_vk_bits(),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use cef::sys::cef_event_flags_t;
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use winit::dpi::PhysicalPosition;
|
use winit::dpi::PhysicalPosition;
|
||||||
use winit::event::{ElementState, MouseButton};
|
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};
|
use crate::cef::consts::{MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT};
|
||||||
|
|
||||||
@@ -19,6 +19,18 @@ impl InputState {
|
|||||||
self.modifiers = *modifiers;
|
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 {
|
pub(crate) fn cursor_move(&mut self, position: &PhysicalPosition<f64>) -> bool {
|
||||||
let new = position.into();
|
let new = position.into();
|
||||||
if self.mouse_position == new {
|
if self.mouse_position == new {
|
||||||
@@ -127,25 +139,26 @@ impl ClickTracker {
|
|||||||
|
|
||||||
let prev_time = record.time;
|
let prev_time = record.time;
|
||||||
let prev_position = record.position;
|
let prev_position = record.position;
|
||||||
|
let prev_count: ClickCount = record.down_count;
|
||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
record.time = now;
|
record.time = now;
|
||||||
record.position = position;
|
record.position = position;
|
||||||
|
|
||||||
match state {
|
match state {
|
||||||
ElementState::Pressed if record.down_count == ClickCount::Double => {
|
ElementState::Pressed if record.down_count == ClickCount::Triple => {
|
||||||
*record = ClickRecord {
|
*record = ClickRecord {
|
||||||
down_count: ClickCount::Single,
|
down_count: ClickCount::Double,
|
||||||
..*record
|
..*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 {
|
*record = ClickRecord {
|
||||||
up_count: ClickCount::Single,
|
up_count: ClickCount::Double,
|
||||||
..*record
|
..*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_dist = dx <= MULTICLICK_ALLOWED_TRAVEL && dy <= MULTICLICK_ALLOWED_TRAVEL;
|
||||||
let within_time = now.saturating_duration_since(prev_time) <= MULTICLICK_TIMEOUT;
|
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 {
|
*record = match state {
|
||||||
ElementState::Pressed => ClickRecord { down_count: count, ..*record },
|
ElementState::Pressed => ClickRecord { down_count: count, ..*record },
|
||||||
@@ -170,12 +187,14 @@ pub(crate) enum ClickCount {
|
|||||||
#[default]
|
#[default]
|
||||||
Single,
|
Single,
|
||||||
Double,
|
Double,
|
||||||
|
Triple,
|
||||||
}
|
}
|
||||||
impl From<ClickCount> for i32 {
|
impl From<ClickCount> for i32 {
|
||||||
fn from(count: ClickCount) -> i32 {
|
fn from(count: ClickCount) -> i32 {
|
||||||
match count {
|
match count {
|
||||||
ClickCount::Single => 1,
|
ClickCount::Single => 1,
|
||||||
ClickCount::Double => 2,
|
ClickCount::Double => 2,
|
||||||
|
ClickCount::Triple => 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,14 +34,33 @@ impl<H: CefEventHandler> ImplApp for BrowserProcessAppImpl<H> {
|
|||||||
fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) {
|
fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) {
|
||||||
if let Some(cmd) = command_line {
|
if let Some(cmd) = command_line {
|
||||||
cmd.append_switch_with_value(Some(&CefString::from("renderer-process-limit")), Some(&CefString::from("1")));
|
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_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("incognito")));
|
||||||
cmd.append_switch(Some(&CefString::from("no-first-run")));
|
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-file-system")));
|
||||||
cmd.append_switch(Some(&CefString::from("disable-local-storage")));
|
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-background-networking")));
|
||||||
cmd.append_switch(Some(&CefString::from("disable-audio-input")));
|
cmd.append_switch(Some(&CefString::from("disable-default-apps")));
|
||||||
cmd.append_switch(Some(&CefString::from("disable-audio-output")));
|
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"))]
|
#[cfg(not(feature = "accelerated_paint"))]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ impl<H: CefEventHandler> ImplDisplayHandler for DisplayHandlerImpl<H> {
|
|||||||
CT_PROGRESS => CursorIcon::Progress,
|
CT_PROGRESS => CursorIcon::Progress,
|
||||||
CT_NODROP => CursorIcon::NoDrop,
|
CT_NODROP => CursorIcon::NoDrop,
|
||||||
CT_COPY => CursorIcon::Copy,
|
CT_COPY => CursorIcon::Copy,
|
||||||
CT_NONE => CursorIcon::Default,
|
|
||||||
CT_NOTALLOWED => CursorIcon::NotAllowed,
|
CT_NOTALLOWED => CursorIcon::NotAllowed,
|
||||||
CT_ZOOMIN => CursorIcon::ZoomIn,
|
CT_ZOOMIN => CursorIcon::ZoomIn,
|
||||||
CT_ZOOMOUT => CursorIcon::ZoomOut,
|
CT_ZOOMOUT => CursorIcon::ZoomOut,
|
||||||
@@ -91,6 +90,10 @@ impl<H: CefEventHandler> ImplDisplayHandler for DisplayHandlerImpl<H> {
|
|||||||
CT_DND_COPY => CursorIcon::Copy,
|
CT_DND_COPY => CursorIcon::Copy,
|
||||||
CT_DND_LINK => CursorIcon::Alias,
|
CT_DND_LINK => CursorIcon::Alias,
|
||||||
CT_NUM_VALUES => CursorIcon::Default,
|
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,
|
_ => CursorIcon::Default,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ pub(crate) const APP_NAME: &str = "Graphite";
|
|||||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||||
pub(crate) const APP_ID: &str = "art.graphite.Graphite";
|
pub(crate) const APP_ID: &str = "art.graphite.Graphite";
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
pub(crate) const APP_DIRECTORY_NAME: &str = "graphite";
|
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_LOCK_FILE_NAME: &str = "instance.lock";
|
||||||
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
|
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
|
||||||
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron";
|
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ pub(crate) enum AppEvent {
|
|||||||
WebCommunicationInitialized,
|
WebCommunicationInitialized,
|
||||||
DesktopWrapperMessage(DesktopWrapperMessage),
|
DesktopWrapperMessage(DesktopWrapperMessage),
|
||||||
NodeGraphExecutionResult(NodeGraphExecutionResult),
|
NodeGraphExecutionResult(NodeGraphExecutionResult),
|
||||||
CloseWindow,
|
Exit,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
MenuEvent {
|
MenuEvent {
|
||||||
id: String,
|
id: String,
|
||||||
|
|||||||
+78
-40
@@ -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 clap::Parser;
|
||||||
use std::process::exit;
|
use std::io::Write;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use winit::event_loop::EventLoop;
|
use winit::event_loop::EventLoop;
|
||||||
|
|
||||||
pub(crate) mod consts;
|
pub(crate) use graphite_desktop_wrapper as wrapper;
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod cef;
|
mod cef;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod dirs;
|
mod dirs;
|
||||||
mod event;
|
mod event;
|
||||||
|
mod gpu_context;
|
||||||
mod persist;
|
mod persist;
|
||||||
|
mod preferences;
|
||||||
mod render;
|
mod render;
|
||||||
mod window;
|
mod window;
|
||||||
|
|
||||||
mod gpu_context;
|
pub(crate) mod consts;
|
||||||
|
|
||||||
pub(crate) use graphite_desktop_wrapper as wrapper;
|
|
||||||
|
|
||||||
use app::App;
|
|
||||||
use cef::CefHandler;
|
|
||||||
use cli::Cli;
|
|
||||||
use event::CreateAppEventSchedulerEventLoopExt;
|
|
||||||
|
|
||||||
use crate::consts::APP_LOCK_FILE_NAME;
|
|
||||||
|
|
||||||
pub fn start() {
|
pub fn start() {
|
||||||
tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init();
|
tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init();
|
||||||
@@ -38,26 +36,35 @@ pub fn start() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut lock = pidlock::Pidlock::new_validated(dirs::app_data_dir().join(APP_LOCK_FILE_NAME)).unwrap();
|
let cli = Cli::parse();
|
||||||
match lock.acquire() {
|
|
||||||
Ok(lock) => {
|
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");
|
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.");
|
tracing::error!("Another instance is already running, Exiting.");
|
||||||
exit(0);
|
std::process::exit(1);
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!("Failed to acquire application lock: {err}");
|
|
||||||
exit(1);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Must be called before event loop initialization or native window integrations will break
|
||||||
App::init();
|
App::init();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
|
||||||
|
|
||||||
let wgpu_context = futures::executor::block_on(gpu_context::create_wgpu_context());
|
let wgpu_context = futures::executor::block_on(gpu_context::create_wgpu_context());
|
||||||
|
|
||||||
let event_loop = EventLoop::new().unwrap();
|
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 (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_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) {
|
let cef_context = match cef_context_builder.initialize(cef_handler, disable_ui_acceleration) {
|
||||||
Ok(c) => {
|
Ok(context) => {
|
||||||
tracing::info!("CEF initialized successfully");
|
tracing::info!("CEF initialized successfully");
|
||||||
c
|
context
|
||||||
}
|
}
|
||||||
Err(cef::InitError::AlreadyRunning) => {
|
Err(cef::InitError::InitializationFailureCode(code)) => {
|
||||||
tracing::error!("Another instance is already running, Exiting.");
|
panic!("CEF initialization failed with code: {code}");
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
Err(cef::InitError::InitializationFailed(code)) => {
|
|
||||||
tracing::error!("Cef initialization failed with code: {code}");
|
|
||||||
exit(1);
|
|
||||||
}
|
}
|
||||||
Err(cef::InitError::BrowserCreationFailed) => {
|
Err(cef::InitError::BrowserCreationFailed) => {
|
||||||
tracing::error!("Failed to create CEF browser");
|
panic!("Failed to create CEF browser");
|
||||||
exit(1);
|
|
||||||
}
|
}
|
||||||
Err(cef::InitError::RequestContextCreationFailed) => {
|
Err(cef::InitError::RequestContextCreationFailed) => {
|
||||||
tracing::error!("Failed to create CEF request context");
|
panic!("Failed to create CEF request context");
|
||||||
exit(1);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, cli.files);
|
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() {
|
pub fn start_helper() {
|
||||||
|
|||||||
+2
-24
@@ -1,4 +1,4 @@
|
|||||||
use crate::wrapper::messages::{Document, DocumentId, Preferences};
|
use crate::wrapper::messages::{Document, DocumentId};
|
||||||
|
|
||||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||||
pub(crate) struct PersistentData {
|
pub(crate) struct PersistentData {
|
||||||
@@ -72,22 +72,6 @@ impl PersistentData {
|
|||||||
self.flush();
|
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) {
|
fn flush(&self) {
|
||||||
let data = match ron::ser::to_string_pretty(self, Default::default()) {
|
let data = match ron::ser::to_string_pretty(self, Default::default()) {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
@@ -129,12 +113,6 @@ impl PersistentData {
|
|||||||
path.push(crate::consts::APP_STATE_FILE_NAME);
|
path.push(crate::consts::APP_STATE_FILE_NAME);
|
||||||
path
|
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)]
|
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -190,7 +168,7 @@ impl DocumentStore {
|
|||||||
|
|
||||||
fn document_path(id: &DocumentId) -> std::path::PathBuf {
|
fn document_path(id: &DocumentId) -> std::path::PathBuf {
|
||||||
let mut path = crate::dirs::app_autosave_documents_dir();
|
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
|
path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+26
-15
@@ -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)]
|
#[derive(derivative::Derivative)]
|
||||||
#[derivative(Debug)]
|
#[derivative(Debug)]
|
||||||
@@ -17,11 +18,12 @@ pub(crate) struct RenderState {
|
|||||||
viewport_scale: [f32; 2],
|
viewport_scale: [f32; 2],
|
||||||
viewport_offset: [f32; 2],
|
viewport_offset: [f32; 2],
|
||||||
viewport_texture: Option<wgpu::Texture>,
|
viewport_texture: Option<wgpu::Texture>,
|
||||||
overlays_texture: Option<wgpu::Texture>,
|
overlays_texture: Option<TargetTexture>,
|
||||||
ui_texture: Option<wgpu::Texture>,
|
ui_texture: Option<wgpu::Texture>,
|
||||||
bind_group: Option<wgpu::BindGroup>,
|
bind_group: Option<wgpu::BindGroup>,
|
||||||
#[derivative(Debug = "ignore")]
|
#[derivative(Debug = "ignore")]
|
||||||
overlays_scene: Option<vello::Scene>,
|
overlays_scene: Option<vello::Scene>,
|
||||||
|
surface_outdated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderState {
|
impl RenderState {
|
||||||
@@ -185,6 +187,7 @@ impl RenderState {
|
|||||||
ui_texture: None,
|
ui_texture: None,
|
||||||
bind_group: None,
|
bind_group: None,
|
||||||
overlays_scene: None,
|
overlays_scene: None,
|
||||||
|
surface_outdated: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +198,7 @@ impl RenderState {
|
|||||||
|
|
||||||
self.desired_width = width;
|
self.desired_width = width;
|
||||||
self.desired_height = height;
|
self.desired_height = height;
|
||||||
|
self.surface_outdated = true;
|
||||||
|
|
||||||
if width > 0 && height > 0 && (self.config.width != width || self.config.height != height) {
|
if width > 0 && height > 0 && (self.config.width != width || self.config.height != height) {
|
||||||
self.config.width = width;
|
self.config.width = width;
|
||||||
@@ -208,25 +212,23 @@ impl RenderState {
|
|||||||
self.update_bindgroup();
|
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) {
|
pub(crate) fn bind_ui_texture(&mut self, bind_ui_texture: wgpu::Texture) {
|
||||||
self.ui_texture = Some(bind_ui_texture);
|
self.ui_texture = Some(bind_ui_texture);
|
||||||
self.update_bindgroup();
|
self.update_bindgroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_viewport_scale(&mut self, scale: [f32; 2]) {
|
pub(crate) fn set_viewport_scale(&mut self, scale: [f32; 2]) {
|
||||||
|
self.surface_outdated = true;
|
||||||
self.viewport_scale = scale;
|
self.viewport_scale = scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_viewport_offset(&mut self, offset: [f32; 2]) {
|
pub(crate) fn set_viewport_offset(&mut self, offset: [f32; 2]) {
|
||||||
|
self.surface_outdated = true;
|
||||||
self.viewport_offset = offset;
|
self.viewport_offset = offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_overlays_scene(&mut self, scene: vello::Scene) {
|
pub(crate) fn set_overlays_scene(&mut self, scene: vello::Scene) {
|
||||||
|
self.surface_outdated = true;
|
||||||
self.overlays_scene = Some(scene);
|
self.overlays_scene = Some(scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,15 +238,18 @@ impl RenderState {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let size = glam::UVec2::new(viewport_texture.width(), viewport_texture.height());
|
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 result = futures::executor::block_on(self.executor.render_vello_scene_to_target_texture(&scene, size, &Default::default(), None, &mut self.overlays_texture));
|
||||||
let Ok(texture) = texture else {
|
if let Err(e) = result {
|
||||||
tracing::error!("Error rendering overlays");
|
tracing::error!("Error rendering overlays: {:?}", e);
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
self.bind_overlays_texture(texture);
|
self.update_bindgroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn render(&mut self, window: &Window) -> Result<(), RenderError> {
|
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
|
let ui_scale = if let Some(ui_texture) = &self.ui_texture
|
||||||
&& (self.desired_width != ui_texture.width() || self.desired_height != ui_texture.height())
|
&& (self.desired_width != ui_texture.width() || self.desired_height != ui_texture.height())
|
||||||
{
|
{
|
||||||
@@ -306,13 +311,19 @@ impl RenderState {
|
|||||||
if ui_scale.is_some() {
|
if ui_scale.is_some() {
|
||||||
return Err(RenderError::OutdatedUITextureError);
|
return Err(RenderError::OutdatedUITextureError);
|
||||||
}
|
}
|
||||||
|
self.surface_outdated = false;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_bindgroup(&mut self) {
|
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 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 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 {
|
let bind_group = self.context.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
@@ -324,7 +335,7 @@ impl RenderState {
|
|||||||
},
|
},
|
||||||
wgpu::BindGroupEntry {
|
wgpu::BindGroupEntry {
|
||||||
binding: 1,
|
binding: 1,
|
||||||
resource: wgpu::BindingResource::TextureView(&overlays_texture_view),
|
resource: wgpu::BindingResource::TextureView(overlays_texture_view.as_ref()),
|
||||||
},
|
},
|
||||||
wgpu::BindGroupEntry {
|
wgpu::BindGroupEntry {
|
||||||
binding: 2,
|
binding: 2,
|
||||||
|
|||||||
+52
-8
@@ -1,13 +1,13 @@
|
|||||||
|
use crate::consts::APP_NAME;
|
||||||
|
use crate::event::AppEventScheduler;
|
||||||
|
use crate::wrapper::messages::MenuItem;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use winit::cursor::{CursorIcon, CustomCursor, CustomCursorSource};
|
use winit::cursor::{CursorIcon, CustomCursor, CustomCursorSource};
|
||||||
use winit::event_loop::ActiveEventLoop;
|
use winit::event_loop::ActiveEventLoop;
|
||||||
|
use winit::monitor::Fullscreen;
|
||||||
use winit::window::{Window as WinitWindow, WindowAttributes};
|
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 {
|
pub(crate) trait NativeWindow {
|
||||||
fn init() {}
|
fn init() {}
|
||||||
fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes;
|
fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes;
|
||||||
@@ -41,7 +41,13 @@ pub(crate) struct Window {
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
native_handle: native::NativeWindowImpl,
|
native_handle: native::NativeWindowImpl,
|
||||||
custom_cursors: HashMap<CustomCursorSource, CustomCursor>,
|
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 {
|
impl Window {
|
||||||
@@ -62,7 +68,7 @@ impl Window {
|
|||||||
|
|
||||||
let winit_window = event_loop.create_window(attributes).unwrap();
|
let winit_window = event_loop.create_window(attributes).unwrap();
|
||||||
let native_handle = native::NativeWindowImpl::new(winit_window.as_ref(), app_event_scheduler);
|
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 {
|
Self {
|
||||||
winit_window: winit_window.into(),
|
winit_window: winit_window.into(),
|
||||||
native_handle,
|
native_handle,
|
||||||
@@ -105,6 +111,9 @@ impl Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn toggle_maximize(&self) {
|
pub(crate) fn toggle_maximize(&self) {
|
||||||
|
if self.is_fullscreen() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.winit_window.set_maximized(!self.winit_window.is_maximized());
|
self.winit_window.set_maximized(!self.winit_window.is_maximized());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,11 +121,22 @@ impl Window {
|
|||||||
self.winit_window.is_maximized()
|
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 {
|
pub(crate) fn is_fullscreen(&self) -> bool {
|
||||||
self.winit_window.fullscreen().is_some()
|
self.winit_window.fullscreen().is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn start_drag(&self) {
|
pub(crate) fn start_drag(&self) {
|
||||||
|
if self.is_fullscreen() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let _ = self.winit_window.drag_window();
|
let _ = self.winit_window.drag_window();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,16 +169,35 @@ impl Window {
|
|||||||
};
|
};
|
||||||
custom_cursor.into()
|
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);
|
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>) {
|
pub(crate) fn update_menu(&self, entries: Vec<MenuItem>) {
|
||||||
self.native_handle.update_menu(entries);
|
self.native_handle.update_menu(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn clipboard_read(&self) -> Option<String> {
|
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),
|
Ok(data) => Some(data),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to read from clipboard: {e}");
|
tracing::error!("Failed to read from clipboard: {e}");
|
||||||
@@ -168,7 +207,11 @@ impl Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn clipboard_write(&mut self, data: String) {
|
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}")
|
tracing::error!("Failed to write to clipboard: {e}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,6 +220,7 @@ impl Window {
|
|||||||
pub(crate) enum Cursor {
|
pub(crate) enum Cursor {
|
||||||
Icon(CursorIcon),
|
Icon(CursorIcon),
|
||||||
Custom(CustomCursorSource),
|
Custom(CustomCursorSource),
|
||||||
|
None,
|
||||||
}
|
}
|
||||||
impl From<CursorIcon> for Cursor {
|
impl From<CursorIcon> for Cursor {
|
||||||
fn from(icon: CursorIcon) -> Self {
|
fn from(icon: CursorIcon) -> Self {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use windows::Win32::System::Com::{COINIT_APARTMENTTHREADED, CoInitializeEx};
|
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::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
|
||||||
use windows::core::HSTRING;
|
use windows::core::HSTRING;
|
||||||
use winit::event_loop::ActiveEventLoop;
|
use winit::event_loop::ActiveEventLoop;
|
||||||
@@ -13,6 +14,12 @@ pub(super) struct NativeWindowImpl {
|
|||||||
|
|
||||||
impl super::NativeWindow for NativeWindowImpl {
|
impl super::NativeWindow for NativeWindowImpl {
|
||||||
fn init() {
|
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);
|
let app_id = HSTRING::from(APP_ID);
|
||||||
unsafe {
|
unsafe {
|
||||||
let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok();
|
let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok();
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ impl NativeWindowHandle {
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
None,
|
main,
|
||||||
None,
|
None,
|
||||||
HINSTANCE(std::ptr::null_mut()),
|
HINSTANCE(std::ptr::null_mut()),
|
||||||
// Pass the main window's HWND to WM_NCCREATE so the helper can store it.
|
// Pass the main window's HWND to WM_NCCREATE so the helper can store it.
|
||||||
@@ -118,7 +118,7 @@ impl NativeWindowHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Force window update
|
// 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
|
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.
|
// 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 {
|
unsafe extern "system" fn main_window_handle_message(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||||
if msg == WM_NCCALCSIZE && wparam.0 != 0 {
|
if msg == WM_NCCALCSIZE && wparam.0 != 0 {
|
||||||
// When maximized, shrink to visible frame so content doesn't extend beyond it.
|
let params = unsafe { &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS) };
|
||||||
if unsafe { IsZoomed(hwnd).as_bool() } {
|
|
||||||
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 dpi = unsafe { GetDpiForWindow(hwnd) };
|
||||||
let size = unsafe { GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) };
|
let size = unsafe { GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) };
|
||||||
let pad = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, 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 w = (r.right - r.left) + RESIZE_BAND_THICKNESS * 2;
|
||||||
let h = (r.bottom - r.top) + 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 {
|
unsafe fn calculate_hit(helper: HWND, lparam: LPARAM) -> u32 {
|
||||||
let x = (lparam.0 & 0xFFFF) as i16 as u32;
|
let x = (lparam.0 & 0xFFFF) as i16 as i32;
|
||||||
let y = ((lparam.0 >> 16) & 0xFFFF) as i16 as u32;
|
let y = ((lparam.0 >> 16) & 0xFFFF) as i16 as i32;
|
||||||
|
|
||||||
let mut r = RECT::default();
|
let mut r = RECT::default();
|
||||||
let _ = unsafe { GetWindowRect(helper, &mut r) };
|
let _ = unsafe { GetWindowRect(helper, &mut r) };
|
||||||
|
|
||||||
let on_top = y < (r.top + 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 u32;
|
let on_right = x >= (r.right - RESIZE_BAND_THICKNESS) as i32;
|
||||||
let on_bottom = y >= (r.bottom - RESIZE_BAND_THICKNESS) as u32;
|
let on_bottom = y >= (r.bottom - RESIZE_BAND_THICKNESS) as i32;
|
||||||
let on_left = x < (r.left + RESIZE_BAND_THICKNESS) as u32;
|
let on_left = x < (r.left + RESIZE_BAND_THICKNESS) as i32;
|
||||||
|
|
||||||
match (on_top, on_right, on_bottom, on_left) {
|
match (on_top, on_right, on_bottom, on_left) {
|
||||||
(true, _, _, true) => HTTOPLEFT,
|
(true, _, _, true) => HTTOPLEFT,
|
||||||
@@ -366,3 +366,27 @@ unsafe fn calculate_resize_direction(helper: HWND, lparam: LPARAM) -> Option<u32
|
|||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the rect is effectively fullscreen, meaning it would cover the entire monitor.
|
||||||
|
// We need to use this heuristic because Windows doesn't provide a way to check for fullscreen state.
|
||||||
|
fn is_effectively_fullscreen(rect: RECT) -> bool {
|
||||||
|
let hmon = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST) };
|
||||||
|
if hmon.is_invalid() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut monitor_info = MONITORINFO {
|
||||||
|
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
if !unsafe { GetMonitorInfoW(hmon, &mut monitor_info) }.as_bool() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow a tiny tolerance for DPI / rounding issues
|
||||||
|
const EPS: i32 = 1;
|
||||||
|
(rect.left - monitor_info.rcMonitor.left).abs() <= EPS
|
||||||
|
&& (rect.top - monitor_info.rcMonitor.top).abs() <= EPS
|
||||||
|
&& (rect.right - monitor_info.rcMonitor.right).abs() <= EPS
|
||||||
|
&& (rect.bottom - monitor_info.rcMonitor.bottom).abs() <= EPS
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
use graphene_std::Color;
|
|
||||||
use graphene_std::raster::Image;
|
|
||||||
use graphite_editor::messages::app_window::app_window_message_handler::AppWindowPlatform;
|
|
||||||
use graphite_editor::messages::clipboard::utility_types::ClipboardContentRaw;
|
use graphite_editor::messages::clipboard::utility_types::ClipboardContentRaw;
|
||||||
use graphite_editor::messages::prelude::*;
|
use graphite_editor::messages::prelude::*;
|
||||||
|
|
||||||
use super::DesktopWrapperMessageDispatcher;
|
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) {
|
pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: DesktopWrapperMessage) {
|
||||||
match message {
|
match message {
|
||||||
@@ -15,9 +12,9 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
|
|||||||
DesktopWrapperMessage::Input(message) => {
|
DesktopWrapperMessage::Input(message) => {
|
||||||
dispatcher.queue_editor_message(EditorMessage::InputPreprocessor(message));
|
dispatcher.queue_editor_message(EditorMessage::InputPreprocessor(message));
|
||||||
}
|
}
|
||||||
DesktopWrapperMessage::OpenFileDialogResult { path, content, context } => match context {
|
DesktopWrapperMessage::FileDialogResult { path, content, context } => match context {
|
||||||
OpenFileDialogContext::Document => {
|
OpenFileDialogContext::Open => {
|
||||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenDocument { path, content });
|
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::OpenFile { path, content });
|
||||||
}
|
}
|
||||||
OpenFileDialogContext::Import => {
|
OpenFileDialogContext::Import => {
|
||||||
dispatcher.queue_desktop_wrapper_message(DesktopWrapperMessage::ImportFile { path, content });
|
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 } => {
|
DesktopWrapperMessage::OpenFile { path, content } => {
|
||||||
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase();
|
let message = PortfolioMessage::OpenFile { path, content };
|
||||||
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,
|
|
||||||
};
|
|
||||||
dispatcher.queue_editor_message(message);
|
dispatcher.queue_editor_message(message);
|
||||||
}
|
}
|
||||||
DesktopWrapperMessage::ImportFile { path, content } => {
|
DesktopWrapperMessage::ImportFile { path, content } => {
|
||||||
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase();
|
let message = PortfolioMessage::ImportFile { path, content };
|
||||||
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,
|
|
||||||
};
|
|
||||||
dispatcher.queue_editor_message(message);
|
dispatcher.queue_editor_message(message);
|
||||||
}
|
}
|
||||||
DesktopWrapperMessage::PollNodeGraphEvaluation => dispatcher.poll_node_graph_evaluation(),
|
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 } => {
|
DesktopWrapperMessage::UpdateMaximized { maximized } => {
|
||||||
let message = FrontendMessage::UpdateMaximized { maximized };
|
let message = FrontendMessage::UpdateMaximized { maximized };
|
||||||
dispatcher.queue_editor_message(message);
|
dispatcher.queue_editor_message(message);
|
||||||
@@ -172,5 +93,13 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
|
|||||||
dispatcher.queue_editor_message(message);
|
dispatcher.queue_editor_message(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DesktopWrapperMessage::PointerLockMove { x, y } => {
|
||||||
|
let message = AppWindowMessage::PointerLockMove { x, y };
|
||||||
|
dispatcher.queue_editor_message(message);
|
||||||
|
}
|
||||||
|
DesktopWrapperMessage::LoadThirdPartyLicenses { text } => {
|
||||||
|
let message = DialogMessage::RequestLicensesThirdPartyDialogWithLicenseText { license_text: text };
|
||||||
|
dispatcher.queue_editor_message(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::path::PathBuf;
|
#[cfg(target_os = "macos")]
|
||||||
|
use graphite_editor::messages::layout::utility_types::layout_widget::LayoutTarget;
|
||||||
use graphite_editor::messages::prelude::FrontendMessage;
|
use graphite_editor::messages::prelude::FrontendMessage;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use super::DesktopWrapperMessageDispatcher;
|
use super::DesktopWrapperMessageDispatcher;
|
||||||
use super::messages::{DesktopFrontendMessage, Document, FileFilter, OpenFileDialogContext, SaveFileDialogContext};
|
use super::messages::{DesktopFrontendMessage, Document, FileFilter, OpenFileDialogContext, SaveFileDialogContext};
|
||||||
@@ -10,29 +11,17 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
|||||||
FrontendMessage::RenderOverlays { context } => {
|
FrontendMessage::RenderOverlays { context } => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene()));
|
dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene()));
|
||||||
}
|
}
|
||||||
FrontendMessage::TriggerOpenDocument => {
|
FrontendMessage::TriggerOpen => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
||||||
title: "Open Document".to_string(),
|
title: "Open Document".to_string(),
|
||||||
filters: vec![FileFilter {
|
filters: vec![],
|
||||||
name: "Graphite".to_string(),
|
context: OpenFileDialogContext::Open,
|
||||||
extensions: vec!["graphite".to_string()],
|
|
||||||
}],
|
|
||||||
context: OpenFileDialogContext::Document,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
FrontendMessage::TriggerImport => {
|
FrontendMessage::TriggerImport => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
dispatcher.respond(DesktopFrontendMessage::OpenFileDialog {
|
||||||
title: "Import File".to_string(),
|
title: "Import File".to_string(),
|
||||||
filters: vec![
|
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()],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
context: OpenFileDialogContext::Import,
|
context: OpenFileDialogContext::Import,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -115,7 +104,10 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
|||||||
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences);
|
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences);
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[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};
|
use graphite_editor::messages::tool::tool_messages::tool_prelude::{DiffUpdate, WidgetDiff};
|
||||||
match diff.as_slice() {
|
match diff.as_slice() {
|
||||||
[
|
[
|
||||||
@@ -136,6 +128,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
|||||||
FrontendMessage::TriggerClipboardWrite { content } => {
|
FrontendMessage::TriggerClipboardWrite { content } => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::ClipboardWrite { content });
|
dispatcher.respond(DesktopFrontendMessage::ClipboardWrite { content });
|
||||||
}
|
}
|
||||||
|
FrontendMessage::WindowPointerLock => {
|
||||||
|
dispatcher.respond(DesktopFrontendMessage::PointerLock);
|
||||||
|
}
|
||||||
FrontendMessage::WindowClose => {
|
FrontendMessage::WindowClose => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::WindowClose);
|
dispatcher.respond(DesktopFrontendMessage::WindowClose);
|
||||||
}
|
}
|
||||||
@@ -145,6 +140,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
|||||||
FrontendMessage::WindowMaximize => {
|
FrontendMessage::WindowMaximize => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::WindowMaximize);
|
dispatcher.respond(DesktopFrontendMessage::WindowMaximize);
|
||||||
}
|
}
|
||||||
|
FrontendMessage::WindowFullscreen => {
|
||||||
|
dispatcher.respond(DesktopFrontendMessage::WindowFullscreen);
|
||||||
|
}
|
||||||
FrontendMessage::WindowDrag => {
|
FrontendMessage::WindowDrag => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::WindowDrag);
|
dispatcher.respond(DesktopFrontendMessage::WindowDrag);
|
||||||
}
|
}
|
||||||
@@ -157,6 +155,12 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
|||||||
FrontendMessage::WindowShowAll => {
|
FrontendMessage::WindowShowAll => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::WindowShowAll);
|
dispatcher.respond(DesktopFrontendMessage::WindowShowAll);
|
||||||
}
|
}
|
||||||
|
FrontendMessage::WindowRestart => {
|
||||||
|
dispatcher.respond(DesktopFrontendMessage::Restart);
|
||||||
|
}
|
||||||
|
FrontendMessage::TriggerDisplayThirdPartyLicensesDialog => {
|
||||||
|
dispatcher.respond(DesktopFrontendMessage::LoadThirdPartyLicenses);
|
||||||
|
}
|
||||||
m => return Some(m),
|
m => return Some(m),
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|||||||
+19
-19
@@ -1,25 +1,21 @@
|
|||||||
use graph_craft::wasm_application_io::WasmApplicationIo;
|
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 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 graphite_editor::consts::FILE_EXTENSION;
|
||||||
pub use graphene_std::Color;
|
pub use wgpu_executor::TargetTexture;
|
||||||
|
|
||||||
pub use wgpu_executor::WgpuContext;
|
pub use wgpu_executor::WgpuContext;
|
||||||
pub use wgpu_executor::WgpuContextBuilder;
|
pub use wgpu_executor::WgpuContextBuilder;
|
||||||
pub use wgpu_executor::WgpuExecutor;
|
pub use wgpu_executor::WgpuExecutor;
|
||||||
pub use wgpu_executor::WgpuFeatures;
|
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 handle_desktop_wrapper_message;
|
||||||
mod intercept_editor_message;
|
mod intercept_editor_message;
|
||||||
mod intercept_frontend_message;
|
mod intercept_frontend_message;
|
||||||
|
mod message_dispatcher;
|
||||||
|
pub mod messages;
|
||||||
pub(crate) mod utils;
|
pub(crate) mod utils;
|
||||||
|
|
||||||
pub struct DesktopWrapper {
|
pub struct DesktopWrapper {
|
||||||
@@ -27,8 +23,18 @@ pub struct DesktopWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DesktopWrapper {
|
impl DesktopWrapper {
|
||||||
pub fn new() -> Self {
|
pub fn new(uuid_random_seed: u64) -> Self {
|
||||||
Self { editor: Editor::new() }
|
#[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) {
|
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 {
|
pub enum NodeGraphExecutionResult {
|
||||||
HasRun(Option<wgpu::Texture>),
|
HasRun(Option<wgpu::Texture>),
|
||||||
NotRun,
|
NotRun,
|
||||||
|
|||||||
@@ -65,19 +65,23 @@ pub enum DesktopFrontendMessage {
|
|||||||
ClipboardWrite {
|
ClipboardWrite {
|
||||||
content: String,
|
content: String,
|
||||||
},
|
},
|
||||||
|
PointerLock,
|
||||||
WindowClose,
|
WindowClose,
|
||||||
WindowMinimize,
|
WindowMinimize,
|
||||||
WindowMaximize,
|
WindowMaximize,
|
||||||
|
WindowFullscreen,
|
||||||
WindowDrag,
|
WindowDrag,
|
||||||
WindowHide,
|
WindowHide,
|
||||||
WindowHideOthers,
|
WindowHideOthers,
|
||||||
WindowShowAll,
|
WindowShowAll,
|
||||||
|
Restart,
|
||||||
|
LoadThirdPartyLicenses,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum DesktopWrapperMessage {
|
pub enum DesktopWrapperMessage {
|
||||||
FromWeb(Box<EditorMessage>),
|
FromWeb(Box<EditorMessage>),
|
||||||
Input(InputMessage),
|
Input(InputMessage),
|
||||||
OpenFileDialogResult {
|
FileDialogResult {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
content: Vec<u8>,
|
content: Vec<u8>,
|
||||||
context: OpenFileDialogContext,
|
context: OpenFileDialogContext,
|
||||||
@@ -86,10 +90,6 @@ pub enum DesktopWrapperMessage {
|
|||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
context: SaveFileDialogContext,
|
context: SaveFileDialogContext,
|
||||||
},
|
},
|
||||||
OpenDocument {
|
|
||||||
path: PathBuf,
|
|
||||||
content: Vec<u8>,
|
|
||||||
},
|
|
||||||
OpenFile {
|
OpenFile {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
content: Vec<u8>,
|
content: Vec<u8>,
|
||||||
@@ -98,16 +98,7 @@ pub enum DesktopWrapperMessage {
|
|||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
content: Vec<u8>,
|
content: Vec<u8>,
|
||||||
},
|
},
|
||||||
ImportSvg {
|
|
||||||
path: PathBuf,
|
|
||||||
content: Vec<u8>,
|
|
||||||
},
|
|
||||||
ImportImage {
|
|
||||||
path: PathBuf,
|
|
||||||
content: Vec<u8>,
|
|
||||||
},
|
|
||||||
PollNodeGraphEvaluation,
|
PollNodeGraphEvaluation,
|
||||||
UpdatePlatform(Platform),
|
|
||||||
UpdateMaximized {
|
UpdateMaximized {
|
||||||
maximized: bool,
|
maximized: bool,
|
||||||
},
|
},
|
||||||
@@ -124,7 +115,7 @@ pub enum DesktopWrapperMessage {
|
|||||||
id: DocumentId,
|
id: DocumentId,
|
||||||
},
|
},
|
||||||
LoadPreferences {
|
LoadPreferences {
|
||||||
preferences: Option<Preferences>,
|
preferences: Preferences,
|
||||||
},
|
},
|
||||||
MenuEvent {
|
MenuEvent {
|
||||||
id: String,
|
id: String,
|
||||||
@@ -132,6 +123,13 @@ pub enum DesktopWrapperMessage {
|
|||||||
ClipboardReadResult {
|
ClipboardReadResult {
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
},
|
},
|
||||||
|
PointerLockMove {
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
},
|
||||||
|
LoadThirdPartyLicenses {
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]
|
#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]
|
||||||
@@ -148,7 +146,7 @@ pub struct FileFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub enum OpenFileDialogContext {
|
pub enum OpenFileDialogContext {
|
||||||
Document,
|
Open,
|
||||||
Import,
|
Import,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,12 +155,6 @@ pub enum SaveFileDialogContext {
|
|||||||
File { content: Vec<u8> },
|
File { content: Vec<u8> },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Platform {
|
|
||||||
Windows,
|
|
||||||
Mac,
|
|
||||||
Linux,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum MenuItem {
|
pub enum MenuItem {
|
||||||
Action {
|
Action {
|
||||||
id: String,
|
id: String,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub(crate) mod menu {
|
|||||||
widgets
|
widgets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|widget| {
|
.map(|widget| {
|
||||||
let text_button = match &widget.widget {
|
let text_button = match widget.widget.as_ref() {
|
||||||
Widget::TextButton(text_button) => text_button,
|
Widget::TextButton(text_button) => text_button,
|
||||||
_ => panic!("Menu bar layout top-level widgets are supposed to be text buttons"),
|
_ => panic!("Menu bar layout top-level widgets are supposed to be text buttons"),
|
||||||
};
|
};
|
||||||
|
|||||||
+6
-4
@@ -18,16 +18,18 @@ fn main() {
|
|||||||
if !gh.trim().is_empty() {
|
if !gh.trim().is_empty() {
|
||||||
gh.trim().to_string()
|
gh.trim().to_string()
|
||||||
} else {
|
} 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.
|
// Instruct Cargo to set environment variables for compile time.
|
||||||
// They are accessed with the `env!("GRAPHITE_*")` macro in the codebase.
|
// 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}");
|
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.
|
/// Get an environment variable, or if it is not set or empty, use the provided fallback function. Returns a string with trimmed whitespace.
|
||||||
|
|||||||
+70
-21
@@ -1,24 +1,31 @@
|
|||||||
use crate::dispatcher::Dispatcher;
|
use crate::dispatcher::Dispatcher;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
pub use graphene_std::uuid::*;
|
pub use graphene_std::uuid::*;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
// TODO: serialize with serde to save the current editor state
|
|
||||||
pub struct Editor {
|
pub struct Editor {
|
||||||
pub dispatcher: Dispatcher,
|
pub dispatcher: Dispatcher,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Editor {
|
impl Editor {
|
||||||
/// Construct the editor.
|
pub fn new(environment: Environment, uuid_random_seed: u64) -> Self {
|
||||||
/// Remember to provide a random seed with `editor::set_uuid_seed(seed)` before any editors can be used.
|
ENVIRONMENT.set(environment).expect("Editor shoud only be initialized once");
|
||||||
pub fn new() -> Self {
|
graphene_std::uuid::set_uuid_seed(uuid_random_seed);
|
||||||
|
|
||||||
Self { dispatcher: Dispatcher::new() }
|
Self { dispatcher: Dispatcher::new() }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn new_local_executor() -> (Self, crate::node_graph_executor::NodeRuntime) {
|
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 (runtime, executor) = crate::node_graph_executor::NodeGraphExecutor::new_with_local_runtime();
|
||||||
let dispatcher = Dispatcher::with_executor(executor);
|
let editor = Self {
|
||||||
(Self { dispatcher }, runtime)
|
dispatcher: Dispatcher::with_executor(executor),
|
||||||
|
};
|
||||||
|
|
||||||
|
(editor, runtime)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_message<T: Into<Message>>(&mut self, message: T) -> Vec<FrontendMessage> {
|
pub fn handle_message<T: Into<Message>>(&mut self, message: T) -> Vec<FrontendMessage> {
|
||||||
@@ -32,26 +39,68 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Editor {
|
static ENVIRONMENT: OnceLock<Environment> = OnceLock::new();
|
||||||
fn default() -> Self {
|
impl Editor {
|
||||||
Self::new()
|
#[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_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_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 {
|
pub fn commit_info_localized(localized_commit_date: &str) -> String {
|
||||||
format!(
|
let mut info = String::new();
|
||||||
"Release Series: {}\n\
|
info.push_str(&format!("Release Series: {GRAPHITE_RELEASE_SERIES}\n"));
|
||||||
Branch: {}\n\
|
if let Some(branch) = GRAPHITE_GIT_COMMIT_BRANCH {
|
||||||
Commit: {}\n\
|
info.push_str(&format!("Branch: {branch}\n"));
|
||||||
{}",
|
}
|
||||||
GRAPHITE_RELEASE_SERIES,
|
info.push_str(&format!("Commit: {}\n", GRAPHITE_GIT_COMMIT_HASH.get(..8).unwrap_or(GRAPHITE_GIT_COMMIT_HASH)));
|
||||||
GRAPHITE_GIT_COMMIT_BRANCH,
|
info.push_str(localized_commit_date);
|
||||||
GRAPHITE_GIT_COMMIT_HASH.get(..8).unwrap_or(GRAPHITE_GIT_COMMIT_HASH),
|
info
|
||||||
localized_commit_date
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ pub const DEFAULT_STROKE_WIDTH: f64 = 2.;
|
|||||||
pub const SELECTION_TOLERANCE: f64 = 5.;
|
pub const SELECTION_TOLERANCE: f64 = 5.;
|
||||||
pub const DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD: f64 = 15.;
|
pub const DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD: f64 = 15.;
|
||||||
pub const SELECTION_DRAG_ANGLE: f64 = 90.;
|
pub const SELECTION_DRAG_ANGLE: f64 = 90.;
|
||||||
|
pub const LAYER_ORIGIN_CROSS_DIAMETER: f64 = 10.;
|
||||||
|
pub const LAYER_ORIGIN_CROSS_THICKNESS: f64 = 1.;
|
||||||
|
|
||||||
// PIVOT
|
// PIVOT
|
||||||
pub const PIVOT_CROSSHAIR_THICKNESS: f64 = 1.;
|
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 SEGMENT_SELECTED_THICKNESS: f64 = 3.;
|
||||||
pub const HANDLE_LENGTH_FACTOR: f64 = 0.5;
|
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
|
// PEN TOOL
|
||||||
pub const CREATE_CURVE_THRESHOLD: f64 = 5.;
|
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 BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
|
||||||
pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
|
pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
|
||||||
|
|
||||||
|
// EYEDROPPER TOOL
|
||||||
|
pub const EYEDROPPER_PREVIEW_AREA_RESOLUTION: u32 = 11;
|
||||||
|
|
||||||
// GIZMOS
|
// GIZMOS
|
||||||
pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.;
|
pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.;
|
||||||
pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9;
|
pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9;
|
||||||
@@ -141,13 +152,19 @@ pub const SCALE_EFFECT: f64 = 0.5;
|
|||||||
// COLORS
|
// COLORS
|
||||||
pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff";
|
pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff";
|
||||||
pub const COLOR_OVERLAY_BLUE_50: &str = "#00a8ff80";
|
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: &str = "#ffc848";
|
||||||
pub const COLOR_OVERLAY_YELLOW_DULL: &str = "#d7ba8b";
|
pub const COLOR_OVERLAY_YELLOW_DULL: &str = "#d7ba8b";
|
||||||
pub const COLOR_OVERLAY_GREEN: &str = "#63ce63";
|
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: &str = "#ef5454";
|
||||||
|
pub const COLOR_OVERLAY_RED_25: &str = "#ef545440";
|
||||||
pub const COLOR_OVERLAY_GRAY: &str = "#cccccc";
|
pub const COLOR_OVERLAY_GRAY: &str = "#cccccc";
|
||||||
pub const COLOR_OVERLAY_GRAY_25: &str = "#cccccc40";
|
pub const COLOR_OVERLAY_GRAY_25: &str = "#cccccc40";
|
||||||
pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
|
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";
|
pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf";
|
||||||
|
|
||||||
// DOCUMENT
|
// DOCUMENT
|
||||||
|
|||||||
+20
-17
@@ -23,12 +23,11 @@ pub struct DispatcherMessageHandlers {
|
|||||||
debug_message_handler: DebugMessageHandler,
|
debug_message_handler: DebugMessageHandler,
|
||||||
defer_message_handler: DeferMessageHandler,
|
defer_message_handler: DeferMessageHandler,
|
||||||
dialog_message_handler: DialogMessageHandler,
|
dialog_message_handler: DialogMessageHandler,
|
||||||
globals_message_handler: GlobalsMessageHandler,
|
|
||||||
input_preprocessor_message_handler: InputPreprocessorMessageHandler,
|
input_preprocessor_message_handler: InputPreprocessorMessageHandler,
|
||||||
key_mapping_message_handler: KeyMappingMessageHandler,
|
key_mapping_message_handler: KeyMappingMessageHandler,
|
||||||
layout_message_handler: LayoutMessageHandler,
|
layout_message_handler: LayoutMessageHandler,
|
||||||
menu_bar_message_handler: MenuBarMessageHandler,
|
menu_bar_message_handler: MenuBarMessageHandler,
|
||||||
pub portfolio_message_handler: PortfolioMessageHandler,
|
pub(crate) portfolio_message_handler: PortfolioMessageHandler,
|
||||||
preferences_message_handler: PreferencesMessageHandler,
|
preferences_message_handler: PreferencesMessageHandler,
|
||||||
tool_message_handler: ToolMessageHandler,
|
tool_message_handler: ToolMessageHandler,
|
||||||
viewport_message_handler: ViewportMessageHandler,
|
viewport_message_handler: ViewportMessageHandler,
|
||||||
@@ -52,6 +51,7 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
|
|||||||
NodeGraphMessageDiscriminant::RunDocumentGraph,
|
NodeGraphMessageDiscriminant::RunDocumentGraph,
|
||||||
))),
|
))),
|
||||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender),
|
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender),
|
||||||
|
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitEyedropperPreviewRender),
|
||||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontDataLoad),
|
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontDataLoad),
|
||||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateUIScale),
|
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateUIScale),
|
||||||
];
|
];
|
||||||
@@ -192,17 +192,11 @@ impl Dispatcher {
|
|||||||
self.responses.push(message);
|
self.responses.push(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Globals(message) => {
|
|
||||||
self.message_handlers.globals_message_handler.process_message(message, &mut queue, ());
|
|
||||||
}
|
|
||||||
Message::InputPreprocessor(message) => {
|
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(
|
self.message_handlers.input_preprocessor_message_handler.process_message(
|
||||||
message,
|
message,
|
||||||
&mut queue,
|
&mut queue,
|
||||||
InputPreprocessorMessageContext {
|
InputPreprocessorMessageContext {
|
||||||
keyboard_platform,
|
|
||||||
viewport: &self.message_handlers.viewport_message_handler,
|
viewport: &self.message_handlers.viewport_message_handler,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -239,6 +233,7 @@ impl Dispatcher {
|
|||||||
Message::MenuBar(message) => {
|
Message::MenuBar(message) => {
|
||||||
let menu_bar_message_handler = &mut self.message_handlers.menu_bar_message_handler;
|
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.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.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;
|
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)
|
/// with a discriminant or the entire payload (depending on settings)
|
||||||
fn log_message(&self, message: &Message, queues: &[VecDeque<Message>], message_logging_verbosity: MessageLoggingVerbosity) {
|
fn log_message(&self, message: &Message, queues: &[VecDeque<Message>], message_logging_verbosity: MessageLoggingVerbosity) {
|
||||||
let discriminant = MessageDiscriminant::from(message);
|
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_blocked =
|
||||||
let is_empty_batched = if let Message::Batched { messages } = message { messages.is_empty() } else { false };
|
|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 {
|
match message_logging_verbosity {
|
||||||
MessageLoggingVerbosity::Off => {}
|
MessageLoggingVerbosity::Off => {}
|
||||||
MessageLoggingVerbosity::Names => {
|
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)",
|
"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 {
|
let responses = editor.editor.handle_message(PortfolioMessage::OpenFile {
|
||||||
document_name: Some(document_name.to_string()),
|
path: file_name.into(),
|
||||||
document_path: None,
|
content: document_serialized_content.bytes().collect(),
|
||||||
document_serialized_content,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if the graph renders
|
// Check if the graph renders
|
||||||
@@ -591,10 +590,14 @@ mod test {
|
|||||||
|
|
||||||
for response in responses {
|
for response in responses {
|
||||||
// Check for the existence of the file format incompatibility warning dialog after opening the test file
|
// 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 DiffUpdate::Layout(sub_layout) = &diff[0].new_value {
|
||||||
if let LayoutGroup::Row { widgets } = &sub_layout.0[0] {
|
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);
|
print_problem_to_terminal_on_failure(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
use super::app_window_message_handler::AppWindowPlatform;
|
|
||||||
|
|
||||||
#[impl_message(Message, AppWindow)]
|
#[impl_message(Message, AppWindow)]
|
||||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum AppWindowMessage {
|
pub enum AppWindowMessage {
|
||||||
UpdatePlatform { platform: AppWindowPlatform },
|
PointerLock,
|
||||||
|
PointerLockMove { x: f64, y: f64 },
|
||||||
|
Restart,
|
||||||
Close,
|
Close,
|
||||||
Minimize,
|
Minimize,
|
||||||
Maximize,
|
Maximize,
|
||||||
|
Fullscreen,
|
||||||
Drag,
|
Drag,
|
||||||
Hide,
|
Hide,
|
||||||
HideOthers,
|
HideOthers,
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
use crate::messages::app_window::AppWindowMessage;
|
use crate::application::{Environment, Platform};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
use crate::{application::Host, messages::app_window::AppWindowMessage};
|
||||||
use graphite_proc_macros::{ExtractField, message_handler_data};
|
use graphite_proc_macros::{ExtractField, message_handler_data};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, ExtractField)]
|
#[derive(Debug, Clone, Default, ExtractField)]
|
||||||
pub struct AppWindowMessageHandler {
|
pub struct AppWindowMessageHandler {}
|
||||||
platform: AppWindowPlatform,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[message_handler_data]
|
#[message_handler_data]
|
||||||
impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
|
impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
|
||||||
fn process_message(&mut self, message: AppWindowMessage, responses: &mut std::collections::VecDeque<Message>, _: ()) {
|
fn process_message(&mut self, message: AppWindowMessage, responses: &mut std::collections::VecDeque<Message>, _: ()) {
|
||||||
match message {
|
match message {
|
||||||
AppWindowMessage::UpdatePlatform { platform } => {
|
AppWindowMessage::PointerLock => {
|
||||||
self.platform = platform;
|
responses.add(FrontendMessage::WindowPointerLock);
|
||||||
responses.add(FrontendMessage::UpdatePlatform { platform: self.platform });
|
}
|
||||||
|
AppWindowMessage::PointerLockMove { x, y } => {
|
||||||
|
responses.add(FrontendMessage::WindowPointerLockMove { x, y });
|
||||||
}
|
}
|
||||||
AppWindowMessage::Close => {
|
AppWindowMessage::Close => {
|
||||||
responses.add(FrontendMessage::WindowClose);
|
responses.add(FrontendMessage::WindowClose);
|
||||||
@@ -24,6 +25,9 @@ impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
|
|||||||
AppWindowMessage::Maximize => {
|
AppWindowMessage::Maximize => {
|
||||||
responses.add(FrontendMessage::WindowMaximize);
|
responses.add(FrontendMessage::WindowMaximize);
|
||||||
}
|
}
|
||||||
|
AppWindowMessage::Fullscreen => {
|
||||||
|
responses.add(FrontendMessage::WindowFullscreen);
|
||||||
|
}
|
||||||
AppWindowMessage::Drag => {
|
AppWindowMessage::Drag => {
|
||||||
responses.add(FrontendMessage::WindowDrag);
|
responses.add(FrontendMessage::WindowDrag);
|
||||||
}
|
}
|
||||||
@@ -36,12 +40,17 @@ impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
|
|||||||
AppWindowMessage::ShowAll => {
|
AppWindowMessage::ShowAll => {
|
||||||
responses.add(FrontendMessage::WindowShowAll);
|
responses.add(FrontendMessage::WindowShowAll);
|
||||||
}
|
}
|
||||||
|
AppWindowMessage::Restart => {
|
||||||
|
responses.add(PortfolioMessage::AutoSaveAllDocuments);
|
||||||
|
responses.add(FrontendMessage::WindowRestart);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
advertise_actions!(AppWindowMessageDiscriminant;
|
advertise_actions!(AppWindowMessageDiscriminant;
|
||||||
Close,
|
Close,
|
||||||
Minimize,
|
Minimize,
|
||||||
Maximize,
|
Maximize,
|
||||||
|
Fullscreen,
|
||||||
Drag,
|
Drag,
|
||||||
Hide,
|
Hide,
|
||||||
HideOthers,
|
HideOthers,
|
||||||
@@ -56,3 +65,14 @@ pub enum AppWindowPlatform {
|
|||||||
Mac,
|
Mac,
|
||||||
Linux,
|
Linux,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&Environment> for AppWindowPlatform {
|
||||||
|
fn from(environment: &Environment) -> Self {
|
||||||
|
match (environment.platform, environment.host) {
|
||||||
|
(Platform::Web, _) => AppWindowPlatform::Web,
|
||||||
|
(Platform::Desktop, Host::Linux) => AppWindowPlatform::Linux,
|
||||||
|
(Platform::Desktop, Host::Mac) => AppWindowPlatform::Mac,
|
||||||
|
(Platform::Desktop, Host::Windows) => AppWindowPlatform::Windows,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ pub enum DialogMessage {
|
|||||||
PreferencesDialog(PreferencesDialogMessage),
|
PreferencesDialog(PreferencesDialogMessage),
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
CloseAllDocumentsWithConfirmation,
|
Dismiss,
|
||||||
CloseDialogAndThen {
|
Close,
|
||||||
|
CloseAndThen {
|
||||||
followups: Vec<Message>,
|
followups: Vec<Message>,
|
||||||
},
|
},
|
||||||
|
CloseAllDocumentsWithConfirmation,
|
||||||
DisplayDialogError {
|
DisplayDialogError {
|
||||||
title: String,
|
title: String,
|
||||||
description: String,
|
description: String,
|
||||||
@@ -35,4 +37,5 @@ pub enum DialogMessage {
|
|||||||
},
|
},
|
||||||
RequestNewDocumentDialog,
|
RequestNewDocumentDialog,
|
||||||
RequestPreferencesDialog,
|
RequestPreferencesDialog,
|
||||||
|
RequestConfirmRestartDialog,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use super::simple_dialogs::{self, AboutGraphiteDialog, DemoArtworkDialog, LicensesDialog};
|
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::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::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`.
|
/// 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)]
|
#[derive(Debug, Default, Clone, ExtractField)]
|
||||||
pub struct DialogMessageHandler {
|
pub struct DialogMessageHandler {
|
||||||
|
on_dismiss: Option<Message>,
|
||||||
export_dialog: ExportDialogMessageHandler,
|
export_dialog: ExportDialogMessageHandler,
|
||||||
new_document_dialog: NewDocumentDialogMessageHandler,
|
new_document_dialog: NewDocumentDialogMessageHandler,
|
||||||
preferences_dialog: PreferencesDialogMessageHandler,
|
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::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, responses, ()),
|
||||||
DialogMessage::PreferencesDialog(message) => self.preferences_dialog.process_message(message, responses, PreferencesDialogMessageContext { preferences }),
|
DialogMessage::PreferencesDialog(message) => self.preferences_dialog.process_message(message, responses, PreferencesDialogMessageContext { preferences }),
|
||||||
|
|
||||||
DialogMessage::CloseAllDocumentsWithConfirmation => {
|
DialogMessage::Dismiss => {
|
||||||
let dialog = simple_dialogs::CloseAllDocumentsDialog {
|
if let Some(message) = self.on_dismiss.take() {
|
||||||
unsaved_document_names: portfolio.unsaved_document_names(),
|
responses.add(message);
|
||||||
};
|
}
|
||||||
dialog.send_dialog_to_frontend(responses);
|
|
||||||
}
|
}
|
||||||
DialogMessage::CloseDialogAndThen { followups } => {
|
DialogMessage::Close => {
|
||||||
|
self.on_dismiss = None;
|
||||||
|
responses.add(FrontendMessage::DialogClose)
|
||||||
|
}
|
||||||
|
DialogMessage::CloseAndThen { followups } => {
|
||||||
for message in followups.into_iter() {
|
for message in followups.into_iter() {
|
||||||
responses.add(message);
|
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.
|
// 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).
|
// 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 } => {
|
DialogMessage::DisplayDialogError { title, description } => {
|
||||||
|
self.on_dismiss = None;
|
||||||
let dialog = simple_dialogs::ErrorDialog { title, description };
|
let dialog = simple_dialogs::ErrorDialog { title, description };
|
||||||
dialog.send_dialog_to_frontend(responses);
|
dialog.send_dialog_to_frontend(responses);
|
||||||
}
|
}
|
||||||
DialogMessage::RequestAboutGraphiteDialog => {
|
DialogMessage::RequestAboutGraphiteDialog => {
|
||||||
|
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||||
responses.add(FrontendMessage::TriggerAboutGraphiteLocalizedCommitDate {
|
responses.add(FrontendMessage::TriggerAboutGraphiteLocalizedCommitDate {
|
||||||
commit_date: env!("GRAPHITE_GIT_COMMIT_DATE").into(),
|
commit_date: GRAPHITE_GIT_COMMIT_DATE.into(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate {
|
DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate {
|
||||||
localized_commit_date,
|
localized_commit_date,
|
||||||
localized_commit_year,
|
localized_commit_year,
|
||||||
} => {
|
} => {
|
||||||
|
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||||
let dialog = AboutGraphiteDialog {
|
let dialog = AboutGraphiteDialog {
|
||||||
localized_commit_date,
|
localized_commit_date,
|
||||||
localized_commit_year,
|
localized_commit_year,
|
||||||
@@ -63,10 +79,12 @@ impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHa
|
|||||||
dialog.send_dialog_to_frontend(responses);
|
dialog.send_dialog_to_frontend(responses);
|
||||||
}
|
}
|
||||||
DialogMessage::RequestDemoArtworkDialog => {
|
DialogMessage::RequestDemoArtworkDialog => {
|
||||||
|
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||||
let dialog = DemoArtworkDialog;
|
let dialog = DemoArtworkDialog;
|
||||||
dialog.send_dialog_to_frontend(responses);
|
dialog.send_dialog_to_frontend(responses);
|
||||||
}
|
}
|
||||||
DialogMessage::RequestExportDialog => {
|
DialogMessage::RequestExportDialog => {
|
||||||
|
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||||
if let Some(document) = portfolio.active_document() {
|
if let Some(document) = portfolio.active_document() {
|
||||||
let artboards = document
|
let artboards = document
|
||||||
.metadata()
|
.metadata()
|
||||||
@@ -84,20 +102,29 @@ impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHa
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
self.export_dialog.artboards = artboards;
|
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.has_selection = document.network_interface.selected_nodes().selected_layers(document.metadata()).next().is_some();
|
||||||
self.export_dialog.send_dialog_to_frontend(responses);
|
self.export_dialog.send_dialog_to_frontend(responses);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DialogMessage::RequestLicensesDialogWithLocalizedCommitDate { localized_commit_year } => {
|
DialogMessage::RequestLicensesDialogWithLocalizedCommitDate { localized_commit_year } => {
|
||||||
|
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||||
let dialog = LicensesDialog { localized_commit_year };
|
let dialog = LicensesDialog { localized_commit_year };
|
||||||
|
|
||||||
dialog.send_dialog_to_frontend(responses);
|
dialog.send_dialog_to_frontend(responses);
|
||||||
}
|
}
|
||||||
DialogMessage::RequestLicensesThirdPartyDialogWithLicenseText { license_text } => {
|
DialogMessage::RequestLicensesThirdPartyDialogWithLicenseText { license_text } => {
|
||||||
|
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||||
let dialog = LicensesThirdPartyDialog { license_text };
|
let dialog = LicensesThirdPartyDialog { license_text };
|
||||||
dialog.send_dialog_to_frontend(responses);
|
dialog.send_dialog_to_frontend(responses);
|
||||||
}
|
}
|
||||||
DialogMessage::RequestNewDocumentDialog => {
|
DialogMessage::RequestNewDocumentDialog => {
|
||||||
|
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||||
self.new_document_dialog = NewDocumentDialogMessageHandler {
|
self.new_document_dialog = NewDocumentDialogMessageHandler {
|
||||||
name: portfolio.generate_new_document_name(),
|
name: portfolio.generate_new_document_name(),
|
||||||
infinite: false,
|
infinite: false,
|
||||||
@@ -106,9 +133,16 @@ impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHa
|
|||||||
self.new_document_dialog.send_dialog_to_frontend(responses);
|
self.new_document_dialog.send_dialog_to_frontend(responses);
|
||||||
}
|
}
|
||||||
DialogMessage::RequestPreferencesDialog => {
|
DialogMessage::RequestPreferencesDialog => {
|
||||||
self.preferences_dialog = PreferencesDialogMessageHandler {};
|
self.on_dismiss = Some(PreferencesDialogMessage::Confirm.into());
|
||||||
self.preferences_dialog.send_dialog_to_frontend(responses, preferences);
|
self.preferences_dialog.send_dialog_to_frontend(responses, preferences);
|
||||||
}
|
}
|
||||||
|
DialogMessage::RequestConfirmRestartDialog => {
|
||||||
|
self.on_dismiss = Some(DialogMessage::Close.into());
|
||||||
|
let dialog = ConfirmRestartDialog {
|
||||||
|
changed_settings: vec!["Disable UI Acceleration".into()],
|
||||||
|
};
|
||||||
|
dialog.send_dialog_to_frontend(responses);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,19 +43,28 @@ impl MessageHandler<ExportDialogMessage, ExportDialogMessageContext<'_>> for Exp
|
|||||||
ExportDialogMessage::TransparentBackground { transparent } => self.transparent_background = transparent,
|
ExportDialogMessage::TransparentBackground { transparent } => self.transparent_background = transparent,
|
||||||
ExportDialogMessage::ExportBounds { bounds } => self.bounds = bounds,
|
ExportDialogMessage::ExportBounds { bounds } => self.bounds = bounds,
|
||||||
|
|
||||||
ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport {
|
ExportDialogMessage::Submit => {
|
||||||
name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
|
let artboard_name = match self.bounds {
|
||||||
file_type: self.file_type,
|
ExportBounds::Artboard(layer) => self.artboards.get(&layer).cloned(),
|
||||||
scale_factor: self.scale_factor,
|
_ => None,
|
||||||
bounds: self.bounds,
|
};
|
||||||
transparent_background: self.file_type != FileType::Jpg && self.transparent_background,
|
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);
|
self.send_dialog_to_frontend(responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
advertise_actions! {ExportDialogUpdate;}
|
advertise_actions!(ExportDialogUpdate;
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DialogLayoutHolder for ExportDialogMessageHandler {
|
impl DialogLayoutHolder for ExportDialogMessageHandler {
|
||||||
@@ -67,13 +76,13 @@ impl DialogLayoutHolder for ExportDialogMessageHandler {
|
|||||||
TextButton::new("Export")
|
TextButton::new("Export")
|
||||||
.emphasized(true)
|
.emphasized(true)
|
||||||
.on_update(|_| {
|
.on_update(|_| {
|
||||||
DialogMessage::CloseDialogAndThen {
|
DialogMessage::CloseAndThen {
|
||||||
followups: vec![ExportDialogMessage::Submit.into()],
|
followups: vec![ExportDialogMessage::Submit.into()],
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
.widget_instance(),
|
.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 }])
|
Layout(vec![LayoutGroup::Row { widgets }])
|
||||||
@@ -92,14 +101,14 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let export_type = vec![
|
let export_type = vec![
|
||||||
TextLabel::new("File Type").table_align(true).min_width("100px").widget_instance(),
|
TextLabel::new("File Type").table_align(true).min_width(100).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
RadioInput::new(entries).selected_index(Some(self.file_type as u32)).widget_instance(),
|
RadioInput::new(entries).selected_index(Some(self.file_type as u32)).widget_instance(),
|
||||||
];
|
];
|
||||||
|
|
||||||
let resolution = vec![
|
let resolution = vec![
|
||||||
TextLabel::new("Scale Factor").table_align(true).min_width("100px").widget_instance(),
|
TextLabel::new("Scale Factor").table_align(true).min_width(100).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
NumberInput::new(Some(self.scale_factor))
|
NumberInput::new(Some(self.scale_factor))
|
||||||
.unit("")
|
.unit("")
|
||||||
.min(0.)
|
.min(0.)
|
||||||
@@ -122,7 +131,7 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
|||||||
} else {
|
} else {
|
||||||
self.bounds
|
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
|
let mut entries = choices
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -144,15 +153,15 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let export_area = vec![
|
let export_area = vec![
|
||||||
TextLabel::new("Bounds").table_align(true).min_width("100px").widget_instance(),
|
TextLabel::new("Bounds").table_align(true).min_width(100).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_instance(),
|
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_instance(),
|
||||||
];
|
];
|
||||||
|
|
||||||
let checkbox_id = CheckboxId::new();
|
let checkbox_id = CheckboxId::new();
|
||||||
let transparent_background = vec![
|
let transparent_background = vec![
|
||||||
TextLabel::new("Transparency").table_align(true).min_width("100px").for_checkbox(checkbox_id).widget_instance(),
|
TextLabel::new("Transparency").table_align(true).min_width(100).for_checkbox(checkbox_id).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
CheckboxInput::new(self.transparent_background)
|
CheckboxInput::new(self.transparent_background)
|
||||||
.disabled(self.file_type == FileType::Jpg)
|
.disabled(self.file_type == FileType::Jpg)
|
||||||
.on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground { transparent: value.checked }.into())
|
.on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground { transparent: value.checked }.into())
|
||||||
|
|||||||
+17
-12
@@ -12,7 +12,7 @@ pub struct NewDocumentDialogMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[message_handler_data]
|
#[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>, _: ()) {
|
fn process_message(&mut self, message: NewDocumentDialogMessage, responses: &mut VecDeque<Message>, _: ()) {
|
||||||
match message {
|
match message {
|
||||||
NewDocumentDialogMessage::Name { name } => self.name = name,
|
NewDocumentDialogMessage::Name { name } => self.name = name,
|
||||||
@@ -34,7 +34,11 @@ impl<'a> MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessa
|
|||||||
responses.add(ViewportMessage::RepropagateUpdate);
|
responses.add(ViewportMessage::RepropagateUpdate);
|
||||||
|
|
||||||
responses.add(DeferMessage::AfterNavigationReady {
|
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);
|
self.send_dialog_to_frontend(responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
advertise_actions! {NewDocumentDialogUpdate;}
|
advertise_actions!(NewDocumentDialogUpdate;
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DialogLayoutHolder for NewDocumentDialogMessageHandler {
|
impl DialogLayoutHolder for NewDocumentDialogMessageHandler {
|
||||||
@@ -57,13 +62,13 @@ impl DialogLayoutHolder for NewDocumentDialogMessageHandler {
|
|||||||
TextButton::new("OK")
|
TextButton::new("OK")
|
||||||
.emphasized(true)
|
.emphasized(true)
|
||||||
.on_update(|_| {
|
.on_update(|_| {
|
||||||
DialogMessage::CloseDialogAndThen {
|
DialogMessage::CloseAndThen {
|
||||||
followups: vec![NewDocumentDialogMessage::Submit.into()],
|
followups: vec![NewDocumentDialogMessage::Submit.into()],
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
.widget_instance(),
|
.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 }])
|
Layout(vec![LayoutGroup::Row { widgets }])
|
||||||
@@ -73,8 +78,8 @@ impl DialogLayoutHolder for NewDocumentDialogMessageHandler {
|
|||||||
impl LayoutHolder for NewDocumentDialogMessageHandler {
|
impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||||
fn layout(&self) -> Layout {
|
fn layout(&self) -> Layout {
|
||||||
let name = vec![
|
let name = vec![
|
||||||
TextLabel::new("Name").table_align(true).min_width("90px").widget_instance(),
|
TextLabel::new("Name").table_align(true).min_width(90).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
TextInput::new(&self.name)
|
TextInput::new(&self.name)
|
||||||
.on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name { name: text_input.value.clone() }.into())
|
.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
|
.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 checkbox_id = CheckboxId::new();
|
||||||
let infinite = vec![
|
let infinite = vec![
|
||||||
TextLabel::new("Infinite Canvas").table_align(true).min_width("90px").for_checkbox(checkbox_id).widget_instance(),
|
TextLabel::new("Infinite Canvas").table_align(true).min_width(90).for_checkbox(checkbox_id).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
CheckboxInput::new(self.infinite)
|
CheckboxInput::new(self.infinite)
|
||||||
.on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite { infinite: checkbox_input.checked }.into())
|
.on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite { infinite: checkbox_input.checked }.into())
|
||||||
.for_label(checkbox_id)
|
.for_label(checkbox_id)
|
||||||
@@ -92,8 +97,8 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let scale = vec![
|
let scale = vec![
|
||||||
TextLabel::new("Dimensions").table_align(true).min_width("90px").widget_instance(),
|
TextLabel::new("Dimensions").table_align(true).min_width(90).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
NumberInput::new(Some(self.dimensions.x as f64))
|
NumberInput::new(Some(self.dimensions.x as f64))
|
||||||
.label("W")
|
.label("W")
|
||||||
.unit(" px")
|
.unit(" px")
|
||||||
@@ -104,7 +109,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
|||||||
.min_width(100)
|
.min_width(100)
|
||||||
.on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsX { width: number_input.value.unwrap() }.into())
|
.on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsX { width: number_input.value.unwrap() }.into())
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
Separator::new(SeparatorType::Related).widget_instance(),
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
NumberInput::new(Some(self.dimensions.y as f64))
|
NumberInput::new(Some(self.dimensions.y as f64))
|
||||||
.label("H")
|
.label("H")
|
||||||
.unit(" px")
|
.unit(" px")
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ use crate::messages::prelude::*;
|
|||||||
#[impl_message(Message, DialogMessage, PreferencesDialog)]
|
#[impl_message(Message, DialogMessage, PreferencesDialog)]
|
||||||
#[derive(Eq, PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(Eq, PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum PreferencesDialogMessage {
|
pub enum PreferencesDialogMessage {
|
||||||
|
MayRequireRestart,
|
||||||
Confirm,
|
Confirm,
|
||||||
|
Update,
|
||||||
}
|
}
|
||||||
|
|||||||
+169
-72
@@ -3,6 +3,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
|||||||
use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle;
|
use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle;
|
||||||
use crate::messages::preferences::SelectionMode;
|
use crate::messages::preferences::SelectionMode;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
use graphene_std::render_node::{EditorPreferences, wgpu_available};
|
||||||
|
|
||||||
#[derive(ExtractField)]
|
#[derive(ExtractField)]
|
||||||
pub struct PreferencesDialogMessageContext<'a> {
|
pub struct PreferencesDialogMessageContext<'a> {
|
||||||
@@ -11,21 +12,35 @@ pub struct PreferencesDialogMessageContext<'a> {
|
|||||||
|
|
||||||
/// A dialog to allow users to customize Graphite editor options
|
/// A dialog to allow users to customize Graphite editor options
|
||||||
#[derive(Debug, Clone, Default, ExtractField)]
|
#[derive(Debug, Clone, Default, ExtractField)]
|
||||||
pub struct PreferencesDialogMessageHandler {}
|
pub struct PreferencesDialogMessageHandler {
|
||||||
|
unmodified_preferences: Option<PreferencesMessageHandler>,
|
||||||
|
}
|
||||||
|
|
||||||
#[message_handler_data]
|
#[message_handler_data]
|
||||||
impl MessageHandler<PreferencesDialogMessage, PreferencesDialogMessageContext<'_>> for PreferencesDialogMessageHandler {
|
impl MessageHandler<PreferencesDialogMessage, PreferencesDialogMessageContext<'_>> for PreferencesDialogMessageHandler {
|
||||||
fn process_message(&mut self, message: PreferencesDialogMessage, responses: &mut VecDeque<Message>, context: PreferencesDialogMessageContext) {
|
fn process_message(&mut self, message: PreferencesDialogMessage, responses: &mut VecDeque<Message>, context: PreferencesDialogMessageContext) {
|
||||||
let PreferencesDialogMessageContext { preferences } = context;
|
let PreferencesDialogMessageContext { preferences } = context;
|
||||||
|
|
||||||
match message {
|
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.
|
// 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 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![
|
let zoom_rate_label = vec![
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
TextLabel::new("Zoom Rate").tooltip_label("Zoom Rate").tooltip_description(zoom_rate_description).widget_instance(),
|
TextLabel::new("Zoom Rate").tooltip_label("Zoom Rate").tooltip_description(zoom_rate_description).widget_instance(),
|
||||||
];
|
];
|
||||||
let zoom_rate = vec![
|
let zoom_rate = vec![
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
NumberInput::new(Some(map_zoom_rate_to_display(preferences.viewport_zoom_wheel_rate)))
|
NumberInput::new(Some(map_zoom_rate_to_display(preferences.viewport_zoom_wheel_rate)))
|
||||||
.tooltip_label("Zoom Rate")
|
.tooltip_label("Zoom Rate")
|
||||||
.tooltip_description(zoom_rate_description)
|
.tooltip_description(zoom_rate_description)
|
||||||
@@ -72,10 +92,15 @@ impl PreferencesDialogMessageHandler {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let checkbox_id = CheckboxId::new();
|
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![
|
let zoom_with_scroll = vec![
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
CheckboxInput::new(preferences.zoom_with_scroll)
|
CheckboxInput::new(preferences.zoom_with_scroll)
|
||||||
.tooltip_label("Zoom with Scroll")
|
.tooltip_label("Zoom with Scroll")
|
||||||
.tooltip_description(zoom_with_scroll_description)
|
.tooltip_description(zoom_with_scroll_description)
|
||||||
@@ -103,12 +128,18 @@ impl PreferencesDialogMessageHandler {
|
|||||||
{
|
{
|
||||||
let header = vec![TextLabel::new("Editing").italic(true).widget_instance()];
|
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![
|
let selection_label = vec![
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
TextLabel::new("Selection")
|
TextLabel::new("Selection")
|
||||||
.tooltip_label("Selection")
|
.tooltip_label("Selection")
|
||||||
.tooltip_description("Choose how targets are selected within dragged rectangular and lasso areas.")
|
.tooltip_description(selection_label_description)
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -147,8 +178,8 @@ impl PreferencesDialogMessageHandler {
|
|||||||
.selected_index(Some(preferences.selection_mode as u32))
|
.selected_index(Some(preferences.selection_mode as u32))
|
||||||
.widget_instance();
|
.widget_instance();
|
||||||
let selection_mode = vec![
|
let selection_mode = vec![
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
selection_mode,
|
selection_mode,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -162,15 +193,20 @@ impl PreferencesDialogMessageHandler {
|
|||||||
{
|
{
|
||||||
let header = vec![TextLabel::new("Interface").italic(true).widget_instance()];
|
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![
|
let scale_label = vec![
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
TextLabel::new("Scale").tooltip_label("Scale").tooltip_description(scale_description).widget_instance(),
|
TextLabel::new("Scale").tooltip_label("Scale").tooltip_description(scale_description).widget_instance(),
|
||||||
];
|
];
|
||||||
let scale = vec![
|
let scale = vec![
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
NumberInput::new(Some(ui_scale_to_display(preferences.ui_scale)))
|
NumberInput::new(Some(ui_scale_to_display(preferences.ui_scale)))
|
||||||
.tooltip_label("Scale")
|
.tooltip_label("Scale")
|
||||||
.tooltip_description(scale_description)
|
.tooltip_description(scale_description)
|
||||||
@@ -202,10 +238,15 @@ impl PreferencesDialogMessageHandler {
|
|||||||
{
|
{
|
||||||
let header = vec![TextLabel::new("Experimental").italic(true).widget_instance()];
|
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![
|
let node_graph_wires_label = vec![
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
TextLabel::new("Node Graph Wires")
|
TextLabel::new("Node Graph Wires")
|
||||||
.tooltip_label("Node Graph Wires")
|
.tooltip_label("Node Graph Wires")
|
||||||
.tooltip_description(node_graph_section_description)
|
.tooltip_description(node_graph_section_description)
|
||||||
@@ -226,48 +267,25 @@ impl PreferencesDialogMessageHandler {
|
|||||||
.selected_index(Some(preferences.graph_wire_style as u32))
|
.selected_index(Some(preferences.graph_wire_style as u32))
|
||||||
.widget_instance();
|
.widget_instance();
|
||||||
let graph_wire_style = vec![
|
let graph_wire_style = vec![
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
graph_wire_style,
|
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 checkbox_id = CheckboxId::new();
|
||||||
let brush_tool_description = "
|
let brush_tool_description = "
|
||||||
Enable the Brush tool to support basic raster-based layer painting.\n\
|
Enable the Brush tool to support basic raster-based layer painting.\n\
|
||||||
\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\
|
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\
|
\n\
|
||||||
Content created with the Brush tool may not be compatible with future versions of Graphite.
|
Content created with the Brush tool may not be compatible with future versions of Graphite.\n\
|
||||||
"
|
\n\
|
||||||
|
*Default: Off.*
|
||||||
|
"
|
||||||
.trim();
|
.trim();
|
||||||
let brush_tool = vec![
|
let brush_tool = vec![
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
Separator::new(SeparatorType::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
CheckboxInput::new(preferences.brush_tool)
|
CheckboxInput::new(preferences.brush_tool)
|
||||||
.tooltip_label("Brush Tool")
|
.tooltip_label("Brush Tool")
|
||||||
.tooltip_description(brush_tool_description)
|
.tooltip_description(brush_tool_description)
|
||||||
@@ -281,7 +299,94 @@ impl PreferencesDialogMessageHandler {
|
|||||||
.widget_instance(),
|
.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())
|
Layout(rows.into_iter().map(|r| LayoutGroup::Row { widgets: r }).collect())
|
||||||
@@ -307,15 +412,7 @@ impl PreferencesDialogMessageHandler {
|
|||||||
|
|
||||||
fn layout_buttons(&self) -> Layout {
|
fn layout_buttons(&self) -> Layout {
|
||||||
let widgets = vec![
|
let widgets = vec![
|
||||||
TextButton::new("OK")
|
TextButton::new("OK").emphasized(true).on_update(|_| PreferencesDialogMessage::Confirm.into()).widget_instance(),
|
||||||
.emphasized(true)
|
|
||||||
.on_update(|_| {
|
|
||||||
DialogMessage::CloseDialogAndThen {
|
|
||||||
followups: vec![PreferencesDialogMessage::Confirm.into()],
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
})
|
|
||||||
.widget_instance(),
|
|
||||||
TextButton::new("Reset to Defaults").on_update(|_| PreferencesMessage::ResetToDefaults.into()).widget_instance(),
|
TextButton::new("Reset to Defaults").on_update(|_| PreferencesMessage::ResetToDefaults.into()).widget_instance(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ impl DialogLayoutHolder for AboutGraphiteDialog {
|
|||||||
const TITLE: &'static str = "About Graphite";
|
const TITLE: &'static str = "About Graphite";
|
||||||
|
|
||||||
fn layout_buttons(&self) -> Layout {
|
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 }])
|
Layout(vec![LayoutGroup::Row { widgets }])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ impl DialogLayoutHolder for CloseAllDocumentsDialog {
|
|||||||
TextButton::new("Discard All")
|
TextButton::new("Discard All")
|
||||||
.emphasized(true)
|
.emphasized(true)
|
||||||
.on_update(|_| {
|
.on_update(|_| {
|
||||||
DialogMessage::CloseDialogAndThen {
|
DialogMessage::CloseAndThen {
|
||||||
followups: vec![PortfolioMessage::CloseAllDocuments.into()],
|
followups: vec![PortfolioMessage::CloseAllDocuments.into()],
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
.widget_instance(),
|
.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 }])
|
Layout(vec![LayoutGroup::Row { widgets }])
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ impl DialogLayoutHolder for CloseDocumentDialog {
|
|||||||
TextButton::new("Save")
|
TextButton::new("Save")
|
||||||
.emphasized(true)
|
.emphasized(true)
|
||||||
.on_update(|_| {
|
.on_update(|_| {
|
||||||
DialogMessage::CloseDialogAndThen {
|
DialogMessage::CloseAndThen {
|
||||||
followups: vec![DocumentMessage::SaveDocument.into()],
|
followups: vec![DocumentMessage::SaveDocument.into()],
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
@@ -26,13 +26,13 @@ impl DialogLayoutHolder for CloseDocumentDialog {
|
|||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
TextButton::new("Discard")
|
TextButton::new("Discard")
|
||||||
.on_update(move |_| {
|
.on_update(move |_| {
|
||||||
DialogMessage::CloseDialogAndThen {
|
DialogMessage::CloseAndThen {
|
||||||
followups: vec![EventMessage::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()],
|
followups: vec![EventMessage::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()],
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
.widget_instance(),
|
.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 }])
|
Layout(vec![LayoutGroup::Row { widgets }])
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
|
/// A dialog for confirming the restart of the application when changing a preference that requires a restart to take effect.
|
||||||
|
pub struct ConfirmRestartDialog {
|
||||||
|
pub changed_settings: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DialogLayoutHolder for ConfirmRestartDialog {
|
||||||
|
const ICON: &'static str = "Warning";
|
||||||
|
const TITLE: &'static str = "Restart Required";
|
||||||
|
|
||||||
|
fn layout_buttons(&self) -> Layout {
|
||||||
|
let widgets = vec![
|
||||||
|
TextButton::new("Restart Now")
|
||||||
|
.emphasized(true)
|
||||||
|
.on_update(|_| {
|
||||||
|
DialogMessage::CloseAndThen {
|
||||||
|
followups: vec![AppWindowMessage::Restart.into()],
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.widget_instance(),
|
||||||
|
TextButton::new("Later").on_update(|_| FrontendMessage::DialogClose.into()).widget_instance(),
|
||||||
|
];
|
||||||
|
|
||||||
|
Layout(vec![LayoutGroup::Row { widgets }])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutHolder for ConfirmRestartDialog {
|
||||||
|
fn layout(&self) -> Layout {
|
||||||
|
let changed_settings = "• ".to_string() + &self.changed_settings.join("\n• ");
|
||||||
|
|
||||||
|
Layout(vec![
|
||||||
|
LayoutGroup::Row {
|
||||||
|
widgets: vec![TextLabel::new("Restart to apply changes?").bold(true).multiline(true).widget_instance()],
|
||||||
|
},
|
||||||
|
LayoutGroup::Row {
|
||||||
|
widgets: vec![
|
||||||
|
TextLabel::new(
|
||||||
|
format!(
|
||||||
|
"
|
||||||
|
Settings that only take effect on next launch:\n\
|
||||||
|
{changed_settings}\n\
|
||||||
|
\n\
|
||||||
|
This only takes a few seconds. Open documents,\n\
|
||||||
|
even unsaved ones, will be automatically restored.
|
||||||
|
"
|
||||||
|
)
|
||||||
|
.trim(),
|
||||||
|
)
|
||||||
|
.multiline(true)
|
||||||
|
.widget_instance(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ impl DialogLayoutHolder for DemoArtworkDialog {
|
|||||||
const TITLE: &'static str = "Demo Artwork";
|
const TITLE: &'static str = "Demo Artwork";
|
||||||
|
|
||||||
fn layout_buttons(&self) -> Layout {
|
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 }])
|
Layout(vec![LayoutGroup::Row { widgets }])
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ impl LayoutHolder for DemoArtworkDialog {
|
|||||||
.chunks(4)
|
.chunks(4)
|
||||||
.flat_map(|chunk| {
|
.flat_map(|chunk| {
|
||||||
fn make_dialog(name: &str, filename: &str) -> Message {
|
fn make_dialog(name: &str, filename: &str) -> Message {
|
||||||
DialogMessage::CloseDialogAndThen {
|
DialogMessage::CloseAndThen {
|
||||||
followups: vec![
|
followups: vec![
|
||||||
FrontendMessage::TriggerFetchAndOpenDocument {
|
FrontendMessage::TriggerFetchAndOpenDocument {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ impl DialogLayoutHolder for ErrorDialog {
|
|||||||
const TITLE: &'static str = "Error";
|
const TITLE: &'static str = "Error";
|
||||||
|
|
||||||
fn layout_buttons(&self) -> Layout {
|
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 }])
|
Layout(vec![LayoutGroup::Row { widgets }])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ impl DialogLayoutHolder for LicensesDialog {
|
|||||||
const TITLE: &'static str = "Licenses";
|
const TITLE: &'static str = "Licenses";
|
||||||
|
|
||||||
fn layout_buttons(&self) -> Layout {
|
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 }])
|
Layout(vec![LayoutGroup::Row { widgets }])
|
||||||
}
|
}
|
||||||
@@ -18,25 +18,19 @@ impl DialogLayoutHolder for LicensesDialog {
|
|||||||
fn layout_column_2(&self) -> Layout {
|
fn layout_column_2(&self) -> Layout {
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
let button_definitions: &[(&str, &str, fn() -> Message)] = &[
|
let button_definitions: &[(&str, &str, fn() -> Message)] = &[
|
||||||
("GraphiteLogo", "Graphite Logo", || {
|
("Code", "Source Code License", || {
|
||||||
FrontendMessage::TriggerVisitLink {
|
FrontendMessage::TriggerVisitLink {
|
||||||
url: "https://graphite.art/logo/".into(),
|
url: "https://graphite.art/license#source-code".into(),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
("IconsGrid", "Graphite Icons", || {
|
("GraphiteLogo", "Branding License", || {
|
||||||
FrontendMessage::TriggerVisitLink {
|
FrontendMessage::TriggerVisitLink {
|
||||||
url: "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/frontend/assets/LICENSE.md".into(),
|
url: "https://graphite.art/license#branding".into(),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
("License", "Graphite License", || {
|
("IconsGrid", "Dependency Licenses", || FrontendMessage::TriggerDisplayThirdPartyLicensesDialog.into()),
|
||||||
FrontendMessage::TriggerVisitLink {
|
|
||||||
url: "https://graphite.art/license/".into(),
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}),
|
|
||||||
("License", "Other Licenses", || FrontendMessage::TriggerDisplayThirdPartyLicensesDialog.into()),
|
|
||||||
];
|
];
|
||||||
let widgets = button_definitions
|
let widgets = button_definitions
|
||||||
.iter()
|
.iter()
|
||||||
@@ -52,13 +46,11 @@ impl LayoutHolder for LicensesDialog {
|
|||||||
let year = &self.localized_commit_year;
|
let year = &self.localized_commit_year;
|
||||||
let description = format!(
|
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\
|
\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\
|
\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\
|
Graphite is distributed with third-party open source code\ndependencies. See \"Dependency Licenses\" for details.
|
||||||
\n\
|
|
||||||
Graphite is distributed with third-party open source code\ndependencies. See \"Other Licenses\" for details.
|
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
let description = description.trim();
|
let description = description.trim();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ impl DialogLayoutHolder for LicensesThirdPartyDialog {
|
|||||||
const TITLE: &'static str = "Third-Party Software License Notices";
|
const TITLE: &'static str = "Third-Party Software License Notices";
|
||||||
|
|
||||||
fn layout_buttons(&self) -> Layout {
|
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 }])
|
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
|
// 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 {
|
Layout(vec![LayoutGroup::Row {
|
||||||
widgets: vec![
|
widgets: vec![
|
||||||
TextLabel::new(license_text)
|
TextLabel::new(license_text)
|
||||||
.monospace(true)
|
.monospace(true)
|
||||||
.multiline(true)
|
.multiline(true)
|
||||||
.min_width(format!("{non_wrapping_column_width}ch"))
|
.min_width_characters(non_wrapping_column_width)
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
],
|
],
|
||||||
}])
|
}])
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
mod about_graphite_dialog;
|
mod about_graphite_dialog;
|
||||||
mod close_all_documents_dialog;
|
mod close_all_documents_dialog;
|
||||||
mod close_document_dialog;
|
mod close_document_dialog;
|
||||||
|
mod confirm_restart_dialog;
|
||||||
mod demo_artwork_dialog;
|
mod demo_artwork_dialog;
|
||||||
mod error_dialog;
|
mod error_dialog;
|
||||||
mod licenses_dialog;
|
mod licenses_dialog;
|
||||||
@@ -9,6 +10,7 @@ mod licenses_third_party_dialog;
|
|||||||
pub use about_graphite_dialog::AboutGraphiteDialog;
|
pub use about_graphite_dialog::AboutGraphiteDialog;
|
||||||
pub use close_all_documents_dialog::CloseAllDocumentsDialog;
|
pub use close_all_documents_dialog::CloseAllDocumentsDialog;
|
||||||
pub use close_document_dialog::CloseDocumentDialog;
|
pub use close_document_dialog::CloseDocumentDialog;
|
||||||
|
pub use confirm_restart_dialog::ConfirmRestartDialog;
|
||||||
pub use demo_artwork_dialog::ARTWORK;
|
pub use demo_artwork_dialog::ARTWORK;
|
||||||
pub use demo_artwork_dialog::DemoArtworkDialog;
|
pub use demo_artwork_dialog::DemoArtworkDialog;
|
||||||
pub use error_dialog::ErrorDialog;
|
pub use error_dialog::ErrorDialog;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument};
|
use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument};
|
||||||
use crate::messages::app_window::app_window_message_handler::AppWindowPlatform;
|
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::input_mapper::utility_types::misc::ActionShortcut;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::portfolio::document::node_graph::utility_types::{
|
use crate::messages::portfolio::document::node_graph::utility_types::{
|
||||||
BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, NodeGraphErrorDiagnostic, Transform,
|
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::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use glam::IVec2;
|
use glam::IVec2;
|
||||||
@@ -27,7 +28,7 @@ pub enum FrontendMessage {
|
|||||||
title: String,
|
title: String,
|
||||||
icon: String,
|
icon: String,
|
||||||
},
|
},
|
||||||
DisplayDialogDismiss,
|
DialogClose,
|
||||||
DisplayDialogPanic {
|
DisplayDialogPanic {
|
||||||
#[serde(rename = "panicInfo")]
|
#[serde(rename = "panicInfo")]
|
||||||
panic_info: String,
|
panic_info: String,
|
||||||
@@ -38,7 +39,7 @@ pub enum FrontendMessage {
|
|||||||
line_height_ratio: f64,
|
line_height_ratio: f64,
|
||||||
#[serde(rename = "fontSize")]
|
#[serde(rename = "fontSize")]
|
||||||
font_size: f64,
|
font_size: f64,
|
||||||
color: Color,
|
color: String,
|
||||||
#[serde(rename = "fontData")]
|
#[serde(rename = "fontData")]
|
||||||
font_data: Vec<u8>,
|
font_data: Vec<u8>,
|
||||||
transform: [f64; 6],
|
transform: [f64; 6],
|
||||||
@@ -64,8 +65,10 @@ pub enum FrontendMessage {
|
|||||||
#[serde(rename = "nodeTypes")]
|
#[serde(rename = "nodeTypes")]
|
||||||
node_types: Vec<FrontendNodeType>,
|
node_types: Vec<FrontendNodeType>,
|
||||||
},
|
},
|
||||||
SendShortcutF11 {
|
SendShortcutFullscreen {
|
||||||
shortcut: Option<ActionShortcut>,
|
shortcut: Option<ActionShortcut>,
|
||||||
|
#[serde(rename = "shortcutMac")]
|
||||||
|
shortcut_mac: Option<ActionShortcut>,
|
||||||
},
|
},
|
||||||
SendShortcutAltClick {
|
SendShortcutAltClick {
|
||||||
shortcut: Option<ActionShortcut>,
|
shortcut: Option<ActionShortcut>,
|
||||||
@@ -105,7 +108,6 @@ pub enum FrontendMessage {
|
|||||||
font: Font,
|
font: Font,
|
||||||
url: String,
|
url: String,
|
||||||
},
|
},
|
||||||
TriggerImport,
|
|
||||||
TriggerPersistenceRemoveDocument {
|
TriggerPersistenceRemoveDocument {
|
||||||
#[serde(rename = "documentId")]
|
#[serde(rename = "documentId")]
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
@@ -120,7 +122,8 @@ pub enum FrontendMessage {
|
|||||||
TriggerLoadRestAutoSaveDocuments,
|
TriggerLoadRestAutoSaveDocuments,
|
||||||
TriggerOpenLaunchDocuments,
|
TriggerOpenLaunchDocuments,
|
||||||
TriggerLoadPreferences,
|
TriggerLoadPreferences,
|
||||||
TriggerOpenDocument,
|
TriggerOpen,
|
||||||
|
TriggerImport,
|
||||||
TriggerSavePreferences {
|
TriggerSavePreferences {
|
||||||
preferences: PreferencesMessageHandler,
|
preferences: PreferencesMessageHandler,
|
||||||
},
|
},
|
||||||
@@ -148,6 +151,11 @@ pub enum FrontendMessage {
|
|||||||
#[serde(rename = "documentId")]
|
#[serde(rename = "documentId")]
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
},
|
},
|
||||||
|
UpdateGradientStopColorPickerPosition {
|
||||||
|
color: Color,
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
},
|
||||||
UpdateImportsExports {
|
UpdateImportsExports {
|
||||||
/// If the primary import is not visible, then it is None.
|
/// If the primary import is not visible, then it is None.
|
||||||
imports: Vec<Option<FrontendGraphOutput>>,
|
imports: Vec<Option<FrontendGraphOutput>>,
|
||||||
@@ -191,7 +199,9 @@ pub enum FrontendMessage {
|
|||||||
UpdateLayersPanelState {
|
UpdateLayersPanelState {
|
||||||
open: bool,
|
open: bool,
|
||||||
},
|
},
|
||||||
UpdateDataPanelLayout {
|
UpdateLayout {
|
||||||
|
#[serde(rename = "layoutTarget")]
|
||||||
|
layout_target: LayoutTarget,
|
||||||
diff: Vec<WidgetDiff>,
|
diff: Vec<WidgetDiff>,
|
||||||
},
|
},
|
||||||
UpdateImportReorderIndex {
|
UpdateImportReorderIndex {
|
||||||
@@ -210,34 +220,18 @@ pub enum FrontendMessage {
|
|||||||
#[serde(rename = "hasLeftInputWire")]
|
#[serde(rename = "hasLeftInputWire")]
|
||||||
has_left_input_wire: HashMap<NodeId, bool>,
|
has_left_input_wire: HashMap<NodeId, bool>,
|
||||||
},
|
},
|
||||||
UpdateDialogButtons {
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdateDialogColumn1 {
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdateDialogColumn2 {
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdateDocumentArtwork {
|
UpdateDocumentArtwork {
|
||||||
svg: String,
|
svg: String,
|
||||||
},
|
},
|
||||||
UpdateImageData {
|
UpdateImageData {
|
||||||
image_data: Vec<(u64, Image<Color>)>,
|
image_data: Vec<(u64, Image<Color>)>,
|
||||||
},
|
},
|
||||||
UpdateDocumentBarLayout {
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdateDocumentLayerDetails {
|
UpdateDocumentLayerDetails {
|
||||||
data: LayerPanelEntry,
|
data: LayerPanelEntry,
|
||||||
},
|
},
|
||||||
UpdateDocumentLayerStructure {
|
UpdateDocumentLayerStructure {
|
||||||
#[serde(rename = "dataBuffer")]
|
#[serde(rename = "layerStructure")]
|
||||||
data_buffer: RawBuffer,
|
layer_structure: Vec<LayerStructureEntry>,
|
||||||
},
|
|
||||||
UpdateDocumentLayerStructureJs {
|
|
||||||
#[serde(rename = "dataBuffer")]
|
|
||||||
data_buffer: JsRawBuffer,
|
|
||||||
},
|
},
|
||||||
UpdateDocumentRulers {
|
UpdateDocumentRulers {
|
||||||
origin: (f64, f64),
|
origin: (f64, f64),
|
||||||
@@ -251,6 +245,7 @@ pub enum FrontendMessage {
|
|||||||
multiplier: (f64, f64),
|
multiplier: (f64, f64),
|
||||||
},
|
},
|
||||||
UpdateEyedropperSamplingState {
|
UpdateEyedropperSamplingState {
|
||||||
|
image: Option<EyedropperPreviewImage>,
|
||||||
#[serde(rename = "mousePosition")]
|
#[serde(rename = "mousePosition")]
|
||||||
mouse_position: Option<(f64, f64)>,
|
mouse_position: Option<(f64, f64)>,
|
||||||
#[serde(rename = "primaryColor")]
|
#[serde(rename = "primaryColor")]
|
||||||
@@ -263,18 +258,6 @@ pub enum FrontendMessage {
|
|||||||
UpdateGraphFadeArtwork {
|
UpdateGraphFadeArtwork {
|
||||||
percentage: f64,
|
percentage: f64,
|
||||||
},
|
},
|
||||||
UpdateLayersPanelControlBarLeftLayout {
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdateLayersPanelControlBarRightLayout {
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdateLayersPanelBottomBarLayout {
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdateMenuBarLayout {
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdateMouseCursor {
|
UpdateMouseCursor {
|
||||||
cursor: MouseCursorIcon,
|
cursor: MouseCursorIcon,
|
||||||
},
|
},
|
||||||
@@ -291,9 +274,6 @@ pub enum FrontendMessage {
|
|||||||
wires: Vec<WirePathUpdate>,
|
wires: Vec<WirePathUpdate>,
|
||||||
},
|
},
|
||||||
ClearAllNodeGraphWires,
|
ClearAllNodeGraphWires,
|
||||||
UpdateNodeGraphControlBarLayout {
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdateNodeGraphSelection {
|
UpdateNodeGraphSelection {
|
||||||
selected: Vec<NodeId>,
|
selected: Vec<NodeId>,
|
||||||
},
|
},
|
||||||
@@ -308,28 +288,10 @@ pub enum FrontendMessage {
|
|||||||
#[serde(rename = "openDocuments")]
|
#[serde(rename = "openDocuments")]
|
||||||
open_documents: Vec<OpenDocument>,
|
open_documents: Vec<OpenDocument>,
|
||||||
},
|
},
|
||||||
UpdatePropertiesPanelLayout {
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdateToolOptionsLayout {
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdateToolShelfLayout {
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdateWirePathInProgress {
|
UpdateWirePathInProgress {
|
||||||
#[serde(rename = "wirePath")]
|
#[serde(rename = "wirePath")]
|
||||||
wire_path: Option<WirePath>,
|
wire_path: Option<WirePath>,
|
||||||
},
|
},
|
||||||
UpdateWelcomeScreenButtonsLayout {
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdateStatusBarHintsLayout {
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdateWorkingColorsLayout {
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdatePlatform {
|
UpdatePlatform {
|
||||||
platform: AppWindowPlatform,
|
platform: AppWindowPlatform,
|
||||||
},
|
},
|
||||||
@@ -360,11 +322,18 @@ pub enum FrontendMessage {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Window prefix: cause the application window to do something
|
// Window prefix: cause the application window to do something
|
||||||
|
WindowPointerLock,
|
||||||
|
WindowPointerLockMove {
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
},
|
||||||
WindowClose,
|
WindowClose,
|
||||||
WindowMinimize,
|
WindowMinimize,
|
||||||
WindowMaximize,
|
WindowMaximize,
|
||||||
|
WindowFullscreen,
|
||||||
WindowDrag,
|
WindowDrag,
|
||||||
WindowHide,
|
WindowHide,
|
||||||
WindowHideOthers,
|
WindowHideOthers,
|
||||||
WindowShowAll,
|
WindowShowAll,
|
||||||
|
WindowRestart,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,3 +62,10 @@ pub enum ExportBounds {
|
|||||||
Selection,
|
Selection,
|
||||||
Artboard(LayerNodeIdentifier),
|
Artboard(LayerNodeIdentifier),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||||
|
pub struct EyedropperPreviewImage {
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
use crate::messages::portfolio::utility_types::Platform;
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
pub static GLOBAL_PLATFORM: OnceLock<Platform> = OnceLock::new();
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
use crate::messages::portfolio::utility_types::Platform;
|
|
||||||
use crate::messages::prelude::*;
|
|
||||||
|
|
||||||
#[impl_message(Message, Globals)]
|
|
||||||
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum GlobalsMessage {
|
|
||||||
SetPlatform { platform: Platform },
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
use crate::messages::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Default, ExtractField)]
|
|
||||||
pub struct GlobalsMessageHandler {}
|
|
||||||
|
|
||||||
#[message_handler_data]
|
|
||||||
impl MessageHandler<GlobalsMessage, ()> for GlobalsMessageHandler {
|
|
||||||
fn process_message(&mut self, message: GlobalsMessage, _responses: &mut VecDeque<Message>, _: ()) {
|
|
||||||
match message {
|
|
||||||
GlobalsMessage::SetPlatform { platform } => {
|
|
||||||
if GLOBAL_PLATFORM.get() != Some(&platform) {
|
|
||||||
GLOBAL_PLATFORM.set(platform).expect("Failed to set GLOBAL_PLATFORM");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
advertise_actions!(GlobalsMessageDiscriminant;
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
mod globals_message;
|
|
||||||
mod globals_message_handler;
|
|
||||||
|
|
||||||
pub mod global_variables;
|
|
||||||
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use globals_message::{GlobalsMessage, GlobalsMessageDiscriminant};
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use globals_message_handler::GlobalsMessageHandler;
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
use super::utility_types::input_keyboard::KeysGroup;
|
use super::utility_types::input_keyboard::KeysGroup;
|
||||||
use super::utility_types::misc::Mapping;
|
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::input_keyboard::{self, Key};
|
||||||
use crate::messages::input_mapper::utility_types::misc::MappingEntry;
|
use crate::messages::input_mapper::utility_types::misc::MappingEntry;
|
||||||
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
|
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
#[derive(ExtractField)]
|
#[derive(ExtractField)]
|
||||||
pub struct InputMapperMessageContext<'a> {
|
pub struct InputMapperMessageContext<'a> {
|
||||||
@@ -34,27 +33,6 @@ impl InputMapperMessageHandler {
|
|||||||
self.mapping = mapping;
|
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> {
|
pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant) -> Option<KeysGroup> {
|
||||||
let all_key_mapping_entries = std::iter::empty()
|
let all_key_mapping_entries = std::iter::empty()
|
||||||
.chain(self.mapping.key_up.iter())
|
.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);
|
let found_actions = all_mapping_entries.filter(|entry| entry.action.to_discriminant() == *action_to_find);
|
||||||
|
|
||||||
// Get the `Key` for this platform's accelerator key
|
// 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 = if Editor::environment().is_mac() { Key::Command } else { Key::Control };
|
||||||
let platform_accel_key = match keyboard_layout() {
|
|
||||||
KeyboardPlatformLayout::Standard => Key::Control,
|
|
||||||
KeyboardPlatformLayout::Mac => Key::Command,
|
|
||||||
};
|
|
||||||
|
|
||||||
let entry_to_key = |entry: &MappingEntry| {
|
let entry_to_key = |entry: &MappingEntry| {
|
||||||
// Get the modifier keys for the entry (and convert them to Key)
|
// Get the modifier keys for the entry (and convert them to Key)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user