mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Make the data model use Item and List types universally, with nodes authored as rank-polymorphic kernels (#4335)
* Add rank polymorphism node audit classifying all 271 nodes
* Implement StaticType for Item<T>
* Generate Item and mapped List wire variants for nodes declaring an Item<T> primary input
* Migrate nine nodes to Item element-wise kernels, dissolving the blending trait boilerplate
* Document the Item kernel implementation and staging plan
* Route Item<Vector> through TaggedValue::TypeDefault
* Add executor integration tests covering the Item and List wire variants
* Collapse element-wise Item/List wire pairs to the List form for conversion insertion
* Migrate sixteen vector modifier nodes to Item element-wise kernels
* Migrate Sample Image, Extend Image to Bounds, and Dehaze to Item element-wise kernels
* Fix bevel_with_transform test to actually exercise the transform attribute
* Implement From<T> for Item<T>
* Register PromoteNode rank adapters wrapping bare values into Item wires
* Insert PromoteNode adapters for Item/List wire pair fields in the preprocessor
* Define a real promote node backing the PromoteNode registry identifiers
* Zip ranked Item connectors by frame slot in the mapped element-wise variant
* Register ItemToListNode singleton raise adapters
* Resolve Item wires against List connectors by inserting promotion adapters at construction
* Rank the Offset Points distance connector and prove mixed-rank resolution end-to-end
* Implement Clampable for Item and List wires with per-variant clamp bounds
* Rank the Round Corners radius connector, exercising hard bounds on a ranked wire
* Implement ApplyTransform for Item
* Add Item wire implementations to the Transform node, keeping rank-0 chains rank 0
* Detect element-wise nodes by lazy primary connectors declaring Output = Item
* Convert Transform to an Item kernel with ranked parameters, delivering the broadcast milestone
* Rename Apply Transform to Bake Transform, baking item transforms on Vector, DAffine2, and DVec2
* Promote bare wires onto Item connectors at resolution via WrapItemNode adapters
* Rank the numeric, vector, and boolean parameters across the migrated element-wise nodes
* Rank the enum, integer, and seed parameters, registering their rank adapters via a consolidated macro
* Amend the audit with the DashPattern value type resolution
* Migrate the string family to Item element-wise kernels
* Unwrap Item wires into bare legacy connectors at resolution via UnwrapItemNode adapters
* Shadow owned node parameters in bodies instead of mut in signatures
* Migrate the math family and string measure nodes to Item element-wise kernels
* Convert the comparison and clamp nodes to Item kernels, dropping unreachable &str rows
* Flat-map expander kernels returning List under the mapped variant's frame
* Migrate the expander nodes to Item kernels flat-mapping under the frame
* Remove the unused peel_list helper
* Rank the raster adjustment and blending kernels, recontextualizing shader nodes onto an Item stand-in
Migrate the 16 adjustment nodes, Mix, Color Overlay, and Gradient Map from whole-List kernels to rank-0 Item kernels, letting the macro derive the List-mapped (zip) variants. Move the Adjust and Blend per-element seams off List onto the element types (add the Raster<CPU> impls, drop the now-dead List impls).
Shader nodes keep their bodies verbatim: PerPixelAdjust re-emits the identical kernel against a transparent no_std Item stand-in, so every Item<T> connector and .element() call resolves to a zero-cost identity on the GPU while the uniform buffer stays bare repr(C). The macro peels Item off ranked uniform params, wraps the fetched texel and uniforms at the entry point, and unwraps the result. This drops the shader_node/Item incompatibility guard. Register rank adapters for the adjustment enums.
* Update the rank polymorphism roadmap for the landed shader-node and adjustments chunk
* Rename the GPU Item stand-in to ShaderItem, aliased as Item at its shader-node import sites
* Flip the vector shape generators to emit rank-0 Item<Vector>
The shape generators (Rectangle, Circle, Ellipse, Arc, Spiral, Polygon, Star, Arrow, Line, Grid, QR Code) each produced exactly one shape wrapped in a singleton List<Vector>. Emit Item<Vector> directly so they connect to the rank-0 content connector of the migrated Transform node. Downstream List consumers receive the value through the existing Item to List promotion.
Relax the element-wise validation so a `()` (generator) primary may return Item<T> without being element-wise. Adapt the Repeat on Points test, which still takes a List content connector, by raising the generator's Item output through a singleton wrapper node.
* Parse ranked Item<T> parameter defaults against the bare element type
A ranked `Item<T>` parameter's default value is a bare, unranked `T` (promoted to the wire at resolution), but the preprocessor was handed the wrapped `Item<T>` type and could not parse the literal, flooding the console with warnings and dropping the defaults. Key the field's default_type metadata off the peeled element type for concrete ranked parameters, leaving generic `Item<T>` primaries and skip_impl nodes untouched.
* Parse an element-wise primary's scalar default against the bare element type
An element-wise node's primary reports its default_type as the List wire form so an unconnected primary defaults to an empty list. But when the primary carries a scalar `#[default]` (such as Root's radicand), that literal must parse as a bare element, not a List. Key the primary's default_type off the bare element type when it has a Default value source, keeping the List form otherwise.
* Add the DashPattern value type for stroke dash sequences
Introduce a rank-0 DashPattern value type (a Vec<f64> of alternating dash and gap lengths) so a stroke's dash pattern is a single frameable value rather than a rank-1 List<f64>. Register it as an auto-generated TaggedValue variant, parse its default from a comma or space separated string, and register its rank adapters. Not yet wired into the Stroke node.
* Rank the Fill and Stroke nodes element-wise and give Stroke a DashPattern connector
Migrate Fill and Stroke to element-wise Item<V> primaries (over Vector and Graphic element types) via a new element-level VectorItemMut trait, so styling one shape yields one shape and rank is preserved instead of promoting the input to a singleton List and emitting a List. The macro derives the List-mapped variant for genuine collections.
Wire the Stroke dash sequence to the new rank-0 DashPattern value type, collapsing the old content x paint x dash cartesian and dropping the IntoF64Vec trait. Update the stroke properties dash widget, the drawing tool, and graph-operation plumbing to read and write DashPattern, and migrate legacy F64Array, F64, and String dash inputs on document open.
Assign Colors stays a whole-collection node: each element's gradient position depends on its index among all siblings, which the element frame does not expose, so it keeps its List primary and the VectorListIterMut trait.
* Register rank adapters for the ranked Stroke enum parameters
The element-wise Stroke node ranks its align, cap, and paint order parameters as Item<StrokeAlign>, Item<StrokeCap>, and Item<PaintOrder>, but those enums lacked promotion adapters, so a bare default enum value could not be promoted to its Item wire and no Stroke variant resolved ("No construct found for node"). Register their rank adapters alongside StrokeJoin.
* Display Item wires in the Data panel without a List's ID column
Add a TableItemLayout impl for Item<T> and recognize Item wire types when introspecting graph data. An Item holds a single element, so it renders as a one-row table of the element plus its attributes with no leading index column, and it labels as its element type T rather than a List's T[]. Add ItemAttributeValues::get_any for the attribute widget dispatch.
* Register MonitorNode for Item wire types so the Data panel introspects them directly
Graph introspection wraps the inspected output in a generic MonitorNode typed to the wire. Without Item<T> monitor registrations, an Item<Vector> output could only be monitored after an Item to List promotion, so the Data panel captured and displayed a List<Vector> despite the connector being Item<Vector>. Register monitors for the Item types the element-wise nodes emit, and add the matching Data panel downcast entries.
* Color and double Item/List wires and cleave layer-stack connectors in the node graph
* Route wire color and rank through hidden nodes and refresh them on type changes
* Rework the DashPattern connector conversions with element-wise promotion and an explicit reducer node
* Rank the remaining value, context, aggregation, and transform nodes onto Item<T> wires
* Back DashPattern with a List<f64> so the Data panel can introspect its lengths
* Carry a single Item<T> through varargs so the Read context nodes emit Item<T> not List<T>
* Relax rank validation for aggregation shapes, add element adapters, and match variants by fewest promotions
* Rank the remaining bare and unnecessarily-List connectors across the node catalog
* Add Graphic::None and the FillChoice paint value, making colors and gradients plain values
* Rename GradientStops to Gradient and the legacy Gradient/Fill structs to LegacyGradient/LegacyFill
* Restore generator frame-from-params ranking to the roadmap as a planned stage
* Rename the ranked-field adapter identifier from PromoteNode to FieldAdapterNode to reflect its full contract
* Unload only the wires whose displayed style changed when types update
* Peel wire rank in the editor's semantic type checks so rank-0 layers are recognized
* Restore the whole-List Transform variant so rank-1 content wires resolve again
* Register the Item wire forms for the Memoize and Context Modification infrastructure nodes
* Give every ranked connector a field adapter and add numeric cast variants for legacy wires
* Key a ranked param's type default off its Item wire form when no literal default exists
* Inherit the layer's content value when splicing a node into an empty chain
* Migrate stale List-form TypeDefault inputs to the definition's current default
* Generate the mapped wire variant only when the element-wise node has a frame source
* Let a bare wire feed a List connector via a wrap-raise adapter, costed as two rank steps
* Add a zip companion to the whole-List Transform so ranked List parameters pair per slot
* Add the Sum, Average, Minimum, Maximum, Any, and All list reducers
* Convert the measure family to element-wise Item kernels per the audit classification
* Prefer the bare element value over the Item type default so ranked params keep their widgets
* Rename GradientStopsUI to GradientUI
* Split Fill's optional transform into a _has_transform bool and a ranked _transform matrix
* Rename the migration-only OptionalDAffine2 TaggedValue to LegacyOptionalDAffine2
* Flow byte buffers as Item<Resource> instead of List<u8> across the byte nodes
* Macro-generate the list-content wire variant, retiring the hand-written Transform-zip, Area, and Centroid companions
* Let ()-primary generators take ranked params and frame over them via the mapped variant, ranking Circle's radius
* Rank the vector shape generators' params to Item, adding a rank-aware input grab to the introspection harness
* Rank the value, color, and text generator params to Item
* Rank the raster, web-request, and context-reader generator params to Item
* Fix the repeat and brush test wirings left behind by the param-ranking sweeps
* Delete the vestigial Some, Unwrap Option, and Size Of debug nodes
* Delete the Attach Attribute node, folding its role into Write Attribute
* Add the Filter and Sort list companion nodes
* Guard the removed-definition migration swap target with a test
* Add the Box Corners value type in place of the rectangle corner radius list
* Split Text to Vector's per-glyph mode into a Text to Vector Glyphs node
* Rank the Combine Channels node's channel connectors to Item
* Make Map Points an element-wise node
* Delete the deprecated Upload Texture node
* Update the implementation roadmap to reflect the landed stages
* Let monitor introspection read rank-0 wires, locking in the layer coercion promotion path
* Prefer the rank-0 default when disconnecting a rank-capable input
* Make Path Modify an element-wise node
* Wrap node paths in a NodeIdPath newtype so they flow as a single Item
* Give Item<Raster<CPU>> a default so an unconnected Brush background resolves
* Stop the Brush node from setting layer attributes its paint operation doesn't produce
* Present-gate Flatten Path's adopted layer path like its fill and stroke
* Gate carried layer attributes on static column presence, not runtime values
* Give the remaining graphic Item<T> types a default so unconnected primaries resolve
* Dispatch a ranked param's Properties widget from its rank-0 element type
* Make Extract Transform an element-wise node, restoring the Origins to Polyline body
* Rename Flatten Path to Combine Paths
* Stamp Legacy Layer Extend's adopted layer path as a readable NodeIdPath
* Drop the dead List<u8> and List<NodeId> wire rows
* Rank Flatten Graphic's Fully Flatten toggle to Item
* Update the implementation roadmap with the endgame scope
* Make Combine Paths a reducer that collapses the whole frame into one path
* Stop type-converter nodes from carrying the source's unrelated attributes
* Format the Origins to Polyline regression test
* Wrap the Brush node's trace in a BrushTrace newtype so it flows as one value
* Make Switch a framed element-wise select, bundling whole collections
* Widen and align element-type coverage across the list and graphic nodes
* Register the compiler's cache chain pair for every ranked enum and newtype wire
* Fix wire colors for Passthrough outputs, bundled lists, and bools, and widen list wires
* Represent List wire types structurally with Type::List, replacing name-parsed rank promotion
* Treat scope and data fields as environment, rank scope wires as Item, and feed the render boundary through a context vararg
* Delete the vestigial Clone debug node
* Reinstate Upload Texture as an element-wise node and fix the GPU variants' scope executor and rank adapters
* Rename Combine Paths back to Flatten Path, deferring that rename to its own PR
* Deduplicate the promotion adapter registrations into the field adapter macro
* Rank Write Attribute's value connector to Item<AttributeValueDyn>, retiring the UnwrapItem bridge
* Vertical wire styling
* Store the editor layer path attribute as a bare NodeIdPath, not an Item<NodeIdPath>
* Rank Context Modification's features connector to Item<ContextFeatures>, dropping the dead memoize row
* Rank Path Modify's modification parameter to Item<Box<VectorModification>>
* Rename the field adapter node family to input adapter
* Drop the dead bare scalar rows from Context Modification's implementations list
* Move the dynamic executor's test module into its own file
* Drop the registry's unreachable bare rows for Memoize, the cache chain, and ConvertNode
* Materialize stored TaggedValues as ranked Item wires at the source
* Remove the bare-wire promotion and adapter machinery made dead by ranked value materialization
* Plant the input adapter for List-only inputs, composing position conversion from standard rows
* Consolidate Into/Convert conversions into the input adapter umbrella and rename the rank adapter identifiers
* Fix grouped layers gaining a phantom None stack element from the FillChoice default hijacking every List<Graphic> disconnect
* Enforce ranked node inputs in the macro, rejecting bare wire declarations
* Remove the unit Context => () machinery rows, leaving () purely as the no-primary sentinel
* Add a --signatures rank-audit mode to node-docs for the ranked-wire migration
* Remove the node-docs --signatures rank-audit mode now that ranked wires are enforced
* Migrate legacy no-color values on the Black & White, Color Overlay, and Empty Image color inputs
* Rewrite the element-wise accessor wire type at the primary input, not raw index 0
* Register the cache chain for Resource wires, replacing the lone hand-written Monitor row
* Gate the remaining Raster<GPU> registry rows behind the gpu feature
* Let List<DVec2> wires erase to ListDyn for the attribute reader and element counter
* Rename Extract Element to Item at Index, Count Elements to List Length, and Omit Element to Remove at Index
* Store paint picks as plain color/gradient values, removing the FillChoice value type
* Code review restructuring
* Sort by the consumed sort_key attribute or natural element order, adding the Sort Key node
* Remove the new list-combinator and reducer nodes to defer them to a follow-up PR
* Parse Fill and Stroke color defaults through the paint wire's Graphic element
* Emit ranked implementation-row default types structurally so their element TypeIds survive to default-literal parsing
* Exempt the deliberate no-paint choice from the stale List-form TypeDefault migration
* Migrate the legacy 4-input Fill directly to the split has-transform shape
* Upgrade the demo artwork
* Fix the valid AI review findings: Item eq/hash contract, table-era no-paint migration, quantize List rows, and other smaller issues
* Remove the rank polymorphism working documents
* Hash Item attribute values directly instead of debug-formatting them, speeding up cached evaluation
* Replace the data panel's dead bare-wire downcast arms with full coverage of the ranked monitor row types
* Derive PartialEq for Item now that attributes participate in equality
* Extend the data panel's attribute dispatchers with the newly supported scalar and choice enum types
* Add List monitor rows for the framed numeric conversion outputs so inspecting them resolves, with matching data panel arms
This commit is contained in:
2
.jjconflict-base-0/.branding
Normal file
2
.jjconflict-base-0/.branding
Normal file
@@ -0,0 +1,2 @@
|
||||
https://github.com/Keavon/graphite-branded-assets/archive/0d004aa61e6b48d316e8e5db6d59ccc4788f192d.tar.gz
|
||||
772d64518be43c99977ba56f69e574531c56e83d2df2f42ab066f77f74b0dd1f
|
||||
22
.jjconflict-base-0/.cargo/config.toml
Normal file
22
.jjconflict-base-0/.cargo/config.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
# Keep `--cfg=web_sys_unstable_apis` here so the wasm wrapper crates build with the same web-sys API signatures (e.g. `put_image_data`/`get_image_data` taking `i32` rather than `f64`) on both native and the wasm target.
|
||||
[target.'cfg(not(target_family = "wasm"))']
|
||||
rustflags = ["--cfg=web_sys_unstable_apis"]
|
||||
|
||||
[target.'cfg(target_family = "wasm")']
|
||||
rustflags = [
|
||||
# Currently disabled because of https://github.com/GraphiteEditor/Graphite/issues/1262
|
||||
# The current simd implementation leads to undefined behavior
|
||||
#"-C",
|
||||
#"target-feature=+simd128",
|
||||
"-C",
|
||||
"target-feature=+bulk-memory",
|
||||
"-C",
|
||||
"link-arg=--max-memory=4294967296",
|
||||
"--cfg=web_sys_unstable_apis",
|
||||
# TODO: Remove this and find a better way to stay within the 25MB limit of cloudflare pages
|
||||
"-C",
|
||||
"opt-level=s",
|
||||
]
|
||||
|
||||
[env]
|
||||
CARGO_WORKSPACE_DIR = { value = "", relative = true }
|
||||
31
.jjconflict-base-0/.devcontainer/devcontainer.json
Normal file
31
.jjconflict-base-0/.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/base:debian",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"profile": "default"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/node:1": {}
|
||||
},
|
||||
"onCreateCommand": "cargo install cargo-about && cargo install -f wasm-bindgen-cli@0.2.121",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
// NOTE: Keep this in sync with `.vscode/extensions.json`
|
||||
"extensions": [
|
||||
// Rust
|
||||
"rust-lang.rust-analyzer",
|
||||
"tamasfe.even-better-toml",
|
||||
// Web
|
||||
"dbaeumer.vscode-eslint",
|
||||
"svelte.svelte-vscode",
|
||||
"vitaliymaz.vscode-svg-previewer",
|
||||
// Code quality
|
||||
"wayou.vscode-todo-highlight",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
// Helpful
|
||||
"mhutchie.git-graph",
|
||||
"qezhu.gitlink",
|
||||
"wmaurer.change-case"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
7
.jjconflict-base-0/.editorconfig
Normal file
7
.jjconflict-base-0/.editorconfig
Normal file
@@ -0,0 +1,7 @@
|
||||
[*.{rs,js,ts,svelte,json,toml,svg,html,css,scss}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 200
|
||||
1
.jjconflict-base-0/.envrc
Normal file
1
.jjconflict-base-0/.envrc
Normal file
@@ -0,0 +1 @@
|
||||
use flake
|
||||
10
.jjconflict-base-0/.gitattributes
vendored
Normal file
10
.jjconflict-base-0/.gitattributes
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Requires Git to check out files with the LF (not CRLF) line endings for files it automatically recognizes as being text-based
|
||||
# The `*` targets all files
|
||||
# The `text=auto` makes it apply a conversion only to files detected as text-based
|
||||
# The `eol=lf` sets the conversion to an LF line ending
|
||||
# https://git-scm.com/docs/gitattributes
|
||||
* text=auto eol=lf
|
||||
|
||||
# Adds syntax highlighting to Graphite files on GitHub and minimizes diffs both locally and on GitHub
|
||||
*.graphite binary linguist-generated linguist-language=JSON
|
||||
/node-graph/graphene-cli/test_files/*.graphite text diff -linguist-generated
|
||||
1
.jjconflict-base-0/.github/FUNDING.yml
vendored
Normal file
1
.jjconflict-base-0/.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
github: [GraphiteEditor]
|
||||
13
.jjconflict-base-0/.github/pull_request_template.md
vendored
Normal file
13
.jjconflict-base-0/.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<!--
|
||||
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
|
||||
|
||||
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.
|
||||
-->
|
||||
793
.jjconflict-base-0/.github/workflows/build.yml
vendored
Normal file
793
.jjconflict-base-0/.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,793 @@
|
||||
name: "Build"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- latest-stable
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
web:
|
||||
description: "Web"
|
||||
type: boolean
|
||||
windows:
|
||||
description: "Windows"
|
||||
type: boolean
|
||||
mac:
|
||||
description: "Mac"
|
||||
type: boolean
|
||||
linux:
|
||||
description: "Linux"
|
||||
type: boolean
|
||||
debug:
|
||||
description: "Debug build"
|
||||
type: boolean
|
||||
workflow_call:
|
||||
inputs:
|
||||
web:
|
||||
type: boolean
|
||||
windows:
|
||||
type: boolean
|
||||
mac:
|
||||
type: boolean
|
||||
linux:
|
||||
type: boolean
|
||||
debug:
|
||||
type: boolean
|
||||
checkout_repo:
|
||||
type: string
|
||||
checkout_ref:
|
||||
type: string
|
||||
pr_number:
|
||||
type: string
|
||||
merge_queue:
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
web:
|
||||
if: github.event_name == 'push' || inputs.web
|
||||
runs-on: [self-hosted, target/wasm]
|
||||
permissions:
|
||||
contents: write
|
||||
deployments: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTC_WRAPPER: /usr/bin/sccache
|
||||
CARGO_INCREMENTAL: 0
|
||||
SCCACHE_DIR: /var/lib/github-actions/.cache
|
||||
WASM_BINDGEN_CLI_VERSION: "0.2.121"
|
||||
BINARYEN_VERSION: "130"
|
||||
steps:
|
||||
- name: 📥 Clone repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ inputs.checkout_repo || github.repository }}
|
||||
ref: ${{ inputs.checkout_ref || '' }}
|
||||
|
||||
- name: 🟢 Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: 🦀 Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
cache: false
|
||||
rustflags: ""
|
||||
target: wasm32-unknown-unknown
|
||||
|
||||
- name: 🚧 Install wasm-bindgen-cli and Binaryen wasm-opt
|
||||
run: |
|
||||
if ! wasm-bindgen --version 2>/dev/null | grep -qF "$WASM_BINDGEN_CLI_VERSION"; then
|
||||
cargo install -f "wasm-bindgen-cli@$WASM_BINDGEN_CLI_VERSION"
|
||||
fi
|
||||
|
||||
BINARYEN_DIR="$HOME/.cache/binaryen-version_$BINARYEN_VERSION"
|
||||
if [ ! -x "$BINARYEN_DIR/bin/wasm-opt" ]; then
|
||||
mkdir -p "$BINARYEN_DIR"
|
||||
curl -sSfL "https://github.com/WebAssembly/binaryen/releases/download/version_$BINARYEN_VERSION/binaryen-version_$BINARYEN_VERSION-x86_64-linux.tar.gz" | tar xz -C "$BINARYEN_DIR" --strip-components=1
|
||||
fi
|
||||
echo "$BINARYEN_DIR/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: 🔀 Choose production deployment environment and insert template
|
||||
id: production-env
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
if [[ "${{ github.ref }}" == "refs/tags/latest-stable" ]]; then
|
||||
echo "cf_project=graphite-editor" >> $GITHUB_OUTPUT
|
||||
DOMAIN="editor.graphite.art"
|
||||
else
|
||||
echo "cf_project=graphite-dev" >> $GITHUB_OUTPUT
|
||||
DOMAIN="dev.graphite.art"
|
||||
fi
|
||||
TEMPLATE="<script defer data-domain=\"$DOMAIN\" data-api=\"https://graphite.art/visit/event\" src=\"https://graphite.art/visit/script.hash.js\"></script>"
|
||||
echo "template=$TEMPLATE" >> $GITHUB_OUTPUT
|
||||
sed -i "s|<!-- INDEX_HTML_HEAD_REPLACEMENT -->|$TEMPLATE|" frontend/index.html
|
||||
|
||||
- name: 🌐 Build Graphite web code
|
||||
env:
|
||||
NODE_ENV: production
|
||||
# Split the Wasm binary to fit Cloudflare Pages' file size limit (see `wasmSplitting` in `frontend/vite.config.ts`)
|
||||
SPLIT_WASM: "1"
|
||||
run: mold -run cargo run build web${{ inputs.debug && ' debug' || '' }}
|
||||
|
||||
- name: 📤 Publish to Cloudflare Pages
|
||||
if: inputs.merge_queue == false
|
||||
id: cloudflare
|
||||
continue-on-error: ${{ github.event_name != 'push' }}
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
run: |
|
||||
if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
|
||||
echo "No Cloudflare API token available (fork PR), skipping deploy."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
DEPLOY_BRANCH="master"
|
||||
else
|
||||
DEPLOY_BRANCH="${{ inputs.checkout_ref || github.head_ref || github.ref_name }}"
|
||||
if [ "$DEPLOY_BRANCH" = "master" ] || [ -z "$DEPLOY_BRANCH" ]; then
|
||||
DEPLOY_BRANCH="preview-${{ github.run_id }}"
|
||||
fi
|
||||
fi
|
||||
|
||||
MAX_ATTEMPTS=8
|
||||
DELAY=15
|
||||
for ATTEMPT in $(seq 1 $MAX_ATTEMPTS); do
|
||||
echo "Attempt $ATTEMPT of $MAX_ATTEMPTS..."
|
||||
npx wrangler@3 pages deploy "frontend/dist" --project-name="${{ steps.production-env.outputs.cf_project || 'graphite-dev' }}" --branch="$DEPLOY_BRANCH" --commit-dirty=true 2>&1 | tee /tmp/wrangler_output
|
||||
if [ ${PIPESTATUS[0]} -eq 0 ]; then
|
||||
URL=$(grep -oP 'https://[^\s]+\.pages\.dev' /tmp/wrangler_output | head -1)
|
||||
echo "url=$URL" >> "$GITHUB_OUTPUT"
|
||||
echo "Published successfully: $URL"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $ATTEMPT failed."
|
||||
if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then
|
||||
echo "Retrying in ${DELAY}s..."
|
||||
sleep $DELAY
|
||||
DELAY=$((DELAY * 2))
|
||||
fi
|
||||
done
|
||||
echo "All $MAX_ATTEMPTS Cloudflare Pages publish attempts failed."
|
||||
exit 1
|
||||
|
||||
- name: 🚀 Create a GitHub environment deployment
|
||||
if: (inputs.checkout_repo == '' || inputs.checkout_repo == github.repository) && inputs.merge_queue == false
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CF_URL: ${{ steps.cloudflare.outputs.url }}
|
||||
run: |
|
||||
if [ -z "$CF_URL" ]; then
|
||||
echo "No Cloudflare URL available, skipping deployment."
|
||||
exit 0
|
||||
fi
|
||||
if [ "${{ github.ref }}" = "refs/tags/latest-stable" ]; then
|
||||
REF="latest-stable"
|
||||
ENVIRONMENT="graphite-editor (Production)"
|
||||
AUTO_INACTIVE="true"
|
||||
TRANSIENT_ENVIRONMENT="false"
|
||||
elif [ "${{ github.event_name }}" = "push" ]; then
|
||||
REF="master"
|
||||
ENVIRONMENT="graphite-dev (Production)"
|
||||
AUTO_INACTIVE="true"
|
||||
TRANSIENT_ENVIRONMENT="false"
|
||||
else
|
||||
REF="${{ inputs.checkout_ref || github.head_ref || github.ref_name }}"
|
||||
ENVIRONMENT="graphite-dev (Preview)"
|
||||
AUTO_INACTIVE="false"
|
||||
TRANSIENT_ENVIRONMENT="true"
|
||||
fi
|
||||
create_deployment() {
|
||||
gh api \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
repos/${{ github.repository }}/deployments \
|
||||
--input - \
|
||||
--jq '.id' <<EOF
|
||||
{
|
||||
"ref": "$1",
|
||||
"environment": "$ENVIRONMENT",
|
||||
"auto_merge": false,
|
||||
"required_contexts": [],
|
||||
"auto_inactive": $AUTO_INACTIVE,
|
||||
"transient_environment": $TRANSIENT_ENVIRONMENT
|
||||
}
|
||||
EOF
|
||||
}
|
||||
# Try branch name first (needed for GitHub's PR "View deployment" button), fall back to commit SHA if the branch was deleted
|
||||
DEPLOY_ID=$(create_deployment "$REF" 2>/dev/null) || DEPLOY_ID=$(create_deployment "$(git rev-parse HEAD)")
|
||||
gh api \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
repos/${{ github.repository }}/deployments/$DEPLOY_ID/statuses \
|
||||
-f state=success \
|
||||
-f environment_url="$CF_URL" \
|
||||
-f log_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
- name: 💬 Comment with the build link
|
||||
if: github.event_name != 'pull_request' && inputs.merge_queue == false
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CF_URL: ${{ steps.cloudflare.outputs.url }}
|
||||
run: |
|
||||
if [ -z "$CF_URL" ]; then
|
||||
echo "No Cloudflare URL available, skipping comment."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
size_of() { find frontend/dist/assets "$@" -printf '%s\n' | awk '{s+=$1} END {printf "%.2f MB", s/1048576}'; }
|
||||
WASM_SIZE=$(size_of -name '*.wasm')
|
||||
JS_SIZE=$(size_of -name '*.js')
|
||||
CSS_SIZE=$(size_of -name '*.css')
|
||||
FONT_SIZE=$(size_of \( -name '*.woff2' -o -name '*.woff' -o -name '*.ttf' -o -name '*.otf' \))
|
||||
IMAGE_SIZE=$(size_of \( -name '*.png' -o -name '*.jpg' -o -name '*.svg' \))
|
||||
ALL_SIZE=$(size_of -type f)
|
||||
|
||||
COMMENT_BODY="| 📦 **Web Build Complete for** $(git rev-parse HEAD) |
|
||||
|-|
|
||||
| $CF_URL |
|
||||
|
||||
Wasm: **$WASM_SIZE** — JS: **$JS_SIZE** — CSS: **$CSS_SIZE** — Fonts: **$FONT_SIZE** — Images: **$IMAGE_SIZE** — All Assets: **$ALL_SIZE**"
|
||||
|
||||
if [ "${{ github.ref }}" = "refs/tags/latest-stable" ]; then
|
||||
# Push tag: skip commenting (commit was already commented on master merge)
|
||||
echo "Tag push, skipping comment."
|
||||
elif [ "${{ github.event_name }}" = "push" ]; then
|
||||
# Push master: comment on the commit hash page
|
||||
gh api \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
repos/${{ github.repository }}/commits/$(git rev-parse HEAD)/comments \
|
||||
-f body="$COMMENT_BODY"
|
||||
elif [ "${{ github.event_name }}" != "pull_request" ]; then
|
||||
# Manual trigger (workflow_dispatch, !build): comment on the PR
|
||||
PR_NUMBER="${{ inputs.pr_number }}"
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "$COMMENT_BODY"
|
||||
else
|
||||
echo "No open PR found, skipping comment."
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: ✂ Strip template from completed build for a clean artifact
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
TEMPLATE: ${{ steps.production-env.outputs.template }}
|
||||
run: sed -i "s|$TEMPLATE||" frontend/dist/index.html
|
||||
|
||||
- name: 📦 Upload web bundle artifact
|
||||
if: github.event_name != 'pull_request' && inputs.merge_queue == false
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: graphite-web-bundle
|
||||
path: frontend/dist
|
||||
|
||||
- name: 👕 Lint Graphite web formatting
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: |
|
||||
cd frontend
|
||||
npm run check
|
||||
|
||||
- name: 📃 Trigger website rebuild if auto-generated code docs are stale
|
||||
if: github.event_name == 'push' && github.ref != 'refs/tags/latest-stable'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
cargo run -p editor-message-tree -- website/generated
|
||||
TREE=volunteer/guide/codebase-overview/hierarchical-message-system-tree
|
||||
curl -sf "https://graphite.art/$TREE.txt" -o "website/static/$TREE.live.txt" \
|
||||
&& diff -q "website/static/$TREE.txt" "website/static/$TREE.live.txt" > /dev/null \
|
||||
|| gh workflow run website.yml --ref master
|
||||
|
||||
windows:
|
||||
if: (github.event_name == 'push' && github.ref != 'refs/tags/latest-stable') || inputs.windows
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
WASM_BINDGEN_CLI_VERSION: "0.2.121"
|
||||
BINARYEN_VERSION: "130"
|
||||
CARGO_ABOUT_VERSION: "0.9.0"
|
||||
|
||||
steps:
|
||||
- name: 📥 Clone repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ inputs.checkout_repo || github.repository }}
|
||||
ref: ${{ inputs.checkout_ref || '' }}
|
||||
|
||||
- name: 🦀 Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
cache: false
|
||||
rustflags: ""
|
||||
target: wasm32-unknown-unknown
|
||||
|
||||
- name: 💾 Set up Cargo cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: 🟢 Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
frontend/package-lock.json
|
||||
|
||||
- name: 📦 Install 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 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
|
||||
|
||||
curl.exe -sSfL -o "$env:RUNNER_TEMP\binaryen.tar.gz" "https://github.com/WebAssembly/binaryen/releases/download/version_$env:BINARYEN_VERSION/binaryen-version_$env:BINARYEN_VERSION-x86_64-windows.tar.gz"
|
||||
tar -xzf "$env:RUNNER_TEMP\binaryen.tar.gz" -C $env:RUNNER_TEMP
|
||||
"$env:RUNNER_TEMP\binaryen-version_$env:BINARYEN_VERSION\bin" | Out-File -FilePath $env:GITHUB_PATH -Append
|
||||
|
||||
cargo binstall --no-confirm --force "cargo-about@$env:CARGO_ABOUT_VERSION"
|
||||
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${{ inputs.debug && ' debug' || '' }}
|
||||
|
||||
- name: 📁 Stage artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
PROFILE=${{ inputs.debug && 'debug' || 'release' }}
|
||||
rm -rf target/artifacts
|
||||
mkdir -p target/artifacts
|
||||
cp -R target/$PROFILE/Graphite target/artifacts/Graphite
|
||||
|
||||
- name: 📦 Upload Windows bundle
|
||||
if: github.event_name != 'push'
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: graphite-windows-bundle
|
||||
path: target/artifacts
|
||||
|
||||
- name: 💬 Comment artifact link on PR
|
||||
if: github.event_name != 'push'
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ARTIFACT_ID=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-windows-bundle") | .id')
|
||||
ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID"
|
||||
PR_NUMBER="${{ inputs.pr_number }}"
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true)
|
||||
fi
|
||||
if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_ID" ]; then
|
||||
BODY="| 📦 **Windows Build Complete for** $(git rev-parse HEAD) |"$'\n'
|
||||
BODY+="|-|"$'\n'
|
||||
BODY+="| [Download binary]($ARTIFACT_URL) |"
|
||||
gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "$BODY"
|
||||
fi
|
||||
|
||||
- name: 🔑 Azure login
|
||||
if: github.event_name == 'push'
|
||||
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.event_name == 'push'
|
||||
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.event_name == 'push'
|
||||
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 signed Windows bundle
|
||||
if: github.event_name == 'push'
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: graphite-windows-bundle-signed
|
||||
path: target/artifacts
|
||||
|
||||
mac:
|
||||
if: (github.event_name == 'push' && github.ref != 'refs/tags/latest-stable') || inputs.mac
|
||||
runs-on: macos-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
WASM_BINDGEN_CLI_VERSION: "0.2.121"
|
||||
BINARYEN_VERSION: "130"
|
||||
CARGO_ABOUT_VERSION: "0.9.0"
|
||||
|
||||
steps:
|
||||
- name: 📥 Clone repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ inputs.checkout_repo || github.repository }}
|
||||
ref: ${{ inputs.checkout_ref || '' }}
|
||||
|
||||
- name: 🦀 Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
cache: false
|
||||
rustflags: ""
|
||||
target: wasm32-unknown-unknown
|
||||
|
||||
- name: 💾 Set up Cargo cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: 🟢 Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
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 \
|
||||
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
|
||||
|
||||
BINARYEN_DIR="$HOME/.cache/binaryen-version_$BINARYEN_VERSION"
|
||||
if [ ! -x "$BINARYEN_DIR/bin/wasm-opt" ]; then
|
||||
mkdir -p "$BINARYEN_DIR"
|
||||
curl -sSfL "https://github.com/WebAssembly/binaryen/releases/download/version_$BINARYEN_VERSION/binaryen-version_$BINARYEN_VERSION-arm64-macos.tar.gz" | tar xz -C "$BINARYEN_DIR" --strip-components=1
|
||||
fi
|
||||
echo "$BINARYEN_DIR/bin" >> "$GITHUB_PATH"
|
||||
|
||||
cargo binstall --no-confirm --force "cargo-about@${CARGO_ABOUT_VERSION}"
|
||||
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${{ inputs.debug && ' debug' || '' }}
|
||||
|
||||
- name: 📁 Stage artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
PROFILE=${{ inputs.debug && 'debug' || 'release' }}
|
||||
rm -rf target/artifacts
|
||||
mkdir -p target/artifacts
|
||||
cp -R target/$PROFILE/Graphite.app target/artifacts/Graphite.app
|
||||
|
||||
- name: 📦 Upload Mac bundle
|
||||
if: github.event_name != 'push'
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: graphite-mac-bundle
|
||||
path: target/artifacts
|
||||
|
||||
- name: 💬 Comment artifact link on PR
|
||||
if: github.event_name != 'push'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ARTIFACT_ID=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-mac-bundle") | .id')
|
||||
ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID"
|
||||
PR_NUMBER="${{ inputs.pr_number }}"
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true)
|
||||
fi
|
||||
if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_ID" ]; then
|
||||
BODY="| 📦 **Mac Build Complete for** $(git rev-parse HEAD) |"$'\n'
|
||||
BODY+="|-|"$'\n'
|
||||
BODY+="| [Download binary]($ARTIFACT_URL) |"
|
||||
gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "$BODY"
|
||||
fi
|
||||
|
||||
- name: 🔏 Sign and notarize (preparation)
|
||||
if: github.event_name == 'push'
|
||||
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
|
||||
if: github.event_name == 'push'
|
||||
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 signed Mac bundle
|
||||
if: github.event_name == 'push'
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: graphite-mac-bundle-signed
|
||||
path: target/artifacts
|
||||
|
||||
linux:
|
||||
if: (github.event_name == 'push' && github.ref != 'refs/tags/latest-stable') || inputs.linux
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: 📥 Clone repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ inputs.checkout_repo || github.repository }}
|
||||
ref: ${{ inputs.checkout_ref || '' }}
|
||||
|
||||
- name: ❄ Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
extra-conf: |
|
||||
extra-substituters = https://graphite.cachix.org https://graphite-dev.cachix.org
|
||||
extra-trusted-public-keys = graphite.cachix.org-1:B7Il1yMpkquN/dXM+5GRmz+4Xmu2aaCS1GcWNfFhsOo= graphite-dev.cachix.org-1:RppXYpiV1qO2TYKTkXXGHsAEQDOB5G51b3VlrN9QmbI=
|
||||
|
||||
- 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 .#graphite${{ inputs.debug && '-dev' || '' }} --no-link --print-out-paths --print-build-logs
|
||||
|
||||
- name: 📤 Push to Nix cache
|
||||
env:
|
||||
NIX_CACHE_AUTH_TOKEN: ${{ (!inputs.debug && github.ref == 'refs/heads/master') && secrets.NIX_CACHE_AUTH_TOKEN || secrets.NIX_CACHE_AUTH_TOKEN_DEV }}
|
||||
NIX_CACHE_NAME: ${{ (!inputs.debug && github.ref == 'refs/heads/master') && 'graphite' || 'graphite-dev' }}
|
||||
run: |
|
||||
nix run nixpkgs#cachix -- authtoken $NIX_CACHE_AUTH_TOKEN
|
||||
nix build .#graphite${{ inputs.debug && '-dev' || '' }} --no-link --print-out-paths | nix run nixpkgs#cachix -- push $NIX_CACHE_NAME
|
||||
|
||||
- name: 📤 Push Dependencies to dev Nix cache
|
||||
env:
|
||||
NIX_CACHE_AUTH_TOKEN: ${{ secrets.NIX_CACHE_AUTH_TOKEN_DEV }}
|
||||
NIX_CACHE_NAME: graphite-dev
|
||||
run: |
|
||||
nix run nixpkgs#cachix -- authtoken $NIX_CACHE_AUTH_TOKEN
|
||||
nix build .#graphite${{ inputs.debug && '-dev' || '' }}.deps --no-link --print-out-paths | nix run nixpkgs#cachix -- push $NIX_CACHE_NAME
|
||||
|
||||
- name: 🏗 Build Linux bundle
|
||||
run: nix build .#graphite-bundle${{ inputs.debug && '-dev' || '' }}.tar.xz && cp ./result ./graphite-linux-bundle.tar.xz
|
||||
|
||||
- name: 📦 Upload Linux bundle
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: graphite-linux-bundle
|
||||
path: graphite-linux-bundle.tar.xz
|
||||
compression-level: 0
|
||||
|
||||
- name: 💬 Comment artifact link on PR
|
||||
id: linux-comment
|
||||
if: github.event_name != 'push'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ARTIFACT_ID=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-linux-bundle") | .id')
|
||||
ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID"
|
||||
PR_NUMBER="${{ inputs.pr_number }}"
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true)
|
||||
fi
|
||||
if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_ID" ]; then
|
||||
BODY="| 📦 **Linux Build Complete for** $(git rev-parse HEAD) |"$'\n'
|
||||
BODY+="|-|"$'\n'
|
||||
BODY+="| [Download binary]($ARTIFACT_URL) |"
|
||||
COMMENT_ID=$(gh api repos/${{ github.repository }}/issues/$PR_NUMBER/comments -f body="$BODY" --jq '.id')
|
||||
echo "comment_id=$COMMENT_ID" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: 🔧 Install 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${{ inputs.debug && '-dev' || '' }}
|
||||
|
||||
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 package
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: graphite-flatpak
|
||||
path: .flatpak/Graphite.flatpak
|
||||
compression-level: 0
|
||||
|
||||
- name: 💬 Update PR comment with Flatpak artifact link
|
||||
if: github.event_name != 'push' && steps.linux-comment.outputs.comment_id
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ARTIFACT_ID=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-flatpak") | .id')
|
||||
ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID"
|
||||
COMMENT_ID="${{ steps.linux-comment.outputs.comment_id }}"
|
||||
if [ -n "$ARTIFACT_ID" ]; then
|
||||
EXISTING_BODY=$(gh api repos/${{ github.repository }}/issues/comments/$COMMENT_ID --jq '.body')
|
||||
BODY="$EXISTING_BODY"$'\n'
|
||||
BODY+="| [Download Flatpak]($ARTIFACT_URL) |"
|
||||
gh api repos/${{ github.repository }}/issues/comments/$COMMENT_ID -X PATCH -f body="$BODY"
|
||||
fi
|
||||
26
.jjconflict-base-0/.github/workflows/cargo-deny.yml
vendored
Normal file
26
.jjconflict-base-0/.github/workflows/cargo-deny.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: "Audit Security Advisories"
|
||||
|
||||
on:
|
||||
# Run once each week
|
||||
schedule:
|
||||
- cron: "0 0 * * 0"
|
||||
|
||||
jobs:
|
||||
cargo-deny:
|
||||
if: github.repository == 'GraphiteEditor/Graphite' # Don't run on forks by default
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 📥 Clone repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🔒 Check crate security advisories for root workspace
|
||||
uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
command: check advisories
|
||||
|
||||
- name: 🔒 Check crate security advisories for /libraries/rawkit
|
||||
uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
command: check advisories
|
||||
manifest-path: libraries/rawkit/Cargo.toml
|
||||
103
.jjconflict-base-0/.github/workflows/check.yml
vendored
Normal file
103
.jjconflict-base-0/.github/workflows/check.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
name: "Check"
|
||||
|
||||
on:
|
||||
pull_request: {}
|
||||
merge_group: {}
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
# Check if CI can be skipped (for merge queue deduplication)
|
||||
skip-check:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
skip: ${{ steps.check.outputs.skip-check }}
|
||||
steps:
|
||||
- name: 📥 Clone repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🚦 Check if CI can be skipped
|
||||
id: check
|
||||
uses: cariad-tech/merge-queue-ci-skipper@cf80db21fc70244e36487acc531b3f1118889b0a
|
||||
|
||||
# Build the web app via the shared build workflow
|
||||
build:
|
||||
needs: skip-check
|
||||
if: needs.skip-check.outputs.skip != 'true'
|
||||
uses: ./.github/workflows/build.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
web: true
|
||||
merge_queue: ${{ github.event_name == 'merge_group' }}
|
||||
|
||||
# Run the Rust tests on the self-hosted native runner
|
||||
test:
|
||||
needs: skip-check
|
||||
if: needs.skip-check.outputs.skip != 'true'
|
||||
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 repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🦀 Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
cache: false
|
||||
rustflags: ""
|
||||
|
||||
- name: 🦀 Fetch Rust dependencies
|
||||
run: cargo fetch --locked
|
||||
|
||||
- name: 🧪 Run Rust tests
|
||||
env:
|
||||
# `--cfg=web_sys_unstable_apis` mirrors the `[build]` section of `.cargo/config.toml`
|
||||
RUSTFLAGS: "-Dwarnings --cfg=web_sys_unstable_apis"
|
||||
run: mold -run cargo test --all-features
|
||||
|
||||
# Rust format check on GitHub runner
|
||||
rust-fmt:
|
||||
needs: skip-check
|
||||
if: needs.skip-check.outputs.skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 📥 Clone repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🦀 Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
cache: false
|
||||
rustflags: ""
|
||||
components: rustfmt
|
||||
|
||||
- name: 🔬 Check Rust formatting
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
# License compatibility check on GitHub runner
|
||||
check-licenses:
|
||||
needs: skip-check
|
||||
if: needs.skip-check.outputs.skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 📥 Clone repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- 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
|
||||
137
.jjconflict-base-0/.github/workflows/comment-!build-commands.yml
vendored
Normal file
137
.jjconflict-base-0/.github/workflows/comment-!build-commands.yml
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
# USAGE:
|
||||
# After reviewing the code, core team members may comment on a PR with `!build` followed by optional `<target>` and `<profile>` arguments.
|
||||
# This matches the syntax of the `cargo run build` CLI command, but allows platforms to be specified.
|
||||
#
|
||||
# `<target>`: `web` (default), `desktop` (all platforms), or `desktop:<platforms>` (subset of `windows+mac+linux`)
|
||||
# `<profile>`: `release` (default) or `debug`
|
||||
#
|
||||
# Examples:
|
||||
# - !build
|
||||
# - !build debug
|
||||
# - !build desktop
|
||||
# - !build desktop:windows+mac
|
||||
# - !build desktop:linux debug
|
||||
name: "!build PR Command"
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
# Command should be limited to core team members (those in the organization) for security.
|
||||
# From the GitHub Actions docs:
|
||||
# author_association = 'MEMBER': Author is a member of the organization that owns the repository.
|
||||
if: >
|
||||
github.event.issue.pull_request &&
|
||||
github.event.comment.author_association == 'MEMBER' &&
|
||||
startsWith(github.event.comment.body, '!build')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
outputs:
|
||||
repo: ${{ steps.pr_info.outputs.repo }}
|
||||
ref: ${{ steps.pr_info.outputs.ref }}
|
||||
web: ${{ steps.pr_info.outputs.web }}
|
||||
windows: ${{ steps.pr_info.outputs.windows }}
|
||||
mac: ${{ steps.pr_info.outputs.mac }}
|
||||
linux: ${{ steps.pr_info.outputs.linux }}
|
||||
debug: ${{ steps.pr_info.outputs.debug }}
|
||||
|
||||
steps:
|
||||
- name: 🔎 Parse command, find branch, and set build flags
|
||||
id: pr_info
|
||||
run: |
|
||||
COMMENT="${{ github.event.comment.body }}"
|
||||
|
||||
# Split into space-separated words
|
||||
read -ra WORDS <<< "$COMMENT"
|
||||
|
||||
# First word must be "!build"
|
||||
if [[ "${WORDS[0]}" != "!build" ]]; then
|
||||
echo "::error::Expected comment to start with !build"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Initialize build flags (web defaults to true, matching the CLI default target)
|
||||
WEB="true"
|
||||
WINDOWS="false"
|
||||
MAC="false"
|
||||
LINUX="false"
|
||||
DEBUG="false"
|
||||
|
||||
# Parse target (optional, defaults to `web` if omitted)
|
||||
IDX=1
|
||||
case "${WORDS[$IDX]:-}" in
|
||||
# Target: `web` enables just the web build (already the default, but accepted explicitly)
|
||||
"web")
|
||||
((IDX++)) ;;
|
||||
# Target: `desktop` enables all three desktop platforms
|
||||
"desktop")
|
||||
WEB="false"; WINDOWS="true"; MAC="true"; LINUX="true"; ((IDX++)) ;;
|
||||
# Target: `desktop:<platforms>` enables a subset of desktop platforms, split by `+`
|
||||
desktop:*)
|
||||
WEB="false"
|
||||
PLATFORMS="${WORDS[$IDX]#desktop:}"
|
||||
IFS='+' read -ra PARTS <<< "$PLATFORMS"
|
||||
for PART in "${PARTS[@]}"; do
|
||||
case "$PART" in
|
||||
"windows") WINDOWS="true" ;;
|
||||
"mac") MAC="true" ;;
|
||||
"linux") LINUX="true" ;;
|
||||
*) echo "::error::Unrecognized platform: $PART"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
((IDX++))
|
||||
;;
|
||||
esac
|
||||
|
||||
# Parse profile (optional, defaults to `release` if omitted)
|
||||
case "${WORDS[$IDX]:-}" in
|
||||
"debug") DEBUG="true"; ((IDX++)) ;;
|
||||
"release") ((IDX++)) ;;
|
||||
esac
|
||||
|
||||
# Reject any unexpected trailing words
|
||||
if [[ $IDX -lt ${#WORDS[@]} ]]; then
|
||||
echo "::error::Unexpected argument: ${WORDS[$IDX]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Write parsed build flags to job outputs
|
||||
echo "web=$WEB" >> $GITHUB_OUTPUT
|
||||
echo "windows=$WINDOWS" >> $GITHUB_OUTPUT
|
||||
echo "mac=$MAC" >> $GITHUB_OUTPUT
|
||||
echo "linux=$LINUX" >> $GITHUB_OUTPUT
|
||||
echo "debug=$DEBUG" >> $GITHUB_OUTPUT
|
||||
|
||||
# Fetch the PR's head branch and repo (needed for forked PRs where the code lives in another repo)
|
||||
RESPONSE=$(curl -L -H 'Accept: application/vnd.github+json' -H 'X-GitHub-Api-Version: 2022-11-28' https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.issue.number }})
|
||||
echo "repo=$(echo $RESPONSE | jq -r '.head.repo.full_name')" >> $GITHUB_OUTPUT
|
||||
echo "ref=$(echo $RESPONSE | jq -r '.head.ref')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 💬 Edit comment with workflow run link
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.updateComment({
|
||||
comment_id: ${{ github.event.comment.id }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '${{ github.event.comment.body }} ([Run ID ' + context.runId + '](https://github.com/GraphiteEditor/Graphite/actions/runs/' + context.runId + '))'
|
||||
});
|
||||
|
||||
invoke-build:
|
||||
needs: setup
|
||||
uses: ./.github/workflows/build.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
web: ${{ needs.setup.outputs.web == 'true' }}
|
||||
windows: ${{ needs.setup.outputs.windows == 'true' }}
|
||||
mac: ${{ needs.setup.outputs.mac == 'true' }}
|
||||
linux: ${{ needs.setup.outputs.linux == 'true' }}
|
||||
debug: ${{ needs.setup.outputs.debug == 'true' }}
|
||||
checkout_repo: ${{ needs.setup.outputs.repo }}
|
||||
checkout_ref: ${{ needs.setup.outputs.ref }}
|
||||
pr_number: ${{ github.event.issue.number }}
|
||||
103
.jjconflict-base-0/.github/workflows/comment-clippy-warnings.yaml
vendored
Normal file
103
.jjconflict-base-0/.github/workflows/comment-clippy-warnings.yaml
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
name: "Clippy Check"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
|
||||
jobs:
|
||||
clippy:
|
||||
name: Run Clippy
|
||||
runs-on: ubuntu-latest
|
||||
# TODO(Keavon): Find a workaround (passing the output text to a separate action with permission to read the secrets?) that allows this to work on fork PRs
|
||||
if: false
|
||||
# if: ${{ !github.event.pull_request.draft && !github.event.pull_request.head.repo.fork }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: clippy
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install libgtk-3-dev libsoup2.4-dev libjavascriptcoregtk-4.0-dev libwebkit2gtk-4.0-dev
|
||||
|
||||
- name: Run Clippy
|
||||
id: clippy
|
||||
run: |
|
||||
# Run Clippy and filter output for the root workspace
|
||||
CLIPPY_OUTPUT=$(cargo clippy --all-targets --all-features -- -W clippy::all 2>&1 | grep -vE "^(\s*Updating|\s*Download|\s*Compiling|\s*Checking|Finished)")
|
||||
|
||||
# Run Clippy and filter output for /libraries/rawkit
|
||||
cd libraries/rawkit
|
||||
CLIPPY_OUTPUT+=$'\n\n'
|
||||
CLIPPY_OUTPUT+=$(cargo clippy --all-targets --all-features -- -W clippy::all 2>&1 | grep -vE "^(\s*Updating|\s*Download|\s*Compiling|\s*Checking|Finished)")
|
||||
cd ../..
|
||||
|
||||
# Escape special characters for JSON
|
||||
ESCAPED_OUTPUT=$(echo "$CLIPPY_OUTPUT" | jq -sR .)
|
||||
echo "CLIPPY_OUTPUT=$ESCAPED_OUTPUT" >> $GITHUB_OUTPUT
|
||||
if echo "$CLIPPY_OUTPUT" | grep -qE "^(warning|error)"; then
|
||||
echo "CLIPPY_ISSUES_FOUND=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "CLIPPY_ISSUES_FOUND=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Delete previous comments
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
|
||||
const botComments = comments.filter((comment) =>
|
||||
comment.user.type === 'Bot' && comment.body.includes('Clippy Warnings/Errors')
|
||||
);
|
||||
|
||||
for (const comment of botComments) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: comment.id,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Comment PR
|
||||
if: steps.clippy.outputs.CLIPPY_ISSUES_FOUND == 'true'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const clippy_output = ${{ steps.clippy.outputs.CLIPPY_OUTPUT }};
|
||||
const output = `
|
||||
<details open>
|
||||
|
||||
<summary>Found Clippy warnings</summary>
|
||||
|
||||
#### Clippy Warnings/Errors
|
||||
|
||||
\`\`\`
|
||||
${clippy_output}
|
||||
\`\`\`
|
||||
|
||||
</details>
|
||||
`;
|
||||
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: output
|
||||
})
|
||||
336
.jjconflict-base-0/.github/workflows/comment-profiling-changes.yaml
vendored
Normal file
336
.jjconflict-base-0/.github/workflows/comment-profiling-changes.yaml
vendored
Normal file
@@ -0,0 +1,336 @@
|
||||
name: "Profiling Changes"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "node-graph/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
profile:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
# Cache on Cargo.lock file
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install gungraun-runner and valgrind
|
||||
uses: gungraun/setup-gungraun@v1
|
||||
|
||||
- name: Checkout master branch
|
||||
run: |
|
||||
git fetch origin master:master
|
||||
git checkout master
|
||||
|
||||
- name: Get master commit SHA
|
||||
id: master-sha
|
||||
run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get CPU info
|
||||
id: cpu-info
|
||||
run: |
|
||||
# Get CPU model and create a short hash for cache key
|
||||
CPU_MODEL=$(cat /proc/cpuinfo | grep "model name" | head -1 | cut -d: -f2 | xargs)
|
||||
CPU_HASH=$(echo "$CPU_MODEL" | sha256sum | cut -c1-8)
|
||||
echo "cpu-hash=$CPU_HASH" >> $GITHUB_OUTPUT
|
||||
echo "CPU: $CPU_MODEL (hash: $CPU_HASH)"
|
||||
|
||||
- name: Cache benchmark baselines
|
||||
id: cache-benchmark-baselines
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: target/gungraun
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-${{ steps.cpu-info.outputs.cpu-hash }}-gungraun-benchmark-baselines-master-${{ steps.master-sha.outputs.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.cpu-info.outputs.cpu-hash }}-gungraun-benchmark-baselines-master-
|
||||
|
||||
- name: Run baseline benchmarks
|
||||
if: steps.cache-benchmark-baselines.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
# Compile benchmarks
|
||||
cargo bench --bench compile_demo_art_gungraun -- --save-baseline=master
|
||||
|
||||
# Runtime benchmarks
|
||||
cargo bench --bench update_executor_gungraun -- --save-baseline=master
|
||||
cargo bench --bench run_once_gungraun -- --save-baseline=master
|
||||
cargo bench --bench run_cached_gungraun -- --save-baseline=master
|
||||
|
||||
- name: Checkout PR branch
|
||||
run: git checkout ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Run PR benchmarks
|
||||
run: |
|
||||
# Compile benchmarks
|
||||
cargo bench --bench compile_demo_art_gungraun -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/compile_output.json
|
||||
|
||||
# Runtime benchmarks
|
||||
cargo bench --bench update_executor_gungraun -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/update_output.json
|
||||
cargo bench --bench run_once_gungraun -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/run_once_output.json
|
||||
cargo bench --bench run_cached_gungraun -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/run_cached_output.json
|
||||
|
||||
- name: Make old comments collapsed by default
|
||||
# Only run if we have write permissions (not a fork)
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
|
||||
const botComments = comments.filter((comment) =>
|
||||
comment.user.type === 'Bot' && comment.body.includes('Performance Benchmark Results') && comment.body.includes('<details open>')
|
||||
);
|
||||
|
||||
for (const comment of botComments) {
|
||||
// Edit the comment to remove the "open" attribute from the <details> tag
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: comment.id,
|
||||
body: comment.body.replace('<details open>', '<details>')
|
||||
});
|
||||
}
|
||||
|
||||
- name: Analyze profiling changes
|
||||
id: analyze
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
function isSignificantChange(diffPct, absoluteChange, benchmarkType) {
|
||||
const meetsPercentageThreshold = Math.abs(diffPct) > 5;
|
||||
const meetsAbsoluteThreshold = absoluteChange > 200000;
|
||||
const isCachedExecution = benchmarkType === 'run_cached' ||
|
||||
benchmarkType.includes('Cached Execution');
|
||||
|
||||
return isCachedExecution
|
||||
? (meetsPercentageThreshold && meetsAbsoluteThreshold)
|
||||
: meetsPercentageThreshold;
|
||||
}
|
||||
|
||||
const allOutputs = [
|
||||
JSON.parse(fs.readFileSync('/tmp/compile_output.json', 'utf8')),
|
||||
JSON.parse(fs.readFileSync('/tmp/update_output.json', 'utf8')),
|
||||
JSON.parse(fs.readFileSync('/tmp/run_once_output.json', 'utf8')),
|
||||
JSON.parse(fs.readFileSync('/tmp/run_cached_output.json', 'utf8'))
|
||||
];
|
||||
const outputNames = ['compile', 'update', 'run_once', 'run_cached'];
|
||||
const sectionTitles = ['Compilation', 'Update', 'Run Once', 'Cached Execution'];
|
||||
|
||||
let hasSignificantChanges = false;
|
||||
let hasRegressions = false;
|
||||
let regressionDetails = [];
|
||||
|
||||
for (let i = 0; i < allOutputs.length; i++) {
|
||||
const benchmarkOutput = allOutputs[i];
|
||||
const outputName = outputNames[i];
|
||||
const sectionTitle = sectionTitles[i];
|
||||
|
||||
for (const benchmark of benchmarkOutput) {
|
||||
if (benchmark.profiles?.[0]?.summaries?.parts?.[0]?.metrics_summary?.Callgrind?.Ir?.diffs?.diff_pct) {
|
||||
const diffPct = parseFloat(benchmark.profiles[0].summaries.parts[0].metrics_summary.Callgrind.Ir.diffs.diff_pct);
|
||||
const oldValue = benchmark.profiles[0].summaries.parts[0].metrics_summary.Callgrind.Ir.metrics.Both[1].Int;
|
||||
const newValue = benchmark.profiles[0].summaries.parts[0].metrics_summary.Callgrind.Ir.metrics.Both[0].Int;
|
||||
const absoluteChange = Math.abs(newValue - oldValue);
|
||||
|
||||
if (isSignificantChange(diffPct, absoluteChange, outputName)) {
|
||||
hasSignificantChanges = true;
|
||||
|
||||
// Only an increase in instruction count is a regression; improvements must not fail CI.
|
||||
if (diffPct > 0) {
|
||||
hasRegressions = true;
|
||||
regressionDetails.push({
|
||||
module_path: benchmark.module_path,
|
||||
id: benchmark.id,
|
||||
diffPct,
|
||||
absoluteChange,
|
||||
sectionTitle
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
core.setOutput('has-significant-changes', hasSignificantChanges);
|
||||
core.setOutput('has-regressions', hasRegressions);
|
||||
core.setOutput('regression-details', JSON.stringify(regressionDetails));
|
||||
|
||||
- name: Comment PR
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
const compileOutput = JSON.parse(fs.readFileSync('/tmp/compile_output.json', 'utf8'));
|
||||
const updateOutput = JSON.parse(fs.readFileSync('/tmp/update_output.json', 'utf8'));
|
||||
const runOnceOutput = JSON.parse(fs.readFileSync('/tmp/run_once_output.json', 'utf8'));
|
||||
const runCachedOutput = JSON.parse(fs.readFileSync('/tmp/run_cached_output.json', 'utf8'));
|
||||
|
||||
const hasSignificantChanges = '${{ steps.analyze.outputs.has-significant-changes }}' === 'true';
|
||||
let commentBody = "";
|
||||
|
||||
function formatNumber(num) {
|
||||
return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
function formatPercentage(pct) {
|
||||
const sign = pct >= 0 ? '+' : '';
|
||||
return `${sign}${pct.toFixed(2)}%`;
|
||||
}
|
||||
|
||||
function padRight(str, len) {
|
||||
return str.padEnd(len);
|
||||
}
|
||||
|
||||
function padLeft(str, len) {
|
||||
return str.padStart(len);
|
||||
}
|
||||
|
||||
function processBenchmarkOutput(benchmarkOutput, sectionTitle, isLast = false) {
|
||||
let sectionBody = "";
|
||||
let hasResults = false;
|
||||
let hasSignificantChanges = false;
|
||||
|
||||
function isSignificantChange(diffPct, absoluteChange, benchmarkType) {
|
||||
const meetsPercentageThreshold = Math.abs(diffPct) > 5;
|
||||
const meetsAbsoluteThreshold = absoluteChange > 200000;
|
||||
const isCachedExecution = benchmarkType === 'run_cached' ||
|
||||
benchmarkType.includes('Cached Execution');
|
||||
|
||||
return isCachedExecution
|
||||
? (meetsPercentageThreshold && meetsAbsoluteThreshold)
|
||||
: meetsPercentageThreshold;
|
||||
}
|
||||
|
||||
for (const benchmark of benchmarkOutput) {
|
||||
if (benchmark.profiles && benchmark.profiles.length > 0) {
|
||||
const profile = benchmark.profiles[0];
|
||||
if (profile.summaries && profile.summaries.parts && profile.summaries.parts.length > 0) {
|
||||
const part = profile.summaries.parts[0];
|
||||
if (part.metrics_summary && part.metrics_summary.Callgrind && part.metrics_summary.Callgrind.Ir) {
|
||||
const irData = part.metrics_summary.Callgrind.Ir;
|
||||
if (irData.diffs && irData.diffs.diff_pct !== null) {
|
||||
const irDiff = {
|
||||
diff_pct: parseFloat(irData.diffs.diff_pct),
|
||||
old: irData.metrics.Both[1].Int,
|
||||
new: irData.metrics.Both[0].Int
|
||||
};
|
||||
hasResults = true;
|
||||
const changePercentage = formatPercentage(irDiff.diff_pct);
|
||||
const color = irDiff.diff_pct > 0 ? "red" : "lime";
|
||||
|
||||
sectionBody += `**${benchmark.module_path} ${benchmark.id}:${benchmark.details}**\n`;
|
||||
sectionBody += `Instructions: \`${formatNumber(irDiff.old)}\` (master) → \`${formatNumber(irDiff.new)}\` (HEAD) : `;
|
||||
sectionBody += `$$\\color{${color}}${changePercentage.replace("%", "\\\\%")}$$\n\n`;
|
||||
|
||||
sectionBody += "<details>\n<summary>Detailed metrics</summary>\n\n```\n";
|
||||
sectionBody += `Baselines: master| HEAD\n`;
|
||||
|
||||
for (const [metricName, metricData] of Object.entries(part.metrics_summary.Callgrind)) {
|
||||
if (metricData.diffs && metricData.diffs.diff_pct !== null) {
|
||||
const changePercentage = formatPercentage(parseFloat(metricData.diffs.diff_pct));
|
||||
const oldValue = metricData.metrics.Both[1].Int || metricData.metrics.Both[1].Float;
|
||||
const newValue = metricData.metrics.Both[0].Int || metricData.metrics.Both[0].Float;
|
||||
const line = `${padRight(metricName, 20)} ${padLeft(formatNumber(Math.round(oldValue)), 11)}|${padLeft(formatNumber(Math.round(newValue)), 11)} ${padLeft(changePercentage, 15)}`;
|
||||
sectionBody += `${line}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
sectionBody += "```\n</details>\n\n";
|
||||
|
||||
if (isSignificantChange(irDiff.diff_pct, Math.abs(irDiff.new - irDiff.old), sectionTitle)) {
|
||||
significantChanges = true;
|
||||
hasSignificantChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasResults) {
|
||||
// Wrap section in collapsible details, open only if there are significant changes
|
||||
const openAttribute = hasSignificantChanges ? " open" : "";
|
||||
const ruler = isLast ? "" : "\n\n---";
|
||||
return `<details${openAttribute}>\n<summary><h2>${sectionTitle}</h2></summary>\n\n${sectionBody}${ruler}\n</details>`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Process each benchmark category
|
||||
const sections = [
|
||||
{ output: compileOutput, title: "🔧 Graph Compilation" },
|
||||
{ output: updateOutput, title: "🔄 Executor Update" },
|
||||
{ output: runOnceOutput, title: "🚀 Render: Cold Execution" },
|
||||
{ output: runCachedOutput, title: "⚡ Render: Cached Execution" }
|
||||
];
|
||||
|
||||
// Generate sections and determine which ones have results
|
||||
const generatedSections = sections.map(({ output, title }) =>
|
||||
processBenchmarkOutput(output, title, true) // temporarily mark all as last
|
||||
).filter(section => section.length > 0);
|
||||
|
||||
// Re-generate with correct isLast flags
|
||||
let sectionIndex = 0;
|
||||
const finalSections = sections.map(({ output, title }) => {
|
||||
const section = processBenchmarkOutput(output, title, true); // check if it has results
|
||||
if (section.length > 0) {
|
||||
const isLast = sectionIndex === generatedSections.length - 1;
|
||||
sectionIndex++;
|
||||
return processBenchmarkOutput(output, title, isLast);
|
||||
}
|
||||
return "";
|
||||
}).filter(section => section.length > 0);
|
||||
|
||||
// Combine all sections
|
||||
commentBody = finalSections.join("\n\n");
|
||||
|
||||
if (commentBody.length > 0) {
|
||||
const output = `<details open>\n<summary>Performance Benchmark Results</summary>\n\n${commentBody}\n</details>`;
|
||||
|
||||
if (hasSignificantChanges) {
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: output
|
||||
});
|
||||
} else {
|
||||
console.log("No significant performance changes detected. Skipping comment.");
|
||||
console.log(output);
|
||||
}
|
||||
} else {
|
||||
console.log("No benchmark results to display.");
|
||||
}
|
||||
|
||||
- name: Fail on significant regressions
|
||||
if: steps.analyze.outputs.has-regressions == 'true'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const regressionDetails = JSON.parse('${{ steps.analyze.outputs.regression-details }}');
|
||||
const firstRegression = regressionDetails[0];
|
||||
|
||||
core.setFailed(`Significant performance regression detected: ${firstRegression.module_path} ${firstRegression.id} increased by ${firstRegression.absoluteChange.toLocaleString()} instructions (${firstRegression.diffPct.toFixed(2)}%)`);
|
||||
66
.jjconflict-base-0/.github/workflows/library-rawkit.yml
vendored
Normal file
66
.jjconflict-base-0/.github/workflows/library-rawkit.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: "Library: Rawkit"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "libraries/rawkit/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "libraries/rawkit/**"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
CARGO_INCREMENTAL: 0
|
||||
SCCACHE_DIR: /var/lib/github-actions/.cache
|
||||
|
||||
steps:
|
||||
- name: 📥 Clone repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🦀 Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
cache: false
|
||||
rustflags: ""
|
||||
|
||||
- name: 📦 Run sccache-cache
|
||||
uses: mozilla-actions/sccache-action@v0.0.6
|
||||
continue-on-error: true
|
||||
|
||||
- name: 🔧 Fallback if sccache fails
|
||||
if: failure()
|
||||
run: |
|
||||
echo "sccache failed, disabling it"
|
||||
echo "RUSTC_WRAPPER=" >> $GITHUB_ENV
|
||||
|
||||
- name: 🔬 Check Rust formatting
|
||||
run: |
|
||||
cd libraries/rawkit
|
||||
cargo fmt --all -- --check
|
||||
|
||||
- name: 🦀 Build Rust code
|
||||
run: |
|
||||
cd libraries/rawkit
|
||||
cargo build --release --all-features
|
||||
|
||||
- name: 🧪 Run Rust tests
|
||||
run: |
|
||||
cd libraries/rawkit
|
||||
cargo test --release --all-features
|
||||
|
||||
- name: 📈 Run sccache stat for check
|
||||
shell: bash
|
||||
run: sccache --show-stats || echo "sccache stats unavailable"
|
||||
51
.jjconflict-base-0/.github/workflows/nix.yml
vendored
Normal file
51
.jjconflict-base-0/.github/workflows/nix.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: "Nix Housekeeping"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
cache-dev-shell:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: 📥 Clone repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ inputs.checkout_repo || github.repository }}
|
||||
ref: ${{ inputs.checkout_ref || '' }}
|
||||
|
||||
- name: ❄ Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
extra-conf: |
|
||||
extra-substituters = https://graphite.cachix.org https://graphite-dev.cachix.org
|
||||
extra-trusted-public-keys = graphite.cachix.org-1:B7Il1yMpkquN/dXM+5GRmz+4Xmu2aaCS1GcWNfFhsOo= graphite-dev.cachix.org-1:RppXYpiV1qO2TYKTkXXGHsAEQDOB5G51b3VlrN9QmbI=
|
||||
|
||||
- name: 🔎 Check whether development shell is already in binary cache
|
||||
id: cache-check
|
||||
run: |
|
||||
out_path="$(nix eval --raw .#devShells.x86_64-linux.default.outPath)"
|
||||
if nix path-info --store https://graphite-dev.cachix.org "$out_path" &>/dev/null; then
|
||||
echo "cached=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Development shell is already cached at $out_path"
|
||||
else
|
||||
echo "cached=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Development shell is not cached"
|
||||
fi
|
||||
|
||||
- name: 📦 Build Nix development shell
|
||||
if: steps.cache-check.outputs.cached == 'false'
|
||||
run: nix build .#devShells.x86_64-linux.default --no-link --print-out-paths
|
||||
|
||||
- name: 📤 Push Nix development shell to binary cache
|
||||
if: steps.cache-check.outputs.cached == 'false'
|
||||
env:
|
||||
NIX_CACHE_AUTH_TOKEN: ${{ secrets.NIX_CACHE_AUTH_TOKEN_DEV }}
|
||||
run: |
|
||||
nix run nixpkgs#cachix -- authtoken $NIX_CACHE_AUTH_TOKEN
|
||||
nix build .#devShells.x86_64-linux.default --no-link --print-out-paths | nix run nixpkgs#cachix -- push graphite-dev
|
||||
45
.jjconflict-base-0/.github/workflows/provide-shaders.yml
vendored
Normal file
45
.jjconflict-base-0/.github/workflows/provide-shaders.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: "Provide Shaders"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: 📥 Clone repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: ❄ Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
extra-conf: |
|
||||
extra-substituters = https://graphite.cachix.org https://graphite-dev.cachix.org
|
||||
extra-trusted-public-keys = graphite.cachix.org-1:B7Il1yMpkquN/dXM+5GRmz+4Xmu2aaCS1GcWNfFhsOo= graphite-dev.cachix.org-1:RppXYpiV1qO2TYKTkXXGHsAEQDOB5G51b3VlrN9QmbI=
|
||||
|
||||
- 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 }}
|
||||
|
||||
- name: 📤 Push shaders to dev Nix cache
|
||||
env:
|
||||
NIX_CACHE_AUTH_TOKEN: ${{ secrets.NIX_CACHE_AUTH_TOKEN_DEV }}
|
||||
NIX_CACHE_NAME: graphite-dev
|
||||
run: |
|
||||
nix run nixpkgs#cachix -- authtoken $NIX_CACHE_AUTH_TOKEN
|
||||
nix build .#graphite-raster-nodes-shaders --no-link --print-out-paths | nix run nixpkgs#cachix -- push $NIX_CACHE_NAME
|
||||
89
.jjconflict-base-0/.github/workflows/scripts/artifact-upload.bash
vendored
Normal file
89
.jjconflict-base-0/.github/workflows/scripts/artifact-upload.bash
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 <owner> <repo> <branch> <target-path> <artifact-file> <commit-message> <github-token>
|
||||
|
||||
Arguments:
|
||||
owner : GitHub user or organization of the target repo
|
||||
repo : Target repo name
|
||||
branch : Branch name (e.g. main)
|
||||
target-path : Full path (including folders + filename) in the target repo where to upload
|
||||
artifact-file : Local file path to upload
|
||||
commit-message : Commit message for creating/updating the file
|
||||
github-token : GitHub token (PAT or equivalent) with write access to the target repo
|
||||
|
||||
This will perform a GitHub API PUT to /repos/{owner}/{repo}/contents/{target-path}.
|
||||
If a file already exists at that path, it will auto-detect the SHA and update; otherwise it will create a new one.
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ $# -ne 7 ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
OWNER="$1"
|
||||
REPO="$2"
|
||||
BRANCH="$3"
|
||||
TARGET_PATH="$4"
|
||||
ARTIFACT_PATH="$5"
|
||||
COMMIT_MSG="$6"
|
||||
TOKEN="$7"
|
||||
|
||||
if [ ! -f "$ARTIFACT_PATH" ]; then
|
||||
echo "Error: artifact file not found: $ARTIFACT_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOCAL_SHA=$(git hash-object "$ARTIFACT_PATH")
|
||||
echo "Local blob SHA: $LOCAL_SHA"
|
||||
|
||||
GET_URL="https://api.github.com/repos/${OWNER}/${REPO}/contents/${TARGET_PATH}?ref=${BRANCH}"
|
||||
GET_RESPONSE=$(curl -s -H "Authorization: token ${TOKEN}" "$GET_URL")
|
||||
|
||||
REMOTE_SHA=$(echo "$GET_RESPONSE" | jq -r .sha 2>/dev/null || echo "")
|
||||
|
||||
if [ "$REMOTE_SHA" != "null" ] && [ -n "$REMOTE_SHA" ]; then
|
||||
echo "Remote blob SHA: $REMOTE_SHA"
|
||||
if [ "$LOCAL_SHA" = "$REMOTE_SHA" ]; then
|
||||
echo "The remote file is identical. Skipping upload."
|
||||
exit 0
|
||||
else
|
||||
echo "Remote file differs. Preparing to upload."
|
||||
fi
|
||||
else
|
||||
echo "No existing remote file or no SHA found. Creating."
|
||||
fi
|
||||
|
||||
CONTENT_TMP_BASE64=$(mktemp)
|
||||
if base64 --help 2>&1 | grep -q -- "-w"; then
|
||||
base64 -w 0 "$ARTIFACT_PATH" > "$CONTENT_TMP_BASE64"
|
||||
else
|
||||
base64 "$ARTIFACT_PATH" | tr -d '\n' > "$CONTENT_TMP_BASE64"
|
||||
fi
|
||||
|
||||
PAYLOAD_TMP=$(mktemp)
|
||||
jq -n \
|
||||
--arg message "$COMMIT_MSG" \
|
||||
--arg branch "$BRANCH" \
|
||||
--arg sha "$REMOTE_SHA" \
|
||||
--rawfile content "$CONTENT_TMP_BASE64" \
|
||||
'{
|
||||
message: $message,
|
||||
content: $content,
|
||||
branch: $branch
|
||||
} + (if ($sha != "" and $sha != "null") then { sha: $sha } else {} end)' \
|
||||
> "$PAYLOAD_TMP"
|
||||
|
||||
UPLOAD_RESPONSE=$(curl -s -X PUT \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @"$PAYLOAD_TMP" \
|
||||
"https://api.github.com/repos/${OWNER}/${REPO}/contents/${TARGET_PATH}")
|
||||
|
||||
echo "Upload Response:"
|
||||
echo "$UPLOAD_RESPONSE"
|
||||
|
||||
rm -f "$CONTENT_TMP_BASE64" "$PAYLOAD_TMP"
|
||||
79
.jjconflict-base-0/.github/workflows/website.yml
vendored
Normal file
79
.jjconflict-base-0/.github/workflows/website.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
name: "Website"
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- website/**
|
||||
pull_request:
|
||||
paths:
|
||||
- website/**
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
INDEX_HTML_HEAD_INCLUSION: <script defer data-domain="graphite.art" data-api="/visit/event" src="/visit/script.hash.js"></script>
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: 📥 Clone repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 🟢 Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: 🕸 Install Zola
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: zola@0.22.0
|
||||
|
||||
- name: 🔍 Check if `website/other` directory changed
|
||||
uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
website-other:
|
||||
- "website/other/**"
|
||||
|
||||
- name: ✂ Replace template in <head> of index.html
|
||||
run: |
|
||||
# Remove the INDEX_HTML_HEAD_INCLUSION environment variable for build links (not master deploys)
|
||||
git rev-parse --abbrev-ref HEAD | grep master > /dev/null || export INDEX_HTML_HEAD_INCLUSION=""
|
||||
|
||||
- name: 🦀 Produce auto-generated code docs data
|
||||
run: |
|
||||
rustup update stable
|
||||
cargo run -p crate-hierarchy-viz -- website/generated
|
||||
cargo run -p editor-message-tree -- website/generated
|
||||
|
||||
- name: 🔧 Install website npm dependencies
|
||||
run: |
|
||||
cd website
|
||||
npm ci
|
||||
|
||||
- name: 📃 Generate node catalog documentation
|
||||
run: cargo run -p node-docs -- website/content/learn/node-catalog
|
||||
|
||||
- name: 🌐 Build Graphite website with Zola
|
||||
env:
|
||||
MODE: prod
|
||||
run: |
|
||||
cd website
|
||||
npm run check
|
||||
zola --config config.toml build --minify
|
||||
|
||||
- name: 📤 Publish to Cloudflare Pages
|
||||
continue-on-error: true
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
run: npx wrangler@3 pages deploy "website/public" --project-name="graphite-website" --commit-dirty=true
|
||||
15
.jjconflict-base-0/.gitignore
vendored
Normal file
15
.jjconflict-base-0/.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
branding/
|
||||
target/
|
||||
third-party-licenses.txt*
|
||||
result/
|
||||
.flatpak-builder/
|
||||
*.spv
|
||||
*.exrc
|
||||
perf.data*
|
||||
profile.json
|
||||
profile.json.gz
|
||||
flamegraph.svg
|
||||
.idea/
|
||||
.direnv
|
||||
.DS_Store
|
||||
.nvim.lua
|
||||
79
.jjconflict-base-0/.nix/default.nix
Normal file
79
.jjconflict-base-0/.nix/default.nix
Normal file
@@ -0,0 +1,79 @@
|
||||
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;
|
||||
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-bundle-dev = (lib.call ./pkgs/graphite-bundle.nix) { graphite = graphite-dev; };
|
||||
graphite-flatpak-manifest = (lib.call ./pkgs/graphite-flatpak-manifest.nix) { };
|
||||
graphite-flatpak-manifest-dev = (lib.call ./pkgs/graphite-flatpak-manifest.nix) { graphite-bundle = graphite-bundle-dev; };
|
||||
graphite-cef = lib.call ./pkgs/graphite-cef.nix;
|
||||
|
||||
# TODO: graphene-cli = lib.call ./pkgs/graphene-cli.nix;
|
||||
}
|
||||
);
|
||||
|
||||
devShells = withArgs (
|
||||
{ lib, ... }:
|
||||
{
|
||||
default = lib.call ./dev.nix;
|
||||
}
|
||||
);
|
||||
|
||||
formatter = withArgs ({ pkgs, ... }: pkgs.nixfmt-tree);
|
||||
}
|
||||
28
.jjconflict-base-0/.nix/deps/crane.nix
Normal file
28
.jjconflict-base-0/.nix/deps/crane.nix
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
pkgs,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
lib = (inputs.crane.mkLib pkgs) // {
|
||||
vendorCargoDepsFlatten =
|
||||
src:
|
||||
pkgs.stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
name = "graphite-cargo-vendored";
|
||||
inherit src;
|
||||
|
||||
installPhase = ''
|
||||
cp -rL --no-preserve=mode "$src" "$out"
|
||||
chmod -R u+w "$out"
|
||||
find "$out" -type f -print0 | xargs -r -0 sed -i "s|$src|$out|g"
|
||||
'';
|
||||
|
||||
disallowedReferences = [ finalAttrs.src ];
|
||||
|
||||
dontUnpack = true;
|
||||
dontConfigure = true;
|
||||
dontBuild = true;
|
||||
});
|
||||
};
|
||||
}
|
||||
55
.jjconflict-base-0/.nix/deps/rust-gpu.nix
Normal file
55
.jjconflict-base-0/.nix/deps/rust-gpu.nix
Normal file
@@ -0,0 +1,55 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
toolchain = pkgs.rust-bin.nightly."2026-04-11".default.override {
|
||||
extensions = [
|
||||
"rust-src"
|
||||
"rust-analyzer"
|
||||
"clippy"
|
||||
"cargo"
|
||||
"rustc-dev"
|
||||
"llvm-tools"
|
||||
];
|
||||
};
|
||||
cargo = pkgs.writeShellScriptBin "cargo" ''
|
||||
#!${pkgs.lib.getExe pkgs.bash}
|
||||
|
||||
filtered_args=()
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
+nightly|+nightly-*) ;;
|
||||
*) filtered_args+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
exec ${toolchain}/bin/cargo ${"\${filtered_args[@]}"}
|
||||
'';
|
||||
rustc_codegen_spirv =
|
||||
(pkgs.makeRustPlatform {
|
||||
cargo = toolchain;
|
||||
rustc = toolchain;
|
||||
}).buildRustPackage
|
||||
(finalAttrs: {
|
||||
pname = "rustc_codegen_spirv";
|
||||
version = "0.10.0-alpha.1";
|
||||
src = pkgs.fetchCrate {
|
||||
inherit (finalAttrs) pname version;
|
||||
sha256 = "sha256-zJEpExkPgYzwo7fR4ge4GxJNj7H5yo4bJ4eTOw36+7c=";
|
||||
};
|
||||
cargoHash = "sha256-J1rtbfGqrL2NJ7Bu2pYfDwCdUmnECB/kzxrpYluA0kY=";
|
||||
cargoBuildFlags = [
|
||||
"-p"
|
||||
"rustc_codegen_spirv"
|
||||
"--features=use-compiled-tools"
|
||||
"--no-default-features"
|
||||
];
|
||||
doCheck = false;
|
||||
});
|
||||
in
|
||||
{
|
||||
toolchain = toolchain;
|
||||
env = {
|
||||
RUST_GPU_PATH_OVERRIDE = "${cargo}/bin:${toolchain}/bin";
|
||||
RUSTC_CODEGEN_SPIRV_PATH = "${rustc_codegen_spirv}/lib/librustc_codegen_spirv.so";
|
||||
};
|
||||
}
|
||||
64
.jjconflict-base-0/.nix/dev.nix
Normal file
64
.jjconflict-base-0/.nix/dev.nix
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
pkgs,
|
||||
deps,
|
||||
self,
|
||||
system,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
libs = [
|
||||
pkgs.wayland
|
||||
pkgs.vulkan-loader
|
||||
pkgs.libGL
|
||||
pkgs.openssl
|
||||
pkgs.libraw
|
||||
|
||||
# X11 Support
|
||||
pkgs.libxkbcommon
|
||||
pkgs.libXcursor
|
||||
pkgs.libxcb
|
||||
pkgs.libX11
|
||||
];
|
||||
in
|
||||
pkgs.mkShell (
|
||||
{
|
||||
packages = libs ++ [
|
||||
pkgs.pkg-config
|
||||
|
||||
pkgs.lld
|
||||
pkgs.nodejs
|
||||
pkgs.binaryen
|
||||
pkgs.wasm-bindgen-cli_0_2_121
|
||||
pkgs.cargo-about
|
||||
|
||||
pkgs.rustc
|
||||
pkgs.cargo
|
||||
pkgs.rust-analyzer
|
||||
pkgs.clippy
|
||||
pkgs.rustfmt
|
||||
|
||||
pkgs.git
|
||||
|
||||
pkgs.cargo-nextest
|
||||
pkgs.cargo-expand
|
||||
|
||||
# Linker
|
||||
pkgs.mold
|
||||
|
||||
# Profiling tools
|
||||
pkgs.gnuplot
|
||||
pkgs.samply
|
||||
pkgs.cargo-flamegraph
|
||||
|
||||
# Plotting tools
|
||||
pkgs.graphviz
|
||||
];
|
||||
|
||||
CEF_PATH = self.packages.${system}.graphite-cef;
|
||||
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath libs}:${self.packages.${system}.graphite-cef}";
|
||||
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";
|
||||
|
||||
}
|
||||
// deps.rustGPU.env
|
||||
)
|
||||
20
.jjconflict-base-0/.nix/pkgs/graphite-branding.nix
Normal file
20
.jjconflict-base-0/.nix/pkgs/graphite-branding.nix
Normal file
@@ -0,0 +1,20 @@
|
||||
{ info, pkgs, ... }:
|
||||
|
||||
let
|
||||
brandingTar = pkgs.fetchurl (
|
||||
let
|
||||
lockContent = builtins.readFile "${info.src}/.branding";
|
||||
lines = builtins.filter (s: s != [ ]) (builtins.split "\n" lockContent);
|
||||
url = builtins.elemAt lines 0;
|
||||
hash = builtins.elemAt lines 1;
|
||||
in
|
||||
{
|
||||
url = url;
|
||||
sha256 = hash;
|
||||
}
|
||||
);
|
||||
in
|
||||
pkgs.runCommand "${info.pname}-branding" { } ''
|
||||
mkdir -p $out
|
||||
tar -xvf ${brandingTar} -C $out --strip-components 1
|
||||
''
|
||||
95
.jjconflict-base-0/.nix/pkgs/graphite-bundle.nix
Normal file
95
.jjconflict-base-0/.nix/pkgs/graphite-bundle.nix
Normal file
@@ -0,0 +1,95 @@
|
||||
{
|
||||
pkgs,
|
||||
deps,
|
||||
self,
|
||||
system,
|
||||
...
|
||||
}:
|
||||
{
|
||||
graphite ? self.packages.${system}.graphite,
|
||||
}:
|
||||
let
|
||||
bundle =
|
||||
{
|
||||
archive ? false,
|
||||
compression ? null,
|
||||
passthru ? { },
|
||||
}:
|
||||
(
|
||||
let
|
||||
tar = if compression == null then archive else true;
|
||||
nameArchiveSuffix = if tar then ".tar" else "";
|
||||
nameCompressionSuffix = if compression == null then "" else "." + compression;
|
||||
name = "graphite-bundle${nameArchiveSuffix}${nameCompressionSuffix}";
|
||||
build = ''
|
||||
mkdir -p out
|
||||
mkdir -p out/bin
|
||||
cp ${graphite}/bin/graphite 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 ${self.packages.${system}.graphite-cef.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";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
46
.jjconflict-base-0/.nix/pkgs/graphite-cef.nix
Normal file
46
.jjconflict-base-0/.nix/pkgs/graphite-cef.nix
Normal file
@@ -0,0 +1,46 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
version = "149.0.5+g6770623+chromium-149.0.7827.197";
|
||||
hashes = {
|
||||
aarch64-linux = "sha256-cBAvcvs1rAg5EKJkCt81RZYupCWpUNIC/nLt3PJow7Q=";
|
||||
x86_64-linux = "sha256-OPGMBJmvvLiLdBDniBQwx7LmTGGI59AcesJdILSeqcs=";
|
||||
};
|
||||
|
||||
selectSystem =
|
||||
attrs:
|
||||
attrs.${pkgs.stdenv.hostPlatform.system}
|
||||
or (throw "Unsupported system ${pkgs.stdenv.hostPlatform.system}");
|
||||
|
||||
src = pkgs.fetchurl {
|
||||
url = "https://cef-builds.spotifycdn.com/cef_binary_${version}_${
|
||||
selectSystem {
|
||||
aarch64-linux = "linuxarm64";
|
||||
x86_64-linux = "linux64";
|
||||
}
|
||||
}_minimal.tar.bz2";
|
||||
hash = selectSystem hashes;
|
||||
};
|
||||
in
|
||||
pkgs.cef-binary.overrideAttrs (finalAttrs: {
|
||||
version = builtins.head (builtins.split "\\+" version);
|
||||
inherit src;
|
||||
postInstall = ''
|
||||
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
|
||||
'';
|
||||
})
|
||||
42
.jjconflict-base-0/.nix/pkgs/graphite-flatpak-manifest.nix
Normal file
42
.jjconflict-base-0/.nix/pkgs/graphite-flatpak-manifest.nix
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
pkgs,
|
||||
self,
|
||||
system,
|
||||
...
|
||||
}:
|
||||
{
|
||||
graphite-bundle ? self.packages.${system}.graphite-bundle,
|
||||
}:
|
||||
|
||||
(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 = graphite-bundle.tar;
|
||||
strip-components = 0;
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
info,
|
||||
deps,
|
||||
self,
|
||||
system,
|
||||
...
|
||||
}:
|
||||
|
||||
(deps.crane.lib.overrideToolchain (_: deps.rustGPU.toolchain)).buildPackage {
|
||||
pname = "graphite-raster-nodes-shaders";
|
||||
inherit (info) version src;
|
||||
|
||||
inherit (self.packages.${system}.graphite) cargoVendorDir cargoArtifacts;
|
||||
|
||||
strictDeps = true;
|
||||
|
||||
env = deps.rustGPU.env;
|
||||
|
||||
buildPhase = ''
|
||||
cargo build -r -p raster-nodes-shaders
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
cp target/spirv-builder/spirv-unknown-naga-wgsl/release/deps/raster_nodes_shaders_entrypoint.wgsl $out
|
||||
'';
|
||||
|
||||
doCheck = false;
|
||||
}
|
||||
159
.jjconflict-base-0/.nix/pkgs/graphite.nix
Normal file
159
.jjconflict-base-0/.nix/pkgs/graphite.nix
Normal file
@@ -0,0 +1,159 @@
|
||||
{
|
||||
info,
|
||||
pkgs,
|
||||
self,
|
||||
deps,
|
||||
system,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
dev ? false,
|
||||
}:
|
||||
|
||||
let
|
||||
branding = self.packages.${system}.graphite-branding;
|
||||
libs = [
|
||||
pkgs.wayland
|
||||
pkgs.vulkan-loader
|
||||
pkgs.libGL
|
||||
pkgs.openssl
|
||||
pkgs.libraw
|
||||
# X11 Support
|
||||
pkgs.libxkbcommon
|
||||
pkgs.libXcursor
|
||||
pkgs.libxcb
|
||||
pkgs.libX11
|
||||
];
|
||||
|
||||
common = {
|
||||
inherit (info) pname version src;
|
||||
cargoVendorDir = deps.crane.lib.vendorCargoDepsFlatten (
|
||||
deps.crane.lib.vendorMultipleCargoDeps {
|
||||
inherit (deps.crane.lib.findCargoFiles (deps.crane.lib.cleanCargoSource info.src)) cargoConfigs;
|
||||
cargoLockList = [
|
||||
"${info.src}/Cargo.lock"
|
||||
"${deps.rustGPU.toolchain.availableComponents.rust-src}/lib/rustlib/src/rust/library/Cargo.lock"
|
||||
];
|
||||
}
|
||||
);
|
||||
buildInputs = libs;
|
||||
strictDeps = true;
|
||||
doCheck = false;
|
||||
};
|
||||
|
||||
cargoArtifacts = deps.crane.lib.buildDepsOnly (
|
||||
common
|
||||
// {
|
||||
nativeBuildInputs = [
|
||||
pkgs.pkg-config
|
||||
pkgs.lld
|
||||
];
|
||||
env.CEF_PATH = self.packages.${system}.graphite-cef;
|
||||
buildPhase =
|
||||
let
|
||||
profile = if dev then "dev" else "release";
|
||||
in
|
||||
''
|
||||
cargo check --profile ${profile} --locked -p graphite-desktop-platform-linux
|
||||
cargo build --profile ${profile} --locked -p graphite-desktop-platform-linux
|
||||
|
||||
cargo check --profile ${profile} --target wasm32-unknown-unknown --locked -p graphite-wasm-wrapper --no-default-features --features native
|
||||
cargo build --profile ${profile} --target wasm32-unknown-unknown --locked -p graphite-wasm-wrapper --no-default-features --features native
|
||||
|
||||
cargo check --locked -p third-party-licenses --features desktop
|
||||
cargo build --locked -p third-party-licenses --features desktop
|
||||
|
||||
cargo check --profile ${profile} --locked -p graphite-desktop-bundle
|
||||
cargo build --profile ${profile} --locked -p graphite-desktop-bundle
|
||||
'';
|
||||
}
|
||||
);
|
||||
in
|
||||
|
||||
deps.crane.lib.buildPackage (
|
||||
common
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
|
||||
buildInputs = libs;
|
||||
nativeBuildInputs = [
|
||||
pkgs.pkg-config
|
||||
pkgs.lld
|
||||
pkgs.nodejs
|
||||
pkgs.binaryen
|
||||
pkgs.wasm-bindgen-cli_0_2_121
|
||||
pkgs.cargo-about
|
||||
pkgs.removeReferencesTo
|
||||
pkgs.importNpmLock.npmConfigHook
|
||||
];
|
||||
|
||||
npmDeps = pkgs.importNpmLock {
|
||||
npmRoot = "${info.src}/frontend";
|
||||
};
|
||||
npmRoot = "frontend";
|
||||
makeCacheWritable = true;
|
||||
|
||||
env = {
|
||||
RASTER_NODES_SHADER_PATH = self.packages.${system}.graphite-raster-nodes-shaders;
|
||||
GRAPHITE_GIT_COMMIT_HASH = self.rev or "unknown";
|
||||
GRAPHITE_GIT_COMMIT_DATE = self.lastModified or "unknown";
|
||||
CEF_PATH = self.packages.${system}.graphite-cef;
|
||||
};
|
||||
|
||||
postPatch = ''
|
||||
mkdir branding
|
||||
cp -r ${branding}/* branding
|
||||
cp ${info.src}/.branding branding/.branding
|
||||
'';
|
||||
|
||||
preBuild = ''
|
||||
# Prevent `cargo-run`'s frontend setup from trying to update npm dependencies
|
||||
touch -r frontend/package-lock.json -d '+1 year' frontend/node_modules/.install-timestamp
|
||||
|
||||
export HOME="$TMPDIR"
|
||||
''
|
||||
+ (
|
||||
if self ? rev then
|
||||
''
|
||||
export GRAPHITE_GIT_COMMIT_DATE="$(date -u -d "@$GRAPHITE_GIT_COMMIT_DATE" +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
''
|
||||
else
|
||||
""
|
||||
);
|
||||
|
||||
buildPhaseCargoCommand = "cargo run build desktop${if dev then " debug" else ""}";
|
||||
|
||||
doNotPostBuildInstallCargoBinaries = true;
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
cp target/${if dev then "debug" else "release"}/graphite $out/bin/graphite
|
||||
|
||||
mkdir -p $out/share/applications
|
||||
cp $src/desktop/assets/*.desktop $out/share/applications/
|
||||
|
||||
mkdir -p $out/share/icons/hicolor/scalable/apps
|
||||
cp ${branding}/app-icons/graphite.svg $out/share/icons/hicolor/scalable/apps/art.graphite.Graphite.svg
|
||||
mkdir -p $out/share/icons/hicolor/512x512/apps
|
||||
cp ${branding}/app-icons/graphite-512.png $out/share/icons/hicolor/512x512/apps/art.graphite.Graphite.png
|
||||
mkdir -p $out/share/icons/hicolor/256x256/apps
|
||||
cp ${branding}/app-icons/graphite-256.png $out/share/icons/hicolor/256x256/apps/art.graphite.Graphite.png
|
||||
mkdir -p $out/share/icons/hicolor/128x128/apps
|
||||
cp ${branding}/app-icons/graphite-128.png $out/share/icons/hicolor/128x128/apps/art.graphite.Graphite.png
|
||||
'';
|
||||
|
||||
postFixup = ''
|
||||
remove-references-to -t "${common.cargoVendorDir}" $out/bin/graphite
|
||||
|
||||
patchelf \
|
||||
--set-rpath "${pkgs.lib.makeLibraryPath libs}:${self.packages.${system}.graphite-cef}" \
|
||||
--add-needed libGL.so \
|
||||
--add-needed libEGL.so \
|
||||
$out/bin/graphite
|
||||
'';
|
||||
|
||||
passthru.deps = cargoArtifacts;
|
||||
}
|
||||
)
|
||||
1
.jjconflict-base-0/.nvmrc
Normal file
1
.jjconflict-base-0/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
24
|
||||
18
.jjconflict-base-0/.prettierrc
Normal file
18
.jjconflict-base-0/.prettierrc
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"singleQuote": false,
|
||||
"useTabs": true,
|
||||
"tabWidth": 4,
|
||||
"printWidth": 200,
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.yml",
|
||||
"*.yaml"
|
||||
],
|
||||
"options": {
|
||||
"useTabs": false,
|
||||
"tabWidth": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
20
.jjconflict-base-0/.vscode/extensions.json
vendored
Normal file
20
.jjconflict-base-0/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
// NOTE: Keep this in sync with `.devcontainer/devcontainer.json`
|
||||
"recommendations": [
|
||||
// Rust
|
||||
"rust-lang.rust-analyzer",
|
||||
"tamasfe.even-better-toml",
|
||||
// Web
|
||||
"dbaeumer.vscode-eslint",
|
||||
"svelte.svelte-vscode",
|
||||
"vitaliymaz.vscode-svg-previewer",
|
||||
// Code quality
|
||||
"wayou.vscode-todo-highlight",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
// Git
|
||||
"mhutchie.git-graph",
|
||||
"qezhu.gitlink",
|
||||
// Helpful
|
||||
"wmaurer.change-case"
|
||||
]
|
||||
}
|
||||
48
.jjconflict-base-0/.vscode/launch.json
vendored
Normal file
48
.jjconflict-base-0/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Graphite debug executable",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=graphite",
|
||||
"--package=graphite",
|
||||
],
|
||||
"filter": {
|
||||
"name": "graphite",
|
||||
"kind": "bin",
|
||||
},
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"RUST_LOG": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in executable 'graphite'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--bin=graphite",
|
||||
"--package=graphite",
|
||||
],
|
||||
"filter": {
|
||||
"name": "graphite",
|
||||
"kind": "bin",
|
||||
},
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
},
|
||||
],
|
||||
}
|
||||
75
.jjconflict-base-0/.vscode/settings.json
vendored
Normal file
75
.jjconflict-base-0/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
// Rust: save on format
|
||||
"[rust]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||
},
|
||||
// Web: save on format
|
||||
"[javascript][typescript][svelte]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
// Configured in `.prettierrc`
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json][jsonc][yaml][github-actions-workflow]": {
|
||||
"editor.formatOnSave": true,
|
||||
// Configured in `.prettierrc`
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
// Website: don't format Zola/Tera-templated HTML on save
|
||||
"[html]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
// Handlebars: don't save on format
|
||||
// (`about.hbs` is used by Cargo About to encode license information)
|
||||
"[handlebars]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
// Rust Analyzer config
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
"rust-analyzer.cargo.allTargets": false,
|
||||
"rust-analyzer.procMacro.ignored": {
|
||||
"serde_derive": ["Serialize", "Deserialize"]
|
||||
},
|
||||
// ESLint config
|
||||
"eslint.format.enable": true,
|
||||
"eslint.workingDirectories": ["./frontend", "./website"],
|
||||
"eslint.validate": ["javascript", "typescript", "svelte"],
|
||||
// Git Graph config
|
||||
"git-graph.repository.fetchAndPrune": true,
|
||||
"git-graph.repository.showRemoteHeads": false,
|
||||
"git-graph.repository.commits.fetchAvatars": true,
|
||||
// VS Code Git config
|
||||
"git.autofetch": true,
|
||||
"git.enableStatusBarSync": false,
|
||||
"git.showActionButton": {
|
||||
"sync": false
|
||||
},
|
||||
// CSpell config
|
||||
"cSpell.language": "en-US",
|
||||
"cSpell.logLevel": "Information",
|
||||
"cSpell.allowCompoundWords": true,
|
||||
// Other extensions config
|
||||
"evenBetterToml.formatter.alignComments": false,
|
||||
"package-json-upgrade.ignorePatterns": ["source-sans-pro"],
|
||||
// VS Code config
|
||||
"html.format.wrapLineLength": 200,
|
||||
"files.eol": "\n",
|
||||
"files.insertFinalNewline": true,
|
||||
"files.associations": {
|
||||
"*.graphite": "json"
|
||||
},
|
||||
"editor.renderWhitespace": "boundary",
|
||||
"editor.minimap.markSectionHeaderRegex": "// ===+\\n\\s*//\\s*(?<label>[^\\n]{1,18})[^\\n]*(\\n\\s*//[^\\n]*)*\\n\\s*// ===+",
|
||||
"git.addAICoAuthor": "off"
|
||||
}
|
||||
8110
.jjconflict-base-0/Cargo.lock
generated
Normal file
8110
.jjconflict-base-0/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
264
.jjconflict-base-0/Cargo.toml
Normal file
264
.jjconflict-base-0/Cargo.toml
Normal file
@@ -0,0 +1,264 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"desktop",
|
||||
"desktop/wrapper",
|
||||
"desktop/ui",
|
||||
"desktop/embedded-resources",
|
||||
"desktop/bundle",
|
||||
"desktop/platform/linux",
|
||||
"desktop/platform/mac",
|
||||
"desktop/platform/win",
|
||||
"document/format",
|
||||
"document/graph-storage",
|
||||
"document/container",
|
||||
"editor",
|
||||
"frontend/wrapper",
|
||||
"libraries/dyn-any",
|
||||
"libraries/math-parser",
|
||||
"libraries/wgpu-sync",
|
||||
"node-graph/libraries/graphene-hash",
|
||||
"node-graph/libraries/*",
|
||||
"node-graph/nodes/*",
|
||||
"node-graph/nodes/raster/shaders",
|
||||
"node-graph/nodes/raster/shaders/entrypoint",
|
||||
"node-graph/graph-craft",
|
||||
"node-graph/graphene-cli",
|
||||
"node-graph/nodes/gstd",
|
||||
"node-graph/interpreted-executor",
|
||||
"node-graph/node-macro",
|
||||
"node-graph/preprocessor",
|
||||
"proc-macros",
|
||||
"tools/cargo-run",
|
||||
"tools/cargo-run/internal/*",
|
||||
"tools/crate-hierarchy-viz",
|
||||
"tools/third-party-licenses",
|
||||
"tools/editor-message-tree",
|
||||
"tools/node-docs",
|
||||
]
|
||||
default-members = [
|
||||
"editor",
|
||||
"frontend/wrapper",
|
||||
"libraries/dyn-any",
|
||||
"libraries/math-parser",
|
||||
"node-graph/graph-craft",
|
||||
"node-graph/interpreted-executor",
|
||||
"node-graph/node-macro",
|
||||
"node-graph/preprocessor",
|
||||
# blocked by https://github.com/rust-lang/cargo/issues/16000
|
||||
# "proc-macros",
|
||||
"tools/cargo-run",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.88"
|
||||
edition = "2024"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
homepage = "https://graphite.art"
|
||||
repository = "https://github.com/GraphiteEditor/Graphite"
|
||||
license = "Apache-2.0"
|
||||
version = "0.0.0"
|
||||
readme = "README.md"
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
# Local dependencies
|
||||
dyn-any = { path = "libraries/dyn-any", features = [
|
||||
"derive",
|
||||
"glam",
|
||||
"reqwest",
|
||||
"log-bad-types",
|
||||
"rc",
|
||||
] }
|
||||
graphene-hash = { path = "node-graph/libraries/graphene-hash", features = ["derive"] }
|
||||
preprocessor = { path = "node-graph/preprocessor" }
|
||||
math-parser = { path = "libraries/math-parser" }
|
||||
graphene-application-io = { path = "node-graph/libraries/application-io" }
|
||||
graphene-resource = { path = "node-graph/libraries/resources" }
|
||||
core-types = { path = "node-graph/libraries/core-types" }
|
||||
no-std-types = { path = "node-graph/libraries/no-std-types" }
|
||||
raster-types = { path = "node-graph/libraries/raster-types" }
|
||||
vector-types = { path = "node-graph/libraries/vector-types" }
|
||||
graphic-types = { path = "node-graph/libraries/graphic-types" }
|
||||
rendering = { path = "node-graph/libraries/rendering" }
|
||||
brush-nodes = { path = "node-graph/nodes/brush" }
|
||||
blending-nodes = { path = "node-graph/nodes/blending" }
|
||||
graphene-core = { path = "node-graph/nodes/gcore" }
|
||||
graphic-nodes = { path = "node-graph/nodes/graphic" }
|
||||
text-nodes = { path = "node-graph/nodes/text" }
|
||||
transform-nodes = { path = "node-graph/nodes/transform" }
|
||||
vector-nodes = { path = "node-graph/nodes/vector" }
|
||||
repeat-nodes = { path = "node-graph/nodes/repeat" }
|
||||
math-nodes = { path = "node-graph/nodes/math" }
|
||||
path-bool-nodes = { path = "node-graph/nodes/path-bool" }
|
||||
graph-craft = { path = "node-graph/graph-craft" }
|
||||
document-format = { path = "document/format" }
|
||||
document-graph-storage = { path = "document/graph-storage", default-features = false }
|
||||
document-container = { path = "document/container" }
|
||||
raster-nodes = { path = "node-graph/nodes/raster" }
|
||||
graphene-std = { path = "node-graph/nodes/gstd" }
|
||||
interpreted-executor = { path = "node-graph/interpreted-executor" }
|
||||
node-macro = { path = "node-graph/node-macro" }
|
||||
wgpu-executor = { path = "node-graph/libraries/wgpu-executor" }
|
||||
wgpu-sync = { path = "libraries/wgpu-sync" }
|
||||
graphite-proc-macros = { path = "proc-macros" }
|
||||
graphite-editor = { path = "editor" }
|
||||
graphene-canvas-utils = { path = "node-graph/libraries/canvas-utils" }
|
||||
|
||||
# Workspace dependencies
|
||||
rustc-hash = "2.0"
|
||||
bytemuck = { version = "1.13", features = ["derive", "min_const_generics"] }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_json = "1.0"
|
||||
rmp-serde = "1.3"
|
||||
serde_bytes = "0.11"
|
||||
serde-wasm-bindgen = "0.6"
|
||||
reqwest = { version = "0.13", features = ["blocking", "json"] }
|
||||
futures = "0.3"
|
||||
env_logger = "0.11"
|
||||
log = "0.4"
|
||||
bitflags = { version = "2.4", features = ["serde"] }
|
||||
ctor = "0.2"
|
||||
convert_case = "0.8"
|
||||
titlecase = "3.6"
|
||||
fancy-regex = "0.18.0"
|
||||
unicode-segmentation = "1.13.2"
|
||||
indoc = "2.0.5"
|
||||
derivative = "2.2"
|
||||
thiserror = "2"
|
||||
anyhow = "1.0"
|
||||
proc-macro2 = { version = "1", features = ["span-locations"] }
|
||||
quote = "1.0"
|
||||
chrono = "0.4"
|
||||
ron = "0.12"
|
||||
fastnoise-lite = "1.1"
|
||||
wgpu = { version = "29.0", features = [
|
||||
# We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm
|
||||
"fragile-send-sync-non-atomic-wasm",
|
||||
"spirv",
|
||||
"strict_asserts",
|
||||
] }
|
||||
once_cell = "1.13" # Remove and replace with `core::cell::LazyCell` (<https://doc.rust-lang.org/core/cell/struct.LazyCell.html>)
|
||||
wasm-bindgen = "=0.2.121" # NOTICE: keep in sync with the `wasm-bindgen-cli` version pinned across CI workflows, devcontainer, Nix, and the `cargo-run` tool. We pin this version because wasm-bindgen upgrades may break various things.
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "=0.3.98"
|
||||
web-sys = { version = "=0.3.98", features = [
|
||||
"Document",
|
||||
"DomRect",
|
||||
"Element",
|
||||
"HtmlCanvasElement",
|
||||
"CanvasRenderingContext2d",
|
||||
"CanvasPattern",
|
||||
"OffscreenCanvas",
|
||||
"OffscreenCanvasRenderingContext2d",
|
||||
"TextMetrics",
|
||||
"Window",
|
||||
"IdleRequestOptions",
|
||||
"ImageData",
|
||||
"Navigator",
|
||||
"Gpu",
|
||||
"HtmlImageElement",
|
||||
"ImageBitmapRenderingContext",
|
||||
] }
|
||||
winit = { git = "https://github.com/rust-windowing/winit.git" }
|
||||
keyboard-types = "0.8"
|
||||
url = "2.5"
|
||||
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt", "rt-multi-thread"] }
|
||||
# Linebender ecosystem (BEGIN)
|
||||
kurbo = { version = "0.13", features = ["serde"] }
|
||||
vello = "0.9"
|
||||
vello_encoding = "0.9"
|
||||
resvg = "0.47"
|
||||
usvg = "0.47"
|
||||
parley = { version = "0.9", default-features = false, features = ["std"] }
|
||||
skrifa = "0.42"
|
||||
polycool = "0.4"
|
||||
color = "0.3"
|
||||
# Linebender ecosystem (END)
|
||||
rand = { version = "0.9", default-features = false, features = ["std_rng"] }
|
||||
rand_chacha = "0.9"
|
||||
glam = { version = "0.32.1", default-features = false, features = [
|
||||
"nostd-libm",
|
||||
"scalar-math",
|
||||
"bytemuck",
|
||||
] }
|
||||
base64 = "0.22"
|
||||
blake3 = "1.5"
|
||||
mmap-io = { version = "0.9", features = ["hugepages"] }
|
||||
image = { version = "0.25", default-features = false, features = [
|
||||
"png",
|
||||
"jpeg",
|
||||
"bmp",
|
||||
"gif",
|
||||
] }
|
||||
pretty_assertions = "1.4"
|
||||
fern = { version = "0.7", features = ["colored"] }
|
||||
num_enum = { version = "0.7", default-features = false }
|
||||
num-derive = "0.4"
|
||||
num-traits = { version = "0.2", default-features = false, features = ["libm"] }
|
||||
tsify = { version = "0.5", default-features = false, features = ["js"] }
|
||||
syn = { version = "2.0", default-features = false, features = [
|
||||
"full",
|
||||
"derive",
|
||||
"parsing",
|
||||
"printing",
|
||||
"visit-mut",
|
||||
"visit",
|
||||
"clone-impls",
|
||||
"extra-traits",
|
||||
"proc-macro",
|
||||
] }
|
||||
lyon_geom = "1.0"
|
||||
petgraph = { version = "0.7", default-features = false, features = ["graphmap"] }
|
||||
half = { version = "2.4", default-features = false, features = ["bytemuck"] }
|
||||
tinyvec = { version = "1", features = ["std"] }
|
||||
criterion = { version = "0.7", features = ["html_reports"] }
|
||||
gungraun = { version = "0.18" }
|
||||
ndarray = "0.16"
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
dirs = "6.0"
|
||||
cef = "149"
|
||||
cef-dll-sys = "149"
|
||||
include_dir = "0.7"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing = "0.1"
|
||||
rfd = "0.17"
|
||||
open = "5.3"
|
||||
spin = "0.10"
|
||||
clap = "4.5"
|
||||
spirv-std = { version = "0.10.0-alpha.1", features = ["bytemuck"] }
|
||||
cargo-gpu-install = { version = "0.10.0-alpha.1", default-features = false }
|
||||
qrcodegen = "1.8"
|
||||
lzma-rust2 = { version = "0.16", default-features = false, features = ["std", "encoder", "optimization", "xz"] }
|
||||
scraper = "0.25"
|
||||
linesweeper = "0.4"
|
||||
smallvec = "1.13.2"
|
||||
zip = { version = "8", default-features = false }
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(target_arch, values("spirv"))'] }
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
[profile.dev.package]
|
||||
no-std-types = { opt-level = 1 }
|
||||
core-types= { opt-level = 1 }
|
||||
interpreted-executor = { opt-level = 1 } # This is a mitigation for https://github.com/rustwasm/wasm-pack/issues/981 which is needed because the node_registry function is too large
|
||||
graphite-proc-macros = { opt-level = 1 }
|
||||
image = { opt-level = 2 }
|
||||
rustc-hash = { opt-level = 3 }
|
||||
serde_derive = { opt-level = 1 }
|
||||
syn = { opt-level = 1 }
|
||||
node-macro = { opt-level = 2 }
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
debug = true
|
||||
|
||||
[patch.crates-io]
|
||||
# Force cargo to use only one version of the dpi crate (vendoring breaks without this)
|
||||
dpi = { git = "https://github.com/rust-windowing/winit.git" }
|
||||
rfd = { git = "https://github.com/timon-schelling/rfd.git", branch = "graphite" } # TODO: Remove this once https://github.com/PolyMeilex/rfd/pull/317 is merged and released
|
||||
cef = { git = "https://github.com/timon-schelling/cef-rs.git", branch = "graphite-149" }
|
||||
cef-dll-sys = { git = "https://github.com/timon-schelling/cef-rs.git", branch = "graphite-149" }
|
||||
201
.jjconflict-base-0/LICENSE.txt
Normal file
201
.jjconflict-base-0/LICENSE.txt
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
83
.jjconflict-base-0/README.md
Normal file
83
.jjconflict-base-0/README.md
Normal file
@@ -0,0 +1,83 @@
|
||||
|
||||
|
||||
<a href="https://graphite.art/">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/9366c148-4405-484f-909a-9a3526eb9209">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/791508ab-bcd5-4e31-a3b9-1187cfd7a2f6">
|
||||
<img alt="Graphite logo" src="https://github.com/user-attachments/assets/791508ab-bcd5-4e31-a3b9-1187cfd7a2f6">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
# Your procedural toolbox for 2D content creation
|
||||
|
||||
**Graphite is a free, open source vector and raster graphics engine, [available now](https://editor.graphite.art) in alpha. Get creative with a fully nondestructive editing workflow that combines layer-based compositing with node-based generative design.**
|
||||
|
||||
Having begun life as a vector editor, Graphite continues evolving into a generalized, all-in-one graphics toolbox that's built more like a game engine than a conventional creative app. The editor's tools wrap its node graph core, providing user-friendly workflows for vector, raster, and beyond. Photo editing, motion graphics, digital painting, desktop publishing, and VFX compositing are additional competencies on the planned [roadmap](https://graphite.art/features/#roadmap) making Graphite into a highly versatile content creation tool.
|
||||
|
||||
Learn more from the [website](https://graphite.art/), subscribe to the [newsletter](https://graphite.art/#newsletter), consider [volunteering](https://graphite.art/volunteer/) or [donating](https://graphite.art/donate/), and remember to give this repository a ⭐!
|
||||
|
||||
<br />
|
||||
<a href="https://discord.graphite.art/">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/ad185fac-3b48-446d-863c-2bcb0724abee">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/aa23f503-f3bf-444a-9080-8eaa19fa2fa8">
|
||||
<img alt="Discord" src="https://github.com/user-attachments/assets/aa23f503-f3bf-444a-9080-8eaa19fa2fa8" width="48" height="48">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<a href="https://www.reddit.com/r/graphite/">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/d8c05686-2eb9-4ac1-8149-728c12b4e71a">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/6f32329a-4d6f-42d8-9a2f-42977c0b3c05">
|
||||
<img alt="Reddit" src="https://github.com/user-attachments/assets/6f32329a-4d6f-42d8-9a2f-42977c0b3c05" width="48" height="48">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<a href="https://bsky.app/profile/graphiteeditor.bsky.social">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/c736d80c-e9bf-4591-a7e0-a7723057a906">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/3db9b0a1-5ab7-4bff-bfd3-8a4ade7b98bd">
|
||||
<img alt="Bluesky" src="https://github.com/user-attachments/assets/3db9b0a1-5ab7-4bff-bfd3-8a4ade7b98bd" width="48" height="48">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<a href="https://twitter.com/graphiteeditor">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/115f04cc-e3c2-4f90-ac35-eb9edd3ca9be">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/4ed4185d-a622-418c-bbf4-a0419e690ca9">
|
||||
<img alt="Twitter" src="https://github.com/user-attachments/assets/4ed4185d-a622-418c-bbf4-a0419e690ca9" width="48" height="48">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<a href="https://www.youtube.com/@GraphiteEditor">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/cbc02fad-5cbc-4715-a8e5-860198e989c7">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/d13b484d-97a8-4d9e-bbe4-c60348b3f676">
|
||||
<img alt="YouTube" src="https://github.com/user-attachments/assets/d13b484d-97a8-4d9e-bbe4-c60348b3f676" width="48" height="48">
|
||||
</picture>
|
||||
</a>
|
||||
<br /><br />
|
||||
|
||||
https://github.com/user-attachments/assets/f4604aea-e8f1-45ce-9218-46ddc666f11d
|
||||
|
||||
## Support our mission ❤️
|
||||
|
||||
Graphite is 100% community built and funded. Please become a part of keeping the project alive and thriving with a [donation](https://graphite.art/donate/) if you share a belief in our **mission**:
|
||||
|
||||
> Graphite strives to unshackle the creativity of every budding artist and seasoned professional by building the best comprehensive art and design tool that's accessible to all.
|
||||
>
|
||||
> Mission success will come when Graphite is an industry standard. A cohesive product vision and focus on innovation over imitation is the strategy that will make that possible.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Contributing/building the code
|
||||
|
||||
Are you a graphics programmer or Rust developer? Graphite aims to be one of the most approachable projects for putting your engineering skills to use in the world of open source. See [instructions here](https://graphite.art/volunteer/guide/) for setting up the project and getting started.
|
||||
|
||||
*By submitting code for inclusion in the project, you are agreeing to license your changes under the Apache 2.0 license, and that you have the authority to do so. Some directories may have other licenses, like dual-licensed MIT/Apache 2.0, and code submissions to those directories mean you agree to the applicable license(s).*
|
||||
40
.jjconflict-base-0/about.toml
Normal file
40
.jjconflict-base-0/about.toml
Normal file
@@ -0,0 +1,40 @@
|
||||
accepted = [
|
||||
"0BSD", # Keep this list in sync with those in `/deny.toml`
|
||||
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/deny.toml`
|
||||
"Apache-2.0", # Keep this list in sync with those in `/deny.toml`
|
||||
"BSD-2-Clause", # Keep this list in sync with those in `/deny.toml`
|
||||
"BSD-3-Clause", # Keep this list in sync with those in `/deny.toml`
|
||||
"BSL-1.0", # Keep this list in sync with those in `/deny.toml`
|
||||
"CC0-1.0", # Keep this list in sync with those in `/deny.toml`
|
||||
"CDLA-Permissive-2.0", # Keep this list in sync with those in `/deny.toml`
|
||||
"ISC", # Keep this list in sync with those in `/deny.toml`
|
||||
"MIT-0", # Keep this list in sync with those in `/deny.toml`
|
||||
"MIT", # Keep this list in sync with those in `/deny.toml`
|
||||
"MPL-2.0", # Keep this list in sync with those in `/deny.toml`
|
||||
"OpenSSL", # Keep this list in sync with those in `/deny.toml`
|
||||
"Unicode-3.0", # Keep this list in sync with those in `/deny.toml`
|
||||
"Unicode-DFS-2016", # Keep this list in sync with those in `/deny.toml`
|
||||
"Zlib", # Keep this list in sync with those in `/deny.toml`
|
||||
"NCSA", # Keep this list in sync with those in `/deny.toml`
|
||||
"bzip2-1.0.6", # Keep this list in sync with those in `/deny.toml`
|
||||
"OFL-1.1", # Keep this list in sync with those in `/deny.toml`
|
||||
]
|
||||
workarounds = ["ring"]
|
||||
ignore-build-dependencies = true
|
||||
ignore-dev-dependencies = true
|
||||
# Clearly Defined's API would occasionally (every few months) return errors for at least a full day (maybe some weird rate limiting?), but we can just disable to perform local checking (see #1653)
|
||||
no-clearly-defined = true
|
||||
|
||||
# https://raw.githubusercontent.com/briansmith/webpki/main/LICENSE
|
||||
# is the ISC license but test code within the repo is BSD-3-Clause, but is not compiled into the crate when we use it
|
||||
[webpki.clarify]
|
||||
license = "ISC"
|
||||
[[webpki.clarify.files]]
|
||||
path = "LICENSE"
|
||||
checksum = "5b698ca13897be3afdb7174256fa1574f8c6892b8bea1a66dd6469d3fe27885a"
|
||||
|
||||
[rustls-webpki.clarify]
|
||||
license = "ISC"
|
||||
[[rustls-webpki.clarify.files]]
|
||||
path = "LICENSE"
|
||||
checksum = "5b698ca13897be3afdb7174256fa1574f8c6892b8bea1a66dd6469d3fe27885a"
|
||||
1
.jjconflict-base-0/demo-artwork/changing-seasons.graphite
generated
Normal file
1
.jjconflict-base-0/demo-artwork/changing-seasons.graphite
generated
Normal file
File diff suppressed because one or more lines are too long
1
.jjconflict-base-0/demo-artwork/isometric-fountain.graphite
generated
Normal file
1
.jjconflict-base-0/demo-artwork/isometric-fountain.graphite
generated
Normal file
File diff suppressed because one or more lines are too long
1
.jjconflict-base-0/demo-artwork/marbled-mandelbrot.graphite
generated
Normal file
1
.jjconflict-base-0/demo-artwork/marbled-mandelbrot.graphite
generated
Normal file
File diff suppressed because one or more lines are too long
1
.jjconflict-base-0/demo-artwork/painted-dreams.graphite
generated
Normal file
1
.jjconflict-base-0/demo-artwork/painted-dreams.graphite
generated
Normal file
File diff suppressed because one or more lines are too long
1
.jjconflict-base-0/demo-artwork/parametric-dunescape.graphite
generated
Normal file
1
.jjconflict-base-0/demo-artwork/parametric-dunescape.graphite
generated
Normal file
File diff suppressed because one or more lines are too long
1
.jjconflict-base-0/demo-artwork/procedural-string-lights.graphite
generated
Normal file
1
.jjconflict-base-0/demo-artwork/procedural-string-lights.graphite
generated
Normal file
File diff suppressed because one or more lines are too long
1
.jjconflict-base-0/demo-artwork/red-dress.graphite
generated
Normal file
1
.jjconflict-base-0/demo-artwork/red-dress.graphite
generated
Normal file
File diff suppressed because one or more lines are too long
1
.jjconflict-base-0/demo-artwork/valley-of-spires.graphite
generated
Normal file
1
.jjconflict-base-0/demo-artwork/valley-of-spires.graphite
generated
Normal file
File diff suppressed because one or more lines are too long
194
.jjconflict-base-0/deny.toml
Normal file
194
.jjconflict-base-0/deny.toml
Normal file
@@ -0,0 +1,194 @@
|
||||
# This template contains all of the possible sections and their default values
|
||||
|
||||
# Note that all fields that take a lint level have these possible values:
|
||||
# * deny - An error will be produced and the check will fail
|
||||
# * warn - A warning will be produced, but the check will not fail
|
||||
# * allow - No warning or error will be produced, though in some cases a note
|
||||
# will be
|
||||
|
||||
# The values provided in this template are the default values that will be used
|
||||
# when any section or field is not specified in your own configuration
|
||||
|
||||
[graph]
|
||||
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||
# only the specified targets will be checked when running `cargo deny check`.
|
||||
# This means, if a particular package is only ever used as a target specific
|
||||
# dependency, such as, for example, the `nix` crate only being used via the
|
||||
# `target_family = "unix"` configuration, that only having windows targets in
|
||||
# this list would mean the nix crate, as well as any of its exclusive
|
||||
# dependencies not shared by any other crates, would be ignored, as the target
|
||||
# list here is effectively saying which targets you are building for.
|
||||
targets = [
|
||||
# The triple can be any string, but only the target triples built in to
|
||||
# rustc (as of 1.40) can be checked against actual config expressions
|
||||
#{ triple = "x86_64-unknown-linux-musl" },
|
||||
# You can also specify which target_features you promise are enabled for a
|
||||
# particular target. target_features are currently not validated against
|
||||
# the actual valid features supported by the target architecture.
|
||||
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check advisories`
|
||||
# More documentation for the advisories section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||
[advisories]
|
||||
# The path where the advisory database is cloned/fetched into
|
||||
db-path = "~/.cargo/advisory-db"
|
||||
# The url(s) of the advisory databases to use
|
||||
db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
"RUSTSEC-2024-0388", # Unmaintained `derivative`, used directly by graphite-editor and graphite-desktop
|
||||
"RUSTSEC-2024-0436", # Unmaintained `paste`, pulled in by dependencies `metal` and `wgpu-hal`
|
||||
"RUSTSEC-2025-0134", # Unmaintained `rustls-pemfile`, pulled in by build dependency `download-cef`
|
||||
"RUSTSEC-2025-0141", # Unmaintained `bincode`, pulled in by dev dependency `gungraun`
|
||||
]
|
||||
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
||||
# lower than the range specified will be ignored. Note that ignored advisories
|
||||
# will still output a note when they are encountered.
|
||||
# * None - CVSS Score 0.0
|
||||
# * Low - CVSS Score 0.1 - 3.9
|
||||
# * Medium - CVSS Score 4.0 - 6.9
|
||||
# * High - CVSS Score 7.0 - 8.9
|
||||
# * Critical - CVSS Score 9.0 - 10.0
|
||||
#severity-threshold =
|
||||
|
||||
# This section is considered when running `cargo deny check licenses`
|
||||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
#
|
||||
allow = [
|
||||
"0BSD", # Keep this list in sync with those in `/about.toml`
|
||||
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/about.toml`
|
||||
"Apache-2.0", # Keep this list in sync with those in `/about.toml`
|
||||
"BSD-2-Clause", # Keep this list in sync with those in `/about.toml`
|
||||
"BSD-3-Clause", # Keep this list in sync with those in `/about.toml`
|
||||
"BSL-1.0", # Keep this list in sync with those in `/about.toml`
|
||||
"CC0-1.0", # Keep this list in sync with those in `/about.toml`
|
||||
"CDLA-Permissive-2.0", # Keep this list in sync with those in `/about.toml`
|
||||
"ISC", # Keep this list in sync with those in `/about.toml`
|
||||
"MIT-0", # Keep this list in sync with those in `/about.toml`
|
||||
"MIT", # Keep this list in sync with those in `/about.toml`
|
||||
"MPL-2.0", # Keep this list in sync with those in `/about.toml`
|
||||
"OpenSSL", # Keep this list in sync with those in `/about.toml`
|
||||
"Unicode-3.0", # Keep this list in sync with those in `/about.toml`
|
||||
"Unicode-DFS-2016", # Keep this list in sync with those in `/about.toml`
|
||||
"Zlib", # Keep this list in sync with those in `/about.toml`
|
||||
"NCSA", # Keep this list in sync with those in `/about.toml`
|
||||
"bzip2-1.0.6", # Keep this list in sync with those in `/about.toml`
|
||||
"OFL-1.1", # Keep this list in sync with those in `/about.toml`
|
||||
]
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
# [possible values: any between 0.0 and 1.0].
|
||||
confidence-threshold = 0.8
|
||||
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
||||
# aren't accepted for every possible crate as with the normal allow list
|
||||
exceptions = [
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], name = "adler32", version = "*" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
[[licenses.clarify]]
|
||||
# The name of the crate the clarification applies to
|
||||
name = "ring"
|
||||
# The optional version constraint for the crate
|
||||
#version = "*"
|
||||
# The SPDX expression for the license requirements of the crate
|
||||
expression = "MIT AND ISC AND OpenSSL"
|
||||
# One or more files in the crate's source used as the "source of truth" for
|
||||
# the license expression. If the contents match, the clarification will be used
|
||||
# when running the license check, otherwise the clarification will be ignored
|
||||
# and the crate will be checked normally, which may produce warnings or errors
|
||||
# depending on the rest of your configuration
|
||||
license-files = [
|
||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 },
|
||||
]
|
||||
|
||||
[licenses.private]
|
||||
# If true, ignores workspace crates that aren't published, or are only
|
||||
# published to private registries
|
||||
ignore = false
|
||||
# One or more private registries that you might publish crates to, if a crate
|
||||
# is only published to private registries, and ignore is true, the crate will
|
||||
# not have its license(s) checked
|
||||
registries = [
|
||||
#"https://sekretz.com/registry
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check bans`.
|
||||
# More documentation about the 'bans' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||
[bans]
|
||||
# Lint level for when multiple versions of the same crate are detected
|
||||
multiple-versions = "allow"
|
||||
|
||||
# Lint level for when a crate version requirement is `*`
|
||||
wildcards = "allow"
|
||||
# The graph highlighting used when creating dotgraphs for crates
|
||||
# with multiple versions
|
||||
# * lowest-version - The path to the lowest versioned duplicate is highlighted
|
||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||
# * all - Both lowest-version and simplest-path are used
|
||||
highlight = "all"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#
|
||||
# Wrapper crates can optionally be specified to allow the crate when it
|
||||
# is a direct dependency of the otherwise banned crate
|
||||
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
|
||||
]
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#{ name = "cfg-if", version = "=0.1.10" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite
|
||||
skip-tree = [
|
||||
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
# More documentation about the 'sources' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||
[sources]
|
||||
# Lint level for what to happen when a crate from a crate registry that is not
|
||||
# in the allow list is encountered
|
||||
unknown-registry = "warn"
|
||||
# Lint level for what to happen when a crate from a git repository that is not
|
||||
# in the allow list is encountered
|
||||
unknown-git = "warn"
|
||||
# List of URLs for allowed crate registries. Defaults to the crates.io index
|
||||
# if not specified. If it is specified but empty, no registries are allowed.
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
# List of URLs for allowed Git repositories
|
||||
allow-git = []
|
||||
|
||||
[sources.allow-org]
|
||||
# 1 or more github.com organizations to allow git sources for
|
||||
github = ["linebender", "Rust-GPU"]
|
||||
# 1 or more gitlab.com organizations to allow git sources for
|
||||
#gitlab = [""]
|
||||
# 1 or more bitbucket.org organizations to allow git sources for
|
||||
#bitbucket = [""]
|
||||
74
.jjconflict-base-0/desktop/Cargo.toml
Normal file
74
.jjconflict-base-0/desktop/Cargo.toml
Normal file
@@ -0,0 +1,74 @@
|
||||
[package]
|
||||
name = "graphite-desktop"
|
||||
version = "0.1.0"
|
||||
description = "Graphite Desktop"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
license = "Apache-2.0"
|
||||
repository = ""
|
||||
edition = "2024"
|
||||
rust-version = "1.87"
|
||||
|
||||
[[bin]]
|
||||
name = "graphite"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["recommended", "embedded_resources"]
|
||||
recommended = ["gpu", "accelerated_paint"]
|
||||
embedded_resources = ["graphite-desktop-ui/embedded_resources"]
|
||||
gpu = ["graphite-desktop-wrapper/gpu"]
|
||||
accelerated_paint = ["graphite-desktop-ui/accelerated_paint"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
graphite-desktop-wrapper = { path = "wrapper" }
|
||||
graphite-desktop-ui = { path = "ui" }
|
||||
|
||||
wgpu = { workspace = true }
|
||||
winit = { workspace = true, features = [
|
||||
"wayland-csd-adwaita-notitlebar",
|
||||
"serde",
|
||||
] }
|
||||
thiserror = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
ron = { workspace = true }
|
||||
bytemuck = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
vello = { workspace = true }
|
||||
derivative = { workspace = true }
|
||||
rfd = { workspace = true }
|
||||
open = { workspace = true }
|
||||
lzma-rust2 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
rand = { workspace = true, features = ["thread_rng"] }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
interprocess = "2.4.2"
|
||||
fd-lock = "4.0.4"
|
||||
ctrlc = "3.5.1"
|
||||
window_clipboard = "0.5"
|
||||
|
||||
# Windows-specific dependencies
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.62.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Console",
|
||||
"Win32_UI_Controls",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_Shell",
|
||||
] }
|
||||
|
||||
# macOS-specific dependencies
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2 = { version = "0.6.1", default-features = false }
|
||||
objc2-foundation = { version = "0.3.2", default-features = false }
|
||||
objc2-app-kit = { version = "0.3.2", default-features = false }
|
||||
muda = { git = "https://github.com/timon-schelling/muda.git", rev = "e5bc28bbd6781b18afbfc237981f9ef47eddf863", default-features = false }
|
||||
@@ -0,0 +1,11 @@
|
||||
[Desktop Entry]
|
||||
Name=Graphite
|
||||
GenericName=Vector & Raster Graphics Editor
|
||||
Comment=Open-source vector & raster graphics editor. Featuring node based procedural nondestructive editing workflow.
|
||||
Exec=graphite
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=art.graphite.Graphite
|
||||
Categories=Graphics;VectorGraphics;RasterGraphics;
|
||||
Keywords=graphite;editor;vector;raster;procedural;design;
|
||||
StartupWMClass=art.graphite.Graphite
|
||||
16
.jjconflict-base-0/desktop/bundle/Cargo.toml
Normal file
16
.jjconflict-base-0/desktop/bundle/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "graphite-desktop-bundle"
|
||||
version = "0.0.0"
|
||||
description = "Graphite Desktop Bundle"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
license = "Apache-2.0"
|
||||
repository = ""
|
||||
edition = "2024"
|
||||
rust-version = "1.87"
|
||||
|
||||
[dependencies]
|
||||
cef-dll-sys = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
serde = { workspace = true }
|
||||
plist = { version = "*" }
|
||||
10
.jjconflict-base-0/desktop/bundle/build.rs
Normal file
10
.jjconflict-base-0/desktop/bundle/build.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-env-changed=CARGO_PROFILE");
|
||||
println!("cargo:rerun-if-env-changed=PROFILE");
|
||||
let profile = std::env::var("CARGO_PROFILE").or_else(|_| std::env::var("PROFILE")).unwrap();
|
||||
println!("cargo:rustc-env=CARGO_PROFILE={profile}");
|
||||
|
||||
println!("cargo:rerun-if-env-changed=DEP_CEF_DLL_WRAPPER_CEF_DIR");
|
||||
let cef_dir = std::env::var("DEP_CEF_DLL_WRAPPER_CEF_DIR").unwrap();
|
||||
println!("cargo:rustc-env=CEF_PATH={cef_dir}");
|
||||
}
|
||||
76
.jjconflict-base-0/desktop/bundle/src/common.rs
Normal file
76
.jjconflict-base-0/desktop/bundle/src/common.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
#![cfg_attr(target_os = "linux", allow(unused))] // TODO: Remove this when bundling for linux is implemented
|
||||
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
pub(crate) const APP_NAME: &str = "Graphite";
|
||||
pub(crate) const APP_BIN: &str = "graphite";
|
||||
|
||||
pub(crate) fn workspace_path() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_WORKSPACE_DIR"))
|
||||
}
|
||||
|
||||
fn profile_name() -> &'static str {
|
||||
let mut profile = env!("CARGO_PROFILE");
|
||||
if profile == "debug" {
|
||||
profile = "dev";
|
||||
}
|
||||
profile
|
||||
}
|
||||
|
||||
pub(crate) fn profile_path() -> PathBuf {
|
||||
workspace_path().join(format!("target/{}", env!("CARGO_PROFILE")))
|
||||
}
|
||||
|
||||
pub(crate) fn cef_path() -> PathBuf {
|
||||
PathBuf::from(env!("CEF_PATH"))
|
||||
}
|
||||
|
||||
pub(crate) fn build_bin(package: &str, bin: Option<&str>, features: Option<&str>) -> Result<PathBuf, Box<dyn Error>> {
|
||||
let mut args = vec!["build", "--package", package, "--profile", profile_name()];
|
||||
if let Some(bin) = bin {
|
||||
args.push("--bin");
|
||||
args.push(bin);
|
||||
}
|
||||
if let Some(features) = features {
|
||||
args.push("--features");
|
||||
args.push(features);
|
||||
}
|
||||
run_command("cargo", &args)?;
|
||||
let profile_path = profile_path();
|
||||
let mut bin_path = if let Some(bin) = bin { profile_path.join(bin) } else { profile_path.join(APP_BIN) };
|
||||
if cfg!(target_os = "windows") {
|
||||
bin_path.set_extension("exe");
|
||||
}
|
||||
Ok(bin_path)
|
||||
}
|
||||
|
||||
pub(crate) fn run_command(program: &str, args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let status = Command::new(program).args(args).stdout(Stdio::inherit()).stderr(Stdio::inherit()).status()?;
|
||||
if !status.success() {
|
||||
return Err(format!("Command '{}' with args {:?} failed with status: {}", program, args, status).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn clean_dir(dir: &Path) {
|
||||
if dir.exists() {
|
||||
fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
fs::create_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
pub(crate) fn copy_dir(src: &Path, dst: &Path) {
|
||||
fs::create_dir_all(dst).unwrap();
|
||||
for entry in fs::read_dir(src).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let dst_path = dst.join(entry.file_name());
|
||||
if entry.file_type().unwrap().is_dir() {
|
||||
copy_dir(&entry.path(), &dst_path);
|
||||
} else {
|
||||
fs::copy(entry.path(), &dst_path).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
20
.jjconflict-base-0/desktop/bundle/src/linux.rs
Normal file
20
.jjconflict-base-0/desktop/bundle/src/linux.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use crate::common::*;
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let app_bin = build_bin("graphite-desktop-platform-linux", None, None)?;
|
||||
|
||||
// TODO: Implement bundling for linux
|
||||
|
||||
// TODO: Consider adding more useful cli
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if let Some(pos) = args.iter().position(|a| a == "open") {
|
||||
let extra_args: Vec<&str> = args[pos + 1..].iter().map(|s| s.as_str()).collect();
|
||||
run_command(&app_bin.to_string_lossy(), &extra_args).expect("failed to open app");
|
||||
} else {
|
||||
eprintln!("Binary built and placed at {}", app_bin.to_string_lossy());
|
||||
eprintln!("Bundling for Linux is not yet implemented.");
|
||||
eprintln!("You can still start the app with the `open` subcommand. `cargo run -p graphite-desktop-bundle -- open`");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
222
.jjconflict-base-0/desktop/bundle/src/mac.rs
Normal file
222
.jjconflict-base-0/desktop/bundle/src/mac.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::common::*;
|
||||
|
||||
const APP_ID: &str = "art.graphite.Graphite";
|
||||
|
||||
const ICONS_FILE_NAME: &str = "graphite.icns";
|
||||
|
||||
const EXEC_PATH: &str = "Contents/MacOS";
|
||||
const FRAMEWORKS_PATH: &str = "Contents/Frameworks";
|
||||
const RESOURCES_PATH: &str = "Contents/Resources";
|
||||
const CEF_FRAMEWORK: &str = "Chromium Embedded Framework.framework";
|
||||
const GRAPHITE_DOCUMENT_TYPE: &str = "art.graphite.document";
|
||||
const GRAPHITE_FILE_EXTENSION: &str = "graphite";
|
||||
const GRAPHITE_MIME_TYPE: &str = "application/graphite+json";
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
let app_bin = build_bin("graphite-desktop-platform-mac", None, Some("main"))?;
|
||||
let helper_bin = build_bin("graphite-desktop-platform-mac", Some("helper"), Some("helper"))?;
|
||||
|
||||
let profile_path = profile_path();
|
||||
let app_dir = bundle(&profile_path, &app_bin, &helper_bin);
|
||||
|
||||
// TODO: Consider adding more useful cli
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if let Some(pos) = args.iter().position(|a| a == "open") {
|
||||
let executable = app_dir.join(EXEC_PATH).join(APP_NAME);
|
||||
let extra_args: Vec<&str> = args[pos + 1..].iter().map(|s| s.as_str()).collect();
|
||||
run_command(&executable.to_string_lossy(), &extra_args).expect("failed to open app");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bundle(out_dir: &Path, app_bin: &Path, helper_bin: &Path) -> PathBuf {
|
||||
let app_dir = out_dir.join(APP_NAME).with_extension("app");
|
||||
|
||||
clean_dir(&app_dir);
|
||||
|
||||
create_app(&app_dir, APP_ID, APP_NAME, app_bin, false);
|
||||
|
||||
for helper_type in [None, Some("GPU"), Some("Renderer")] {
|
||||
let helper_id_suffix = helper_type.map(|t| format!(".{t}")).unwrap_or_default();
|
||||
let helper_id = format!("{APP_ID}.helper{helper_id_suffix}");
|
||||
let helper_name_suffix = helper_type.map(|t| format!(" ({t})")).unwrap_or_default();
|
||||
let helper_name = format!("{APP_NAME} Helper{helper_name_suffix}");
|
||||
let helper_app_dir = app_dir.join(FRAMEWORKS_PATH).join(&helper_name).with_extension("app");
|
||||
create_app(&helper_app_dir, &helper_id, &helper_name, helper_bin, true);
|
||||
}
|
||||
|
||||
copy_dir(&cef_path().join(CEF_FRAMEWORK), &app_dir.join(FRAMEWORKS_PATH).join(CEF_FRAMEWORK));
|
||||
|
||||
let resource_dir = app_dir.join(RESOURCES_PATH);
|
||||
fs::create_dir_all(&resource_dir).expect("failed to create app resource dir");
|
||||
|
||||
let icon_file = workspace_path().join("branding/app-icons").join(ICONS_FILE_NAME);
|
||||
fs::copy(icon_file, resource_dir.join(ICONS_FILE_NAME)).expect("failed to copy icon file");
|
||||
|
||||
app_dir
|
||||
}
|
||||
|
||||
fn create_app(app_dir: &Path, id: &str, name: &str, bin: &Path, is_helper: bool) {
|
||||
fs::create_dir_all(app_dir.join(EXEC_PATH)).unwrap();
|
||||
|
||||
let app_contents_dir: &Path = &app_dir.join("Contents");
|
||||
create_info_plist(app_contents_dir, id, name, is_helper).unwrap();
|
||||
fs::copy(bin, app_dir.join(EXEC_PATH).join(name)).unwrap();
|
||||
}
|
||||
|
||||
fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let info = InfoPlist {
|
||||
cf_bundle_name: exec_name.to_string(),
|
||||
cf_bundle_identifier: id.to_string(),
|
||||
cf_bundle_display_name: exec_name.to_string(),
|
||||
cf_bundle_executable: exec_name.to_string(),
|
||||
cf_bundle_icon_file: if is_helper { None } else { Some(ICONS_FILE_NAME.to_string()) },
|
||||
cf_bundle_info_dictionary_version: "6.0".to_string(),
|
||||
cf_bundle_package_type: "APPL".to_string(),
|
||||
cf_bundle_signature: "????".to_string(),
|
||||
cf_bundle_version: "0.0.0".to_string(),
|
||||
cf_bundle_short_version_string: "0.0".to_string(),
|
||||
cf_bundle_development_region: "en".to_string(),
|
||||
ls_environment: [("MallocNanoZone".to_string(), "0".to_string())].iter().cloned().collect(),
|
||||
ls_file_quarantine_enabled: true,
|
||||
ls_minimum_system_version: "11.0".to_string(),
|
||||
ls_ui_element: if is_helper { Some("1".to_string()) } else { None },
|
||||
ns_supports_automatic_graphics_switching: true,
|
||||
cf_bundle_document_types: (!is_helper).then(document_types),
|
||||
ut_exported_type_declarations: (!is_helper).then(exported_type_declarations),
|
||||
};
|
||||
|
||||
let plist_file = dir.join("Info.plist");
|
||||
plist::to_file_xml(plist_file, &info)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn document_types() -> Vec<DocumentType> {
|
||||
vec![
|
||||
DocumentType {
|
||||
cf_bundle_type_name: "Graphite Document".to_string(),
|
||||
cf_bundle_type_role: "Editor".to_string(),
|
||||
cf_bundle_type_extensions: Some(vec![GRAPHITE_FILE_EXTENSION.to_string()]),
|
||||
cf_bundle_type_icon_file: Some(ICONS_FILE_NAME.to_string()),
|
||||
ls_handler_rank: Some("Owner".to_string()),
|
||||
ls_item_content_types: vec![GRAPHITE_DOCUMENT_TYPE.to_string()],
|
||||
},
|
||||
DocumentType {
|
||||
cf_bundle_type_name: "SVG Image".to_string(),
|
||||
cf_bundle_type_role: "Editor".to_string(),
|
||||
cf_bundle_type_extensions: Some(vec!["svg".to_string()]),
|
||||
cf_bundle_type_icon_file: None,
|
||||
ls_handler_rank: Some("Alternate".to_string()),
|
||||
ls_item_content_types: vec!["public.svg-image".to_string()],
|
||||
},
|
||||
DocumentType {
|
||||
cf_bundle_type_name: "Image".to_string(),
|
||||
cf_bundle_type_role: "Editor".to_string(),
|
||||
cf_bundle_type_extensions: None,
|
||||
cf_bundle_type_icon_file: None,
|
||||
ls_handler_rank: Some("Alternate".to_string()),
|
||||
ls_item_content_types: vec!["public.image".to_string()],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn exported_type_declarations() -> Vec<ExportedTypeDeclaration> {
|
||||
vec![ExportedTypeDeclaration {
|
||||
ut_type_identifier: GRAPHITE_DOCUMENT_TYPE.to_string(),
|
||||
ut_type_description: "Graphite Document".to_string(),
|
||||
ut_type_conforms_to: vec!["public.json".to_string()],
|
||||
ut_type_tag_specification: TypeTagSpecification {
|
||||
public_filename_extension: vec![GRAPHITE_FILE_EXTENSION.to_string()],
|
||||
public_mime_type: GRAPHITE_MIME_TYPE.to_string(),
|
||||
},
|
||||
}]
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct InfoPlist {
|
||||
#[serde(rename = "CFBundleName")]
|
||||
cf_bundle_name: String,
|
||||
#[serde(rename = "CFBundleIdentifier")]
|
||||
cf_bundle_identifier: String,
|
||||
#[serde(rename = "CFBundleDisplayName")]
|
||||
cf_bundle_display_name: String,
|
||||
#[serde(rename = "CFBundleExecutable")]
|
||||
cf_bundle_executable: String,
|
||||
#[serde(rename = "CFBundleIconFile")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cf_bundle_icon_file: Option<String>,
|
||||
#[serde(rename = "CFBundleInfoDictionaryVersion")]
|
||||
cf_bundle_info_dictionary_version: String,
|
||||
#[serde(rename = "CFBundlePackageType")]
|
||||
cf_bundle_package_type: String,
|
||||
#[serde(rename = "CFBundleSignature")]
|
||||
cf_bundle_signature: String,
|
||||
#[serde(rename = "CFBundleVersion")]
|
||||
cf_bundle_version: String,
|
||||
#[serde(rename = "CFBundleShortVersionString")]
|
||||
cf_bundle_short_version_string: String,
|
||||
#[serde(rename = "CFBundleDevelopmentRegion")]
|
||||
cf_bundle_development_region: String,
|
||||
#[serde(rename = "LSEnvironment")]
|
||||
ls_environment: HashMap<String, String>,
|
||||
#[serde(rename = "LSFileQuarantineEnabled")]
|
||||
ls_file_quarantine_enabled: bool,
|
||||
#[serde(rename = "LSMinimumSystemVersion")]
|
||||
ls_minimum_system_version: String,
|
||||
#[serde(rename = "LSUIElement")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
ls_ui_element: Option<String>,
|
||||
#[serde(rename = "NSSupportsAutomaticGraphicsSwitching")]
|
||||
ns_supports_automatic_graphics_switching: bool,
|
||||
#[serde(rename = "CFBundleDocumentTypes")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cf_bundle_document_types: Option<Vec<DocumentType>>,
|
||||
#[serde(rename = "UTExportedTypeDeclarations")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
ut_exported_type_declarations: Option<Vec<ExportedTypeDeclaration>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct DocumentType {
|
||||
#[serde(rename = "CFBundleTypeName")]
|
||||
cf_bundle_type_name: String,
|
||||
#[serde(rename = "CFBundleTypeRole")]
|
||||
cf_bundle_type_role: String,
|
||||
#[serde(rename = "CFBundleTypeExtensions")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cf_bundle_type_extensions: Option<Vec<String>>,
|
||||
#[serde(rename = "CFBundleTypeIconFile")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cf_bundle_type_icon_file: Option<String>,
|
||||
#[serde(rename = "LSHandlerRank")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
ls_handler_rank: Option<String>,
|
||||
#[serde(rename = "LSItemContentTypes")]
|
||||
ls_item_content_types: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct ExportedTypeDeclaration {
|
||||
#[serde(rename = "UTTypeIdentifier")]
|
||||
ut_type_identifier: String,
|
||||
#[serde(rename = "UTTypeDescription")]
|
||||
ut_type_description: String,
|
||||
#[serde(rename = "UTTypeConformsTo")]
|
||||
ut_type_conforms_to: Vec<String>,
|
||||
#[serde(rename = "UTTypeTagSpecification")]
|
||||
ut_type_tag_specification: TypeTagSpecification,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct TypeTagSpecification {
|
||||
#[serde(rename = "public.filename-extension")]
|
||||
public_filename_extension: Vec<String>,
|
||||
#[serde(rename = "public.mime-type")]
|
||||
public_mime_type: String,
|
||||
}
|
||||
17
.jjconflict-base-0/desktop/bundle/src/main.rs
Normal file
17
.jjconflict-base-0/desktop/bundle/src/main.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
mod common;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win;
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_os = "linux")]
|
||||
linux::main().unwrap();
|
||||
#[cfg(target_os = "macos")]
|
||||
mac::main().unwrap();
|
||||
#[cfg(target_os = "windows")]
|
||||
win::main().unwrap();
|
||||
}
|
||||
61
.jjconflict-base-0/desktop/bundle/src/win.rs
Normal file
61
.jjconflict-base-0/desktop/bundle/src/win.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::common::*;
|
||||
|
||||
const EXECUTABLE: &str = "Graphite.exe";
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
let app_bin = build_bin("graphite-desktop-platform-win", None, None)?;
|
||||
|
||||
let executable = bundle(&profile_path(), &app_bin);
|
||||
|
||||
// TODO: Consider adding more useful cli
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if let Some(pos) = args.iter().position(|a| a == "open") {
|
||||
let extra_args: Vec<&str> = args[pos + 1..].iter().map(|s| s.as_str()).collect();
|
||||
run_command(&executable.to_string_lossy(), &extra_args).expect("failed to open app")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bundle(out_dir: &Path, app_bin: &Path) -> PathBuf {
|
||||
let app_dir = out_dir.join(APP_NAME);
|
||||
|
||||
clean_dir(&app_dir);
|
||||
|
||||
copy_dir(&cef_path(), &app_dir);
|
||||
|
||||
if let Err(e) = remove_unnecessary_cef_files(&app_dir) {
|
||||
eprintln!("Failed to remove unnecessary CEF files: {}", e);
|
||||
}
|
||||
|
||||
let bin_path = app_dir.join(EXECUTABLE);
|
||||
fs::copy(app_bin, &bin_path).unwrap();
|
||||
|
||||
bin_path
|
||||
}
|
||||
|
||||
fn remove_unnecessary_cef_files(app_dir: &Path) -> Result<(), Box<dyn Error>> {
|
||||
fs::remove_dir_all(app_dir.join("cmake"))?;
|
||||
fs::remove_dir_all(app_dir.join("include"))?;
|
||||
fs::remove_dir_all(app_dir.join("libcef_dll"))?;
|
||||
|
||||
for entry in fs::read_dir(app_dir.join("locales"))? {
|
||||
let path = entry?.path();
|
||||
if path.is_file() && path.file_name() != Some("en-US.pak".as_ref()) {
|
||||
fs::remove_file(path)?;
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_file(app_dir.join("archive.json"))?;
|
||||
fs::remove_file(app_dir.join("CMakeLists.txt"))?;
|
||||
fs::remove_file(app_dir.join("bootstrapc.exe"))?;
|
||||
fs::remove_file(app_dir.join("bootstrap.exe"))?;
|
||||
fs::remove_file(app_dir.join("libcef.lib"))?;
|
||||
fs::remove_file(app_dir.join("CREDITS.html"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
15
.jjconflict-base-0/desktop/embedded-resources/Cargo.toml
Normal file
15
.jjconflict-base-0/desktop/embedded-resources/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "graphite-desktop-embedded-resources"
|
||||
version = "0.1.0"
|
||||
description = "Graphite Desktop Embedded Resources"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
license = "Apache-2.0"
|
||||
repository = ""
|
||||
edition = "2024"
|
||||
rust-version = "1.87"
|
||||
|
||||
[dependencies]
|
||||
include_dir = { workspace = true }
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(embedded_resources)'] }
|
||||
32
.jjconflict-base-0/desktop/embedded-resources/build.rs
Normal file
32
.jjconflict-base-0/desktop/embedded-resources/build.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
const EMBEDDED_RESOURCES_ENV: &str = "EMBEDDED_RESOURCES";
|
||||
const DEFAULT_RESOURCES_DIR: &str = "../../frontend/dist";
|
||||
|
||||
fn main() {
|
||||
let mut embedded_resources: Option<String> = None;
|
||||
|
||||
println!("cargo:rerun-if-env-changed={EMBEDDED_RESOURCES_ENV}");
|
||||
if let Ok(embedded_resources_env) = std::env::var(EMBEDDED_RESOURCES_ENV)
|
||||
&& std::path::PathBuf::from(&embedded_resources_env).exists()
|
||||
{
|
||||
embedded_resources = Some(embedded_resources_env);
|
||||
}
|
||||
|
||||
if embedded_resources.is_none() {
|
||||
// Check if the directory `DEFAULT_RESOURCES_DIR` exists and sets the embedded_resources cfg accordingly
|
||||
// Absolute path of `DEFAULT_RESOURCES_DIR` available via the `EMBEDDED_RESOURCES` environment variable
|
||||
let crate_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
println!("cargo:rerun-if-changed={DEFAULT_RESOURCES_DIR}");
|
||||
if let Ok(resources) = crate_dir.join(DEFAULT_RESOURCES_DIR).canonicalize()
|
||||
&& resources.exists()
|
||||
{
|
||||
embedded_resources = Some(resources.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(embedded_resources) = embedded_resources {
|
||||
println!("cargo:rustc-cfg=embedded_resources");
|
||||
println!("cargo:rustc-env={EMBEDDED_RESOURCES_ENV}={embedded_resources}");
|
||||
} else {
|
||||
println!("cargo:warning=Resource directory does not exist. Resources will not be embedded. Did you forget to build the frontend?");
|
||||
}
|
||||
}
|
||||
10
.jjconflict-base-0/desktop/embedded-resources/src/lib.rs
Normal file
10
.jjconflict-base-0/desktop/embedded-resources/src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! This crate provides `EMBEDDED_RESOURCES` that can be included in the desktop application binary.
|
||||
//! It is intended to be used by the `embedded_resources` feature of the `graphite-desktop` crate.
|
||||
//! The build script checks if the specified resources directory exists and sets the `embedded_resources` cfg flag accordingly.
|
||||
//! If the resources directory does not exist, resources will not be embedded and a warning will be reported during compilation.
|
||||
|
||||
#[cfg(embedded_resources)]
|
||||
pub static EMBEDDED_RESOURCES: Option<include_dir::Dir> = Some(include_dir::include_dir!("$EMBEDDED_RESOURCES"));
|
||||
|
||||
#[cfg(not(embedded_resources))]
|
||||
pub static EMBEDDED_RESOURCES: Option<include_dir::Dir> = None;
|
||||
16
.jjconflict-base-0/desktop/platform/linux/Cargo.toml
Normal file
16
.jjconflict-base-0/desktop/platform/linux/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "graphite-desktop-platform-linux"
|
||||
version = "0.0.0"
|
||||
description = "Graphite Desktop Platform Linux"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
license = "Apache-2.0"
|
||||
repository = ""
|
||||
edition = "2024"
|
||||
rust-version = "1.87"
|
||||
|
||||
[[bin]]
|
||||
name = "graphite"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
graphite-desktop = { path = "../.." }
|
||||
3
.jjconflict-base-0/desktop/platform/linux/src/main.rs
Normal file
3
.jjconflict-base-0/desktop/platform/linux/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() -> std::process::ExitCode {
|
||||
graphite_desktop::start()
|
||||
}
|
||||
27
.jjconflict-base-0/desktop/platform/mac/Cargo.toml
Normal file
27
.jjconflict-base-0/desktop/platform/mac/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "graphite-desktop-platform-mac"
|
||||
version = "0.0.0"
|
||||
description = "Graphite Desktop Platform Mac"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
license = "Apache-2.0"
|
||||
repository = ""
|
||||
edition = "2024"
|
||||
rust-version = "1.87"
|
||||
|
||||
[features]
|
||||
main = ["dep:graphite-desktop"]
|
||||
helper = ["dep:graphite-desktop-ui"]
|
||||
|
||||
[[bin]]
|
||||
name = "graphite"
|
||||
path = "src/main.rs"
|
||||
required-features = ["main"]
|
||||
|
||||
[[bin]]
|
||||
name = "helper"
|
||||
path = "src/helper.rs"
|
||||
required-features = ["helper"]
|
||||
|
||||
[dependencies]
|
||||
graphite-desktop = { path = "../..", optional = true }
|
||||
graphite-desktop-ui = { path = "../../ui", optional = true }
|
||||
3
.jjconflict-base-0/desktop/platform/mac/src/helper.rs
Normal file
3
.jjconflict-base-0/desktop/platform/mac/src/helper.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() -> std::process::ExitCode {
|
||||
graphite_desktop_ui::run_helper()
|
||||
}
|
||||
3
.jjconflict-base-0/desktop/platform/mac/src/main.rs
Normal file
3
.jjconflict-base-0/desktop/platform/mac/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() -> std::process::ExitCode {
|
||||
graphite_desktop::start()
|
||||
}
|
||||
19
.jjconflict-base-0/desktop/platform/win/Cargo.toml
Normal file
19
.jjconflict-base-0/desktop/platform/win/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "graphite-desktop-platform-win"
|
||||
version = "0.0.0"
|
||||
description = "Graphite Desktop Platform Windows"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
license = "Apache-2.0"
|
||||
repository = ""
|
||||
edition = "2024"
|
||||
rust-version = "1.87"
|
||||
|
||||
[[bin]]
|
||||
name = "graphite"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
graphite-desktop = { path = "../.." }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.build-dependencies]
|
||||
winres = "0.1"
|
||||
32
.jjconflict-base-0/desktop/platform/win/build.rs
Normal file
32
.jjconflict-base-0/desktop/platform/win/build.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let mut res = winres::WindowsResource::new();
|
||||
|
||||
res.set_icon("../../../branding/app-icons/graphite.ico");
|
||||
|
||||
res.set_language(0x0409); // English (US)
|
||||
|
||||
// TODO: Replace with actual version
|
||||
res.set_version_info(winres::VersionInfo::FILEVERSION, {
|
||||
const MAJOR: u64 = 0;
|
||||
const MINOR: u64 = 0;
|
||||
const PATCH: u64 = 0;
|
||||
const RELEASE: u64 = 0;
|
||||
(MAJOR << 48) | (MINOR << 32) | (PATCH << 16) | RELEASE
|
||||
});
|
||||
res.set("FileVersion", "0.0.0.0");
|
||||
res.set("ProductVersion", "0.0.0.0");
|
||||
|
||||
res.set("OriginalFilename", "Graphite.exe");
|
||||
|
||||
res.set("FileDescription", "Graphite");
|
||||
res.set("ProductName", "Graphite");
|
||||
|
||||
// TODO: Pull this year from the Git commit date
|
||||
res.set("LegalCopyright", "Copyright © 2026 Graphite Labs, LLC");
|
||||
res.set("CompanyName", "Graphite Labs, LLC");
|
||||
|
||||
res.compile().expect("Failed to compile Windows resources");
|
||||
}
|
||||
}
|
||||
4
.jjconflict-base-0/desktop/platform/win/src/main.rs
Normal file
4
.jjconflict-base-0/desktop/platform/win/src/main.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
fn main() -> std::process::ExitCode {
|
||||
graphite_desktop::start()
|
||||
}
|
||||
696
.jjconflict-base-0/desktop/src/app.rs
Normal file
696
.jjconflict-base-0/desktop/src/app.rs
Normal file
@@ -0,0 +1,696 @@
|
||||
use rand::Rng;
|
||||
use rfd::AsyncFileDialog;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::{Receiver, SyncSender};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::dpi::{PhysicalPosition, PhysicalSize};
|
||||
use winit::event::{ButtonSource, ElementState, MouseButton, StartCause, WindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
use winit::window::WindowId;
|
||||
|
||||
use crate::dirs;
|
||||
use crate::event::{AppEvent, AppEventScheduler};
|
||||
use crate::persist;
|
||||
use crate::preferences;
|
||||
use crate::render::{RenderError, RenderState};
|
||||
use crate::ui::{UiCommand, UiInstance};
|
||||
use crate::window::Window;
|
||||
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences};
|
||||
use crate::wrapper::{DesktopWrapper, MmapResourceStorage, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
||||
|
||||
pub(crate) struct App {
|
||||
render_state: Option<RenderState>,
|
||||
wgpu_context: WgpuContext,
|
||||
window: Option<Window>,
|
||||
window_scale: f64,
|
||||
window_size: PhysicalSize<u32>,
|
||||
window_maximized: bool,
|
||||
window_fullscreen: bool,
|
||||
window_pending_drag: bool,
|
||||
pointer_position: PhysicalPosition<f64>,
|
||||
pointer_lock_position: Option<PhysicalPosition<f64>>,
|
||||
ui_scale: f64,
|
||||
app_event_receiver: Receiver<AppEvent>,
|
||||
app_event_scheduler: AppEventScheduler,
|
||||
desktop_wrapper: DesktopWrapper,
|
||||
ui: UiInstance,
|
||||
ui_frame_received: bool,
|
||||
start_render_sender: SyncSender<()>,
|
||||
web_communication_initialized: bool,
|
||||
web_communication_startup_buffer: Vec<Vec<u8>>,
|
||||
preferences: Preferences,
|
||||
launch_documents: Option<Vec<PathBuf>>,
|
||||
startup_time: Option<Instant>,
|
||||
exiting: Arc<AtomicBool>,
|
||||
exit_reason: ExitReason,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub(crate) fn init() {
|
||||
Window::init();
|
||||
}
|
||||
|
||||
pub(crate) fn new(
|
||||
ui: UiInstance,
|
||||
wgpu_context: WgpuContext,
|
||||
app_event_receiver: Receiver<AppEvent>,
|
||||
app_event_scheduler: AppEventScheduler,
|
||||
preferences: Preferences,
|
||||
launch_documents: Vec<PathBuf>,
|
||||
) -> Self {
|
||||
let ctrlc_app_event_scheduler = app_event_scheduler.clone();
|
||||
ctrlc::set_handler(move || {
|
||||
tracing::info!("Termination signal received, exiting...");
|
||||
ctrlc_app_event_scheduler.schedule(AppEvent::Exit);
|
||||
})
|
||||
.expect("Error setting Ctrl-C handler");
|
||||
|
||||
let exiting = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let rendering_app_event_scheduler = app_event_scheduler.clone();
|
||||
let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1);
|
||||
let exiting_clone = exiting.clone();
|
||||
std::thread::spawn(move || {
|
||||
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||
loop {
|
||||
let result = runtime.block_on(DesktopWrapper::execute_node_graph());
|
||||
rendering_app_event_scheduler.schedule(AppEvent::NodeGraphExecutionResult(result));
|
||||
let _ = start_render_receiver.recv_timeout(Duration::from_millis(10));
|
||||
if exiting_clone.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let resource_storage = MmapResourceStorage::new(dirs::app_resources_dir()).expect("Failed to initialize on-disk resource storage");
|
||||
|
||||
// Wake the winit event loop when an editor future completes.
|
||||
let wake_scheduler = app_event_scheduler.clone();
|
||||
let wake = Arc::new(move || {
|
||||
wake_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::Wake));
|
||||
});
|
||||
let desktop_wrapper = DesktopWrapper::new(rand::rng().random(), Arc::new(resource_storage), dirs::app_autosave_documents_dir(), wgpu_context.clone(), wake);
|
||||
|
||||
let completion_render_sender = start_render_sender.clone();
|
||||
DesktopWrapper::set_completion_notifier(move || {
|
||||
let _ = completion_render_sender.try_send(());
|
||||
});
|
||||
|
||||
Self {
|
||||
render_state: None,
|
||||
wgpu_context,
|
||||
window: None,
|
||||
window_scale: 1.,
|
||||
window_size: PhysicalSize { width: 0, height: 0 },
|
||||
window_maximized: false,
|
||||
window_fullscreen: false,
|
||||
window_pending_drag: false,
|
||||
pointer_position: Default::default(),
|
||||
pointer_lock_position: Default::default(),
|
||||
ui_scale: 1.,
|
||||
app_event_receiver,
|
||||
app_event_scheduler,
|
||||
desktop_wrapper,
|
||||
ui,
|
||||
ui_frame_received: false,
|
||||
start_render_sender,
|
||||
web_communication_initialized: false,
|
||||
web_communication_startup_buffer: Vec::new(),
|
||||
preferences,
|
||||
launch_documents: Some(launch_documents),
|
||||
startup_time: None,
|
||||
exiting,
|
||||
exit_reason: ExitReason::Shutdown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run(mut self, event_loop: EventLoop) -> ExitReason {
|
||||
event_loop.run_app(&mut self).unwrap();
|
||||
self.exit_reason
|
||||
}
|
||||
|
||||
fn exit(&mut self, reason: Option<ExitReason>) {
|
||||
if self.exiting.swap(true, Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let _ = self.start_render_sender.send(());
|
||||
if let Some(reason) = reason {
|
||||
self.exit_reason = reason;
|
||||
}
|
||||
self.app_event_scheduler.schedule(AppEvent::Exit);
|
||||
}
|
||||
|
||||
fn resize(&mut self) {
|
||||
let Some(window) = &self.window else {
|
||||
tracing::error!("Resize failed due to missing window");
|
||||
return;
|
||||
};
|
||||
|
||||
let maximized = window.is_maximized();
|
||||
if maximized != self.window_maximized {
|
||||
self.window_maximized = maximized;
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::UpdateMaximized { maximized }));
|
||||
}
|
||||
|
||||
let fullscreen = window.is_fullscreen();
|
||||
if fullscreen != self.window_fullscreen {
|
||||
self.window_fullscreen = fullscreen;
|
||||
self.app_event_scheduler
|
||||
.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::UpdateFullscreen { fullscreen }));
|
||||
}
|
||||
|
||||
let size = window.surface_size();
|
||||
let scale = window.scale_factor() * self.ui_scale;
|
||||
let is_new_size = size != self.window_size;
|
||||
let is_new_scale = scale != self.window_scale;
|
||||
|
||||
if !is_new_size && !is_new_scale {
|
||||
return;
|
||||
}
|
||||
|
||||
if is_new_size {
|
||||
self.ui.send(UiCommand::Resized {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
});
|
||||
}
|
||||
if is_new_scale {
|
||||
self.ui.send(UiCommand::ScaleChanged(scale));
|
||||
}
|
||||
|
||||
self.ui.send(UiCommand::Refresh);
|
||||
|
||||
if let Some(render_state) = &mut self.render_state {
|
||||
render_state.resize(size.width, size.height);
|
||||
}
|
||||
|
||||
window.request_redraw();
|
||||
|
||||
self.window_size = size;
|
||||
self.window_scale = scale;
|
||||
}
|
||||
|
||||
fn handle_desktop_frontend_message(&mut self, message: DesktopFrontendMessage, responses: &mut Vec<DesktopWrapperMessage>) {
|
||||
match message {
|
||||
DesktopFrontendMessage::ToWeb(messages) => {
|
||||
let Some(bytes) = serialize_frontend_messages(messages) else {
|
||||
tracing::error!("Failed to serialize frontend messages");
|
||||
return;
|
||||
};
|
||||
self.send_or_queue_web_message(bytes);
|
||||
}
|
||||
DesktopFrontendMessage::OpenFileDialog { title, filters, multiple, context } => {
|
||||
let app_event_scheduler = self.app_event_scheduler.clone();
|
||||
let _ = thread::spawn(move || {
|
||||
let mut dialog = AsyncFileDialog::new().set_title(title);
|
||||
for filter in filters {
|
||||
dialog = dialog.add_filter(filter.name, &filter.extensions);
|
||||
}
|
||||
|
||||
let handles = if multiple {
|
||||
futures::executor::block_on(dialog.pick_files()).unwrap_or_default()
|
||||
} else {
|
||||
futures::executor::block_on(dialog.pick_file()).into_iter().collect()
|
||||
};
|
||||
|
||||
for handle in handles {
|
||||
let path = handle.path().to_path_buf();
|
||||
match fs::read(&path) {
|
||||
Ok(content) => {
|
||||
let message = DesktopWrapperMessage::FileDialogResult { path, content, context };
|
||||
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
Err(e) => tracing::error!("Failed to read file {}: {}", path.display(), e),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
DesktopFrontendMessage::SaveFileDialog {
|
||||
title,
|
||||
default_filename,
|
||||
default_folder,
|
||||
filters,
|
||||
context,
|
||||
} => {
|
||||
let app_event_scheduler = self.app_event_scheduler.clone();
|
||||
let _ = thread::spawn(move || {
|
||||
let mut dialog = AsyncFileDialog::new().set_title(title).set_file_name(default_filename);
|
||||
if let Some(folder) = default_folder {
|
||||
dialog = dialog.set_directory(folder);
|
||||
}
|
||||
for filter in filters {
|
||||
dialog = dialog.add_filter(filter.name, &filter.extensions);
|
||||
}
|
||||
|
||||
let show_dialog = async move { dialog.save_file().await.map(|f| f.path().to_path_buf()) };
|
||||
|
||||
if let Some(path) = futures::executor::block_on(show_dialog) {
|
||||
let message = DesktopWrapperMessage::SaveFileDialogResult { path, context };
|
||||
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
});
|
||||
}
|
||||
DesktopFrontendMessage::WriteFile { path, content } => {
|
||||
if let Err(e) = fs::write(&path, content) {
|
||||
tracing::error!("Failed to write file {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::OpenUrl(url) => {
|
||||
let _ = thread::spawn(move || {
|
||||
if let Err(e) = open::that(&url) {
|
||||
tracing::error!("Failed to open URL: {}: {}", url, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
DesktopFrontendMessage::UpdateViewportPhysicalBounds { x, y, width, height } => {
|
||||
if let Some(render_state) = &mut self.render_state
|
||||
&& let Some(window) = &self.window
|
||||
{
|
||||
let window_size = window.surface_size();
|
||||
|
||||
let viewport_offset_x = x / window_size.width as f64;
|
||||
let viewport_offset_y = y / window_size.height as f64;
|
||||
render_state.set_viewport_offset([viewport_offset_x as f32, viewport_offset_y as f32]);
|
||||
|
||||
let viewport_scale_x = if width != 0. { window_size.width as f64 / width } else { 1. };
|
||||
let viewport_scale_y = if height != 0. { window_size.height as f64 / height } else { 1. };
|
||||
render_state.set_viewport_scale([viewport_scale_x as f32, viewport_scale_y as f32]);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::UpdateUIScale { scale } => {
|
||||
self.ui_scale = scale;
|
||||
self.resize();
|
||||
}
|
||||
DesktopFrontendMessage::UpdateOverlays(scene) => {
|
||||
if let Some(render_state) = &mut self.render_state {
|
||||
render_state.set_overlays_scene(scene);
|
||||
}
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceWriteState { state } => {
|
||||
persist::write_state(state);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceReadState => {
|
||||
responses.push(DesktopWrapperMessage::LoadPersistedState { state: persist::read_state() });
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceReadDocument { id } => {
|
||||
if let Some(document) = persist::read_document_content(&id) {
|
||||
responses.push(DesktopWrapperMessage::LoadDocumentContent { id, document });
|
||||
} else {
|
||||
tracing::error!("Failed to read document content for {id:?}");
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceWriteDocument { id, document_serialized_content } => {
|
||||
persist::write_document_content(id, document_serialized_content);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceDeleteDocument { id } => {
|
||||
persist::delete_document(&id);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceWritePreferences { preferences } => {
|
||||
preferences::write(preferences);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceLoadPreferences => {
|
||||
let preferences = preferences::read();
|
||||
let message = DesktopWrapperMessage::LoadPreferences { preferences };
|
||||
responses.push(message);
|
||||
}
|
||||
DesktopFrontendMessage::OpenLaunchDocuments => {
|
||||
let Some(launch_documents) = std::mem::take(&mut self.launch_documents) else {
|
||||
tracing::error!("OpenLaunchDocuments should only be sent once");
|
||||
return;
|
||||
};
|
||||
self.app_event_scheduler.schedule(AppEvent::OpenFiles(launch_documents));
|
||||
}
|
||||
DesktopFrontendMessage::UpdateMenu { entries } => {
|
||||
if let Some(window) = &self.window {
|
||||
window.update_menu(entries);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::ClipboardRead => {
|
||||
if let Some(window) = &self.window {
|
||||
let content = window.clipboard_read();
|
||||
let message = DesktopWrapperMessage::ClipboardReadResult { content };
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::ClipboardWrite { content } => {
|
||||
if let Some(window) = &mut self.window {
|
||||
window.clipboard_write(content);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::PointerLock => {
|
||||
self.pointer_lock_position = Some(self.pointer_position);
|
||||
if let Some(window) = &self.window {
|
||||
window.start_pointer_lock();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::WindowClose => {
|
||||
self.app_event_scheduler.schedule(AppEvent::Exit);
|
||||
}
|
||||
DesktopFrontendMessage::WindowMinimize => {
|
||||
if let Some(window) = &self.window {
|
||||
window.minimize();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::WindowMaximize => {
|
||||
if let Some(window) = &self.window {
|
||||
window.toggle_maximize();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::WindowFullscreen => {
|
||||
if let Some(window) = &mut self.window {
|
||||
window.toggle_fullscreen();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::WindowDrag => {
|
||||
self.window_pending_drag = true;
|
||||
}
|
||||
DesktopFrontendMessage::WindowFocus => {
|
||||
if let Some(window) = &self.window {
|
||||
window.focus();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::WindowHide => {
|
||||
if let Some(window) = &self.window {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::WindowHideOthers => {
|
||||
if let Some(window) = &self.window {
|
||||
window.hide_others();
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::WindowShowAll => {
|
||||
if let Some(window) = &self.window {
|
||||
window.show_all();
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_desktop_frontend_messages(&mut self, messages: Vec<DesktopFrontendMessage>) {
|
||||
let mut responses = Vec::new();
|
||||
for message in messages {
|
||||
self.handle_desktop_frontend_message(message, &mut responses);
|
||||
}
|
||||
for message in responses {
|
||||
self.dispatch_desktop_wrapper_message(message);
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_desktop_wrapper_message(&mut self, message: DesktopWrapperMessage) {
|
||||
let responses = self.desktop_wrapper.dispatch(message);
|
||||
self.handle_desktop_frontend_messages(responses);
|
||||
}
|
||||
|
||||
fn send_or_queue_web_message(&mut self, message: Vec<u8>) {
|
||||
if self.web_communication_initialized {
|
||||
self.ui.send(UiCommand::Message(message));
|
||||
} else {
|
||||
self.web_communication_startup_buffer.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
fn user_event(&mut self, event_loop: &dyn ActiveEventLoop, event: AppEvent) {
|
||||
match event {
|
||||
AppEvent::WebCommunicationInitialized => {
|
||||
self.web_communication_initialized = true;
|
||||
for message in self.web_communication_startup_buffer.drain(..) {
|
||||
self.ui.send(UiCommand::Message(message));
|
||||
}
|
||||
}
|
||||
AppEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message),
|
||||
AppEvent::NodeGraphExecutionResult(result) => match result {
|
||||
NodeGraphExecutionResult::HasRun(texture) => {
|
||||
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::PollNodeGraphEvaluation);
|
||||
if let Some(texture) = texture
|
||||
&& let Some(render_state) = self.render_state.as_mut()
|
||||
&& let Some(window) = self.window.as_ref()
|
||||
{
|
||||
render_state.bind_viewport_texture(texture);
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
NodeGraphExecutionResult::NotRun => {}
|
||||
},
|
||||
AppEvent::UiUpdate(texture) => {
|
||||
if let Some(render_state) = self.render_state.as_mut() {
|
||||
render_state.bind_ui_texture(texture);
|
||||
}
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
if !self.ui_frame_received {
|
||||
self.ui_frame_received = true;
|
||||
}
|
||||
}
|
||||
AppEvent::CursorChange(cursor) => {
|
||||
if let Some(window) = &mut self.window {
|
||||
window.set_cursor(event_loop, cursor);
|
||||
}
|
||||
}
|
||||
AppEvent::Exit => {
|
||||
tracing::info!("Exiting main event loop");
|
||||
event_loop.exit();
|
||||
}
|
||||
AppEvent::UiCrashed => {
|
||||
tracing::error!("UI process crashed, exiting.");
|
||||
self.exit(Some(ExitReason::Shutdown));
|
||||
}
|
||||
AppEvent::OpenFiles(paths) => {
|
||||
// Accumulate launch documents until OpenLaunchDocuments message is received
|
||||
if let Some(launch_documents) = &mut self.launch_documents {
|
||||
launch_documents.extend(paths);
|
||||
return;
|
||||
}
|
||||
|
||||
if paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
let app_event_scheduler = self.app_event_scheduler.clone();
|
||||
let _ = thread::spawn(move || {
|
||||
for path in paths {
|
||||
tracing::info!("Opening file: {}", path.display());
|
||||
if let Ok(content) = fs::read(&path) {
|
||||
let message = DesktopWrapperMessage::OpenFile { path, content };
|
||||
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
} else {
|
||||
tracing::error!("Failed to read file: {}", path.display());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
AppEvent::MenuEvent { id } => {
|
||||
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ApplicationHandler for App {
|
||||
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
|
||||
let window = Window::new(event_loop, self.app_event_scheduler.clone());
|
||||
self.window = Some(window);
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let present_mode = None;
|
||||
#[cfg(target_os = "macos")]
|
||||
let present_mode = if !self.preferences.vsync { Some(wgpu::PresentMode::Immediate) } else { None };
|
||||
|
||||
let render_state = RenderState::new(self.window.as_ref().unwrap(), self.wgpu_context.clone(), present_mode);
|
||||
self.render_state = Some(render_state);
|
||||
|
||||
if let Some(window) = &self.window.as_ref() {
|
||||
window.show();
|
||||
}
|
||||
|
||||
self.resize();
|
||||
|
||||
self.startup_time = Some(Instant::now());
|
||||
}
|
||||
|
||||
fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) {
|
||||
while let Ok(event) = self.app_event_receiver.try_recv() {
|
||||
self.user_event(event_loop, event);
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(&mut self, _event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
|
||||
// Handle pointer lock release
|
||||
if let Some(pointer_lock_position) = self.pointer_lock_position
|
||||
&& let WindowEvent::PointerButton {
|
||||
state: ElementState::Released,
|
||||
button: ButtonSource::Mouse(MouseButton::Left),
|
||||
..
|
||||
} = event
|
||||
{
|
||||
self.pointer_lock_position = None;
|
||||
if let Some(window) = &self.window {
|
||||
window.end_pointer_lock();
|
||||
}
|
||||
self.ui.send(UiCommand::Input(WindowEvent::PointerMoved {
|
||||
device_id: None,
|
||||
position: pointer_lock_position,
|
||||
primary: true,
|
||||
source: winit::event::PointerSource::Mouse,
|
||||
}));
|
||||
}
|
||||
|
||||
self.ui.send(UiCommand::Input(event.clone()));
|
||||
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
self.app_event_scheduler.schedule(AppEvent::Exit);
|
||||
}
|
||||
WindowEvent::SurfaceResized(_) | WindowEvent::ScaleFactorChanged { .. } => {
|
||||
self.resize();
|
||||
}
|
||||
WindowEvent::RedrawRequested => {
|
||||
#[cfg(target_os = "macos")]
|
||||
self.resize();
|
||||
|
||||
let Some(render_state) = &mut self.render_state else { return };
|
||||
if let Some(window) = &self.window {
|
||||
if !window.can_render() {
|
||||
return;
|
||||
}
|
||||
|
||||
match render_state.render(window) {
|
||||
Ok(_) => {}
|
||||
Err(RenderError::OutdatedUITextureError) => {
|
||||
self.ui.send(UiCommand::Refresh);
|
||||
}
|
||||
Err(RenderError::SurfaceLost) => {
|
||||
tracing::warn!("lost surface");
|
||||
}
|
||||
Err(other) => tracing::error!("Render error: {:?}", other),
|
||||
}
|
||||
let _ = self.start_render_sender.try_send(());
|
||||
}
|
||||
|
||||
if !self.ui_frame_received
|
||||
&& !self.preferences.disable_ui_acceleration
|
||||
&& self.web_communication_initialized
|
||||
&& let Some(startup_time) = self.startup_time
|
||||
&& startup_time.elapsed() > Duration::from_secs(3)
|
||||
{
|
||||
tracing::error!("UI acceleration not working, exiting.");
|
||||
self.exit(Some(ExitReason::UiAccelerationFailure));
|
||||
}
|
||||
}
|
||||
WindowEvent::DragDropped { paths, .. } => {
|
||||
for path in paths {
|
||||
match fs::read(&path) {
|
||||
Ok(content) => {
|
||||
let message = DesktopWrapperMessage::ImportFile { path, content };
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to read dropped file {}: {}", path.display(), e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Forward and Back buttons are not supported by CEF and thus need to be directly forwarded the editor
|
||||
WindowEvent::PointerButton {
|
||||
button: ButtonSource::Mouse(button),
|
||||
state: ElementState::Pressed,
|
||||
..
|
||||
} => {
|
||||
let mouse_keys = match button {
|
||||
MouseButton::Back => Some(MouseKeys::BACK),
|
||||
MouseButton::Forward => Some(MouseKeys::FORWARD),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(mouse_keys) = mouse_keys {
|
||||
let message = DesktopWrapperMessage::Input(InputMessage::PointerDown {
|
||||
editor_mouse_state: MouseState { mouse_keys, ..Default::default() },
|
||||
modifier_keys: Default::default(),
|
||||
});
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
|
||||
let message = DesktopWrapperMessage::Input(InputMessage::PointerUp {
|
||||
editor_mouse_state: Default::default(),
|
||||
modifier_keys: Default::default(),
|
||||
});
|
||||
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::PointerMoved { position, .. } | WindowEvent::PointerLeft { position: Some(position), .. } | WindowEvent::PointerEntered { position, .. }
|
||||
if self.pointer_lock_position.is_none() =>
|
||||
{
|
||||
self.pointer_position = position;
|
||||
|
||||
if self.window_pending_drag {
|
||||
self.window_pending_drag = false;
|
||||
if let Some(window) = &self.window {
|
||||
window.start_drag();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::PointerButton {
|
||||
button: ButtonSource::Mouse(MouseButton::Left),
|
||||
state: ElementState::Released,
|
||||
..
|
||||
} => {
|
||||
self.window_pending_drag = false;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
event_loop.set_control_flow(ControlFlow::WaitUntil(Instant::now() + Duration::from_millis(10)));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum ExitReason {
|
||||
Shutdown,
|
||||
Restart,
|
||||
UiAccelerationFailure,
|
||||
}
|
||||
9
.jjconflict-base-0/desktop/src/cli.rs
Normal file
9
.jjconflict-base-0/desktop/src/cli.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
#[derive(clap::Parser)]
|
||||
#[clap(name = "graphite", version)]
|
||||
pub struct Cli {
|
||||
#[arg(help = "Files to open on startup")]
|
||||
pub files: Vec<std::path::PathBuf>,
|
||||
|
||||
#[arg(long, action = clap::ArgAction::SetTrue, help = "Disable hardware accelerated UI rendering")]
|
||||
pub disable_ui_acceleration: bool,
|
||||
}
|
||||
14
.jjconflict-base-0/desktop/src/consts.rs
Normal file
14
.jjconflict-base-0/desktop/src/consts.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
pub(crate) const APP_NAME: &str = "Graphite";
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
pub(crate) const APP_ID: &str = "art.graphite.Graphite";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) const APP_DIRECTORY_NAME: &str = "graphite";
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) const APP_DIRECTORY_NAME: &str = "Graphite";
|
||||
pub(crate) const APP_LOCK_FILE_NAME: &str = "instance.lock";
|
||||
pub(crate) const APP_SOCKET_FILE_NAME: &str = "instance.sock";
|
||||
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
|
||||
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron";
|
||||
pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents";
|
||||
pub(crate) const APP_RESOURCES_DIRECTORY_NAME: &str = "resources";
|
||||
55
.jjconflict-base-0/desktop/src/dirs.rs
Normal file
55
.jjconflict-base-0/desktop/src/dirs.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::consts::{APP_DIRECTORY_NAME, APP_DOCUMENTS_DIRECTORY_NAME, APP_RESOURCES_DIRECTORY_NAME};
|
||||
|
||||
pub(crate) fn ensure_dir_exists(path: &PathBuf) {
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(path).unwrap_or_else(|_| panic!("Failed to create directory at {path:?}"));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_dir(path: &PathBuf) {
|
||||
let Ok(entries) = fs::read_dir(path) else {
|
||||
tracing::error!("Failed to read directory at {path:?}");
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let entry_path = entry.path();
|
||||
if entry_path.is_dir() {
|
||||
if let Err(e) = fs::remove_dir_all(&entry_path) {
|
||||
tracing::error!("Failed to remove directory at {:?}: {}", entry_path, e);
|
||||
}
|
||||
} else if entry_path.is_file() {
|
||||
if let Err(e) = fs::remove_file(&entry_path) {
|
||||
tracing::error!("Failed to remove file at {:?}: {}", entry_path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn app_data_dir() -> PathBuf {
|
||||
let path = dirs::data_dir().expect("Failed to get data directory").join(APP_DIRECTORY_NAME);
|
||||
ensure_dir_exists(&path);
|
||||
path
|
||||
}
|
||||
|
||||
pub(crate) fn app_autosave_documents_dir() -> PathBuf {
|
||||
let path = app_data_dir().join(APP_DOCUMENTS_DIRECTORY_NAME);
|
||||
ensure_dir_exists(&path);
|
||||
path
|
||||
}
|
||||
|
||||
pub(crate) fn app_resources_dir() -> PathBuf {
|
||||
let path = app_data_dir().join(APP_RESOURCES_DIRECTORY_NAME);
|
||||
ensure_dir_exists(&path);
|
||||
path
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this cleanup code for the old "browser" CEF directory
|
||||
pub(crate) fn delete_old_cef_browser_directory() {
|
||||
let old_browser_dir = crate::dirs::app_data_dir().join("browser");
|
||||
if old_browser_dir.is_dir() {
|
||||
let _ = std::fs::remove_dir_all(&old_browser_dir);
|
||||
}
|
||||
}
|
||||
41
.jjconflict-base-0/desktop/src/event.rs
Normal file
41
.jjconflict-base-0/desktop/src/event.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use crate::ui::Cursor;
|
||||
use crate::wrapper::NodeGraphExecutionResult;
|
||||
use crate::wrapper::messages::DesktopWrapperMessage;
|
||||
|
||||
pub(crate) enum AppEvent {
|
||||
UiUpdate(wgpu::Texture),
|
||||
CursorChange(Cursor),
|
||||
WebCommunicationInitialized,
|
||||
DesktopWrapperMessage(DesktopWrapperMessage),
|
||||
NodeGraphExecutionResult(NodeGraphExecutionResult),
|
||||
Exit,
|
||||
UiCrashed,
|
||||
OpenFiles(Vec<std::path::PathBuf>),
|
||||
#[cfg(target_os = "macos")]
|
||||
MenuEvent {
|
||||
id: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AppEventScheduler {
|
||||
pub(crate) proxy: winit::event_loop::EventLoopProxy,
|
||||
pub(crate) sender: std::sync::mpsc::Sender<AppEvent>,
|
||||
}
|
||||
|
||||
impl AppEventScheduler {
|
||||
pub(crate) fn schedule(&self, event: AppEvent) {
|
||||
let _ = self.sender.send(event);
|
||||
self.proxy.wake_up();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait CreateAppEventSchedulerEventLoopExt {
|
||||
fn create_app_event_scheduler(&self, sender: std::sync::mpsc::Sender<AppEvent>) -> AppEventScheduler;
|
||||
}
|
||||
|
||||
impl CreateAppEventSchedulerEventLoopExt for winit::event_loop::EventLoop {
|
||||
fn create_app_event_scheduler(&self, sender: std::sync::mpsc::Sender<AppEvent>) -> AppEventScheduler {
|
||||
AppEventScheduler { proxy: self.create_proxy(), sender }
|
||||
}
|
||||
}
|
||||
21
.jjconflict-base-0/desktop/src/gpu_context.rs
Normal file
21
.jjconflict-base-0/desktop/src/gpu_context.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use crate::wrapper::{WgpuContext, WgpuContextBuilder, WgpuFeatures};
|
||||
|
||||
pub(super) async fn create_wgpu_context() -> WgpuContext {
|
||||
let mut wgpu_context_builder = WgpuContextBuilder::new().with_features(WgpuFeatures::IMMEDIATES);
|
||||
|
||||
// TODO: make this configurable via cli flags instead
|
||||
if let Some(index) = std::env::var("GRAPHITE_WGPU_ADAPTER").ok().and_then(|s| s.parse().ok()) {
|
||||
tracing::info!("Overriding WGPU adapter selection with adapter index {index}");
|
||||
wgpu_context_builder = wgpu_context_builder.with_selection(index);
|
||||
}
|
||||
|
||||
// TODO: add a cli flag to list adapters and exit instead of always printing
|
||||
println!("\nAvailable WGPU adapters:\n{}", wgpu_context_builder.available_adapters_fmt().await);
|
||||
|
||||
let wgpu_context = wgpu_context_builder.build().await.expect("Failed to create WGPU context");
|
||||
|
||||
// TODO: add a cli flag to list adapters and exit instead of always printing
|
||||
println!("Using WGPU adapter: {:?}", wgpu_context.adapter.get_info());
|
||||
|
||||
wgpu_context
|
||||
}
|
||||
175
.jjconflict-base-0/desktop/src/lib.rs
Normal file
175
.jjconflict-base-0/desktop/src/lib.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
use crate::app::App;
|
||||
use crate::cli::Cli;
|
||||
use crate::consts::APP_LOCK_FILE_NAME;
|
||||
use crate::event::{AppEvent, CreateAppEventSchedulerEventLoopExt};
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
use std::process::ExitCode;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use ui::{Acceleration, UiConfig, UiContext, UiEvent, UiSetupResult};
|
||||
use winit::event_loop::EventLoop;
|
||||
|
||||
pub(crate) use graphite_desktop_ui as ui;
|
||||
pub(crate) use graphite_desktop_wrapper as wrapper;
|
||||
|
||||
mod app;
|
||||
mod cli;
|
||||
mod dirs;
|
||||
mod event;
|
||||
mod gpu_context;
|
||||
mod persist;
|
||||
mod preferences;
|
||||
mod render;
|
||||
mod socket;
|
||||
mod window;
|
||||
|
||||
pub(crate) mod consts;
|
||||
|
||||
pub fn start() -> ExitCode {
|
||||
tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init();
|
||||
|
||||
let ui_context = match UiContext::setup() {
|
||||
UiSetupResult::Ready(context) => context,
|
||||
UiSetupResult::Helper(code) => return code,
|
||||
UiSetupResult::Failed => {
|
||||
tracing::error!("Failed to set up the UI runtime");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
let Ok(lock_file) = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(dirs::app_data_dir().join(APP_LOCK_FILE_NAME))
|
||||
else {
|
||||
tracing::error!("Failed to open lock file.");
|
||||
return ExitCode::FAILURE;
|
||||
};
|
||||
let mut lock = fd_lock::RwLock::new(lock_file);
|
||||
let lock = match lock.try_write() {
|
||||
Ok(mut guard) => {
|
||||
tracing::info!("Acquired application lock");
|
||||
let _ = guard.set_len(0);
|
||||
let _ = write!(guard, "{}", std::process::id());
|
||||
let _ = guard.sync_all();
|
||||
guard
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!("Another instance is already running, Exiting.");
|
||||
if !cli.files.is_empty()
|
||||
&& let Err(error) = socket::send(socket::Message::OpenFiles(cli.files))
|
||||
{
|
||||
tracing::error!("Failed to send socket message to running instance: {}", error);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
};
|
||||
|
||||
dirs::clear_dir(&ui::temp_dir_root());
|
||||
|
||||
// TODO: Eventually remove this cleanup code for the old "browser" CEF directory
|
||||
dirs::delete_old_cef_browser_directory();
|
||||
|
||||
let mut prefs = preferences::read();
|
||||
|
||||
// Must be called before event loop initialization or native window integrations will break
|
||||
App::init();
|
||||
|
||||
let wgpu_context = futures::executor::block_on(gpu_context::create_wgpu_context());
|
||||
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
let (app_event_sender, app_event_receiver) = std::sync::mpsc::channel();
|
||||
let app_event_scheduler = event_loop.create_app_event_scheduler(app_event_sender);
|
||||
|
||||
let _socket_handle = socket::start(app_event_scheduler.clone());
|
||||
|
||||
if cli.disable_ui_acceleration {
|
||||
prefs.disable_ui_acceleration = true;
|
||||
}
|
||||
if prefs.disable_ui_acceleration {
|
||||
println!("UI acceleration is disabled");
|
||||
}
|
||||
|
||||
let acceleration = if prefs.disable_ui_acceleration { Acceleration::Disabled } else { Acceleration::Auto };
|
||||
let ui_context = match ui_context.start(UiConfig { acceleration }) {
|
||||
Ok(context) => context,
|
||||
Err(error) => {
|
||||
tracing::error!("Failed to start the UI runtime: {error}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let ui = match ui_context.instance(&wgpu_context.device, &wgpu_context.queue) {
|
||||
Ok(ui) => ui,
|
||||
Err(error) => {
|
||||
tracing::error!("Failed to start the UI: {error}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
tracing::info!("UI runtime started successfully");
|
||||
|
||||
{
|
||||
let ui = ui.clone();
|
||||
let scheduler = app_event_scheduler.clone();
|
||||
let spawned = std::thread::Builder::new().name("ui-events".to_string()).spawn(move || {
|
||||
while let Some(event) = ui.recv() {
|
||||
match event {
|
||||
UiEvent::Ready => scheduler.schedule(AppEvent::WebCommunicationInitialized),
|
||||
UiEvent::Frame(texture) => scheduler.schedule(AppEvent::UiUpdate(texture)),
|
||||
UiEvent::Cursor(cursor) => scheduler.schedule(AppEvent::CursorChange(cursor)),
|
||||
UiEvent::Message(message) => match wrapper::deserialize_editor_message(&message) {
|
||||
Some(message) => scheduler.schedule(AppEvent::DesktopWrapperMessage(message)),
|
||||
None => tracing::error!("Failed to deserialize web message"),
|
||||
},
|
||||
UiEvent::Failure(error) => {
|
||||
tracing::error!("UI failure: {error}");
|
||||
scheduler.schedule(AppEvent::UiCrashed);
|
||||
}
|
||||
UiEvent::Crashed => scheduler.schedule(AppEvent::UiCrashed),
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Err(error) = spawned {
|
||||
tracing::error!("Failed to spawn the UI event bridge thread: {error}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
let app = App::new(ui.clone(), wgpu_context, app_event_receiver, app_event_scheduler, prefs, cli.files);
|
||||
|
||||
let exit_reason = app.run(event_loop);
|
||||
|
||||
// ui needs to be shutdown before restarting
|
||||
ui.shutdown();
|
||||
|
||||
// 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 {
|
||||
app::ExitReason::Restart | app::ExitReason::UiAccelerationFailure => {
|
||||
tracing::info!("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(target_family = "unix")]
|
||||
tracing::error!("Failed to restart application");
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
let _ = command.spawn();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
3
.jjconflict-base-0/desktop/src/main.rs
Normal file
3
.jjconflict-base-0/desktop/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() -> std::process::ExitCode {
|
||||
graphite_desktop::start()
|
||||
}
|
||||
92
.jjconflict-base-0/desktop/src/persist.rs
Normal file
92
.jjconflict-base-0/desktop/src/persist.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use crate::wrapper::messages::{DocumentId, PersistedState};
|
||||
|
||||
pub(crate) fn read_state() -> PersistedState {
|
||||
let path = state_file_path();
|
||||
let data = match std::fs::read_to_string(&path) {
|
||||
Ok(d) => d,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
tracing::info!("No persistent data file found at {path:?}, starting fresh");
|
||||
return PersistedState::default();
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to read persistent data from disk: {e}");
|
||||
return PersistedState::default();
|
||||
}
|
||||
};
|
||||
let loaded = match ron::from_str(&data) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to deserialize persistent data: {e}");
|
||||
return PersistedState::default();
|
||||
}
|
||||
};
|
||||
|
||||
garbage_collect_document_files(&loaded);
|
||||
loaded
|
||||
}
|
||||
|
||||
pub(crate) fn write_state(state: PersistedState) {
|
||||
let state: &PersistedState = &state;
|
||||
let data = match ron::ser::to_string_pretty(state, Default::default()) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to serialize persistent data: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) = std::fs::write(state_file_path(), data) {
|
||||
tracing::error!("Failed to write persistent data to disk: {e}");
|
||||
}
|
||||
garbage_collect_document_files(state);
|
||||
}
|
||||
|
||||
pub(crate) fn write_document_content(id: DocumentId, document_content: String) {
|
||||
if let Err(e) = std::fs::write(document_content_path(&id), document_content) {
|
||||
tracing::error!("Failed to write document {id:?} to disk: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read_document_content(id: &DocumentId) -> Option<String> {
|
||||
std::fs::read_to_string(document_content_path(id)).ok()
|
||||
}
|
||||
|
||||
pub(crate) fn delete_document(id: &DocumentId) {
|
||||
if let Err(e) = std::fs::remove_file(document_content_path(id)) {
|
||||
tracing::error!("Failed to delete document {id:?} from disk: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
fn garbage_collect_document_files(state: &PersistedState) {
|
||||
let valid_paths: std::collections::HashSet<_> = state.documents.iter().map(|doc| document_content_path(&doc.id)).collect();
|
||||
|
||||
let directory = crate::dirs::app_autosave_documents_dir();
|
||||
let entries = match std::fs::read_dir(&directory) {
|
||||
Ok(entries) => entries,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to read autosave documents directory: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_file() && !valid_paths.contains(&path) {
|
||||
if let Err(e) = std::fs::remove_file(&path) {
|
||||
tracing::error!("Failed to remove orphaned document file {path:?}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn state_file_path() -> std::path::PathBuf {
|
||||
let mut path = crate::dirs::app_data_dir();
|
||||
path.push(crate::consts::APP_STATE_FILE_NAME);
|
||||
path
|
||||
}
|
||||
|
||||
fn document_content_path(id: &DocumentId) -> std::path::PathBuf {
|
||||
let mut path = crate::dirs::app_autosave_documents_dir();
|
||||
path.push(format!("{:x}.{}", id.0, graphite_desktop_wrapper::FILE_EXTENSION));
|
||||
path
|
||||
}
|
||||
33
.jjconflict-base-0/desktop/src/preferences.rs
Normal file
33
.jjconflict-base-0/desktop/src/preferences.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use graphite_desktop_wrapper::messages::Preferences;
|
||||
|
||||
pub(crate) fn write(preferences: Preferences) {
|
||||
let Ok(preferences) = ron::ser::to_string_pretty(&preferences, Default::default()) else {
|
||||
tracing::error!("Failed to serialize preferences");
|
||||
return;
|
||||
};
|
||||
std::fs::write(file_path(), &preferences).unwrap_or_else(|e| {
|
||||
tracing::error!("Failed to write preferences to disk: {e}");
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn read() -> Preferences {
|
||||
let Ok(data) = std::fs::read_to_string(file_path()) else {
|
||||
return Preferences::default();
|
||||
};
|
||||
let Ok(preferences) = ron::from_str(&data) else {
|
||||
return Preferences::default();
|
||||
};
|
||||
preferences
|
||||
}
|
||||
|
||||
pub(crate) fn modify(f: impl FnOnce(&mut Preferences)) {
|
||||
let mut preferences = read();
|
||||
f(&mut preferences);
|
||||
write(preferences);
|
||||
}
|
||||
|
||||
fn file_path() -> std::path::PathBuf {
|
||||
let mut path = crate::dirs::app_data_dir();
|
||||
path.push(crate::consts::APP_PREFERENCES_FILE_NAME);
|
||||
path
|
||||
}
|
||||
2
.jjconflict-base-0/desktop/src/render.rs
Normal file
2
.jjconflict-base-0/desktop/src/render.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod state;
|
||||
pub(crate) use state::{RenderError, RenderState};
|
||||
121
.jjconflict-base-0/desktop/src/render/composite_shader.wgsl
Normal file
121
.jjconflict-base-0/desktop/src/render/composite_shader.wgsl
Normal file
@@ -0,0 +1,121 @@
|
||||
// =============
|
||||
// VERTEX SHADER
|
||||
// =============
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) tex_coords: vec2<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
let pos = array(
|
||||
vec2f(-1.0, -1.0),
|
||||
vec2f(3.0, -1.0),
|
||||
vec2f(-1.0, 3.0),
|
||||
);
|
||||
let xy = pos[vertex_index];
|
||||
out.clip_position = vec4f(xy, 0.0, 1.0);
|
||||
let coords = xy / 2. + 0.5;
|
||||
out.tex_coords = vec2f(coords.x, 1. - coords.y);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ===============
|
||||
// FRAGMENT SHADER
|
||||
// ===============
|
||||
|
||||
struct Immediates {
|
||||
viewport_scale: vec2<f32>,
|
||||
viewport_offset: vec2<f32>,
|
||||
ui_scale: vec2<f32>,
|
||||
background_color: vec4<f32>,
|
||||
};
|
||||
|
||||
var<immediate> immediates: Immediates;
|
||||
|
||||
@group(0) @binding(0)
|
||||
var t_viewport: texture_2d<f32>;
|
||||
@group(0) @binding(1)
|
||||
var t_overlays: texture_2d<f32>;
|
||||
@group(0) @binding(2)
|
||||
var t_ui: texture_2d<f32>;
|
||||
@group(0) @binding(3)
|
||||
var s_diffuse: sampler;
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let ui_coordinate = in.tex_coords * immediates.ui_scale;
|
||||
if (ui_coordinate.x < 0.0 || ui_coordinate.x > 1.0 ||
|
||||
ui_coordinate.y < 0.0 || ui_coordinate.y > 1.0) {
|
||||
return srgb_to_linear(immediates.background_color);
|
||||
}
|
||||
|
||||
let ui_linear = srgb_to_linear(textureSample(t_ui, s_diffuse, ui_coordinate));
|
||||
if (ui_linear.a >= 0.999) {
|
||||
return ui_linear;
|
||||
}
|
||||
|
||||
// UI texture is premultiplied, we need to unpremultiply before blending
|
||||
let ui_srgb = linear_to_srgb(unpremultiply(ui_linear));
|
||||
|
||||
let viewport_coordinate = (in.tex_coords - immediates.viewport_offset) * immediates.viewport_scale;
|
||||
if (viewport_coordinate.x < 0.0 || viewport_coordinate.x > 1.0 ||
|
||||
viewport_coordinate.y < 0.0 || viewport_coordinate.y > 1.0) {
|
||||
return srgb_to_linear(immediates.background_color);
|
||||
}
|
||||
|
||||
let overlay_srgb = textureSample(t_overlays, s_diffuse, viewport_coordinate);
|
||||
var viewport_srgb = textureSample(t_viewport, s_diffuse, viewport_coordinate);
|
||||
|
||||
if (viewport_srgb.a < 0.001) {
|
||||
viewport_srgb = immediates.background_color;
|
||||
} else if (viewport_srgb.a < 0.999) {
|
||||
viewport_srgb = blend(viewport_srgb, immediates.background_color);
|
||||
}
|
||||
|
||||
if (overlay_srgb.a < 0.001) {
|
||||
if (ui_srgb.a < 0.001) {
|
||||
return srgb_to_linear(viewport_srgb);
|
||||
} else {
|
||||
return srgb_to_linear(blend(ui_srgb, viewport_srgb));
|
||||
}
|
||||
}
|
||||
|
||||
let composite_linear = blend(srgb_to_linear(overlay_srgb), srgb_to_linear(viewport_srgb));
|
||||
|
||||
if (ui_srgb.a < 0.001) {
|
||||
return composite_linear;
|
||||
}
|
||||
|
||||
return srgb_to_linear(blend(ui_srgb, linear_to_srgb(composite_linear)));
|
||||
}
|
||||
|
||||
fn blend(fg: vec4<f32>, bg: vec4<f32>) -> vec4<f32> {
|
||||
let a = fg.a + bg.a * (1.0 - fg.a);
|
||||
let rgb = fg.rgb * fg.a + bg.rgb * bg.a * (1.0 - fg.a);
|
||||
return vec4<f32>(rgb, a);
|
||||
}
|
||||
|
||||
fn linear_to_srgb(in: vec4<f32>) -> vec4<f32> {
|
||||
let cutoff = vec3<f32>(0.0031308);
|
||||
let lo = in.rgb * 12.92;
|
||||
let hi = 1.055 * pow(max(in.rgb, vec3<f32>(0.0)), vec3<f32>(1.0/2.4)) - 0.055;
|
||||
return vec4<f32>(select(lo, hi, in.rgb > cutoff), in.a);
|
||||
}
|
||||
|
||||
fn srgb_to_linear(in: vec4<f32>) -> vec4<f32> {
|
||||
let cutoff = vec3<f32>(0.04045);
|
||||
let lo = in.rgb / 12.92;
|
||||
let hi = pow((in.rgb + 0.055) / 1.055, vec3<f32>(2.4));
|
||||
return vec4<f32>(select(lo, hi, in.rgb > cutoff), in.a);
|
||||
}
|
||||
|
||||
fn unpremultiply(in: vec4<f32>) -> vec4<f32> {
|
||||
if (in.a > 0.0) {
|
||||
return vec4<f32>((in.rgb / in.a), in.a);
|
||||
} else {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
}
|
||||
385
.jjconflict-base-0/desktop/src/render/state.rs
Normal file
385
.jjconflict-base-0/desktop/src/render/state.rs
Normal file
@@ -0,0 +1,385 @@
|
||||
use wgpu::PresentMode;
|
||||
|
||||
use crate::window::Window;
|
||||
use crate::wrapper::{WgpuContext, WgpuCurrentSurfaceTexture, WgpuExecutor, WgpuSurface};
|
||||
|
||||
#[derive(derivative::Derivative)]
|
||||
#[derivative(Debug)]
|
||||
pub(crate) struct RenderState {
|
||||
surface: WgpuSurface,
|
||||
executor: WgpuExecutor,
|
||||
config: wgpu::SurfaceConfiguration,
|
||||
render_pipeline: wgpu::RenderPipeline,
|
||||
transparent_texture: std::sync::Arc<wgpu::Texture>,
|
||||
sampler: wgpu::Sampler,
|
||||
desired_width: u32,
|
||||
desired_height: u32,
|
||||
viewport_scale: [f32; 2],
|
||||
viewport_offset: [f32; 2],
|
||||
viewport_texture: Option<std::sync::Arc<wgpu::Texture>>,
|
||||
overlays_texture: Option<std::sync::Arc<wgpu::Texture>>,
|
||||
ui_texture: Option<wgpu::Texture>,
|
||||
bind_group: Option<wgpu::BindGroup>,
|
||||
#[derivative(Debug = "ignore")]
|
||||
overlays_scene: Option<vello::Scene>,
|
||||
surface_outdated: bool,
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
pub(crate) fn new(window: &Window, context: WgpuContext, present_mode: Option<PresentMode>) -> Self {
|
||||
let size = window.surface_size();
|
||||
let surface = window.create_surface(&context.instance);
|
||||
|
||||
let surface_caps = surface.get_capabilities(&context.adapter);
|
||||
let surface_format = surface_caps.formats.iter().find(|f| f.is_srgb()).copied().unwrap_or(surface_caps.formats[0]);
|
||||
|
||||
let config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface_format,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: present_mode.unwrap_or(surface_caps.present_modes[0]),
|
||||
alpha_mode: surface_caps.alpha_modes[0],
|
||||
view_formats: vec![],
|
||||
desired_maximum_frame_latency: 1,
|
||||
};
|
||||
|
||||
surface.configure(&context.device, &config);
|
||||
|
||||
let transparent_texture = std::sync::Arc::new(context.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("Transparent Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: 1,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
}));
|
||||
|
||||
// Create shader module
|
||||
let shader = context.device.create_shader_module(wgpu::include_wgsl!("composite_shader.wgsl"));
|
||||
|
||||
// Create sampler
|
||||
let sampler = context.device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Nearest,
|
||||
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let texture_bind_group_layout = context.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
multisampled: false,
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
multisampled: false,
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
multisampled: false,
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 3,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: Some("texture_bind_group_layout"),
|
||||
});
|
||||
|
||||
let render_pipeline_layout = context.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Render Pipeline Layout"),
|
||||
bind_group_layouts: &[Some(&texture_bind_group_layout)],
|
||||
immediate_size: size_of::<Immediates>() as u32,
|
||||
});
|
||||
|
||||
let render_pipeline = context.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Render Pipeline"),
|
||||
layout: Some(&render_pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: config.format,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let executor = WgpuExecutor::with_context(context).expect("Failed to create WgpuExecutor");
|
||||
|
||||
Self {
|
||||
surface,
|
||||
executor,
|
||||
config,
|
||||
render_pipeline,
|
||||
transparent_texture,
|
||||
sampler,
|
||||
desired_width: size.width,
|
||||
desired_height: size.height,
|
||||
viewport_scale: [1., 1.],
|
||||
viewport_offset: [0., 0.],
|
||||
viewport_texture: None,
|
||||
overlays_texture: None,
|
||||
ui_texture: None,
|
||||
bind_group: None,
|
||||
overlays_scene: None,
|
||||
surface_outdated: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resize(&mut self, width: u32, height: u32) {
|
||||
if width == self.desired_width && height == self.desired_height {
|
||||
return;
|
||||
}
|
||||
|
||||
self.desired_width = width;
|
||||
self.desired_height = height;
|
||||
self.surface_outdated = true;
|
||||
}
|
||||
|
||||
pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: std::sync::Arc<wgpu::Texture>) {
|
||||
self.viewport_texture = Some(viewport_texture);
|
||||
self.update_bindgroup();
|
||||
}
|
||||
|
||||
pub(crate) fn bind_ui_texture(&mut self, ui_texture: wgpu::Texture) {
|
||||
if self.ui_texture.as_ref() == Some(&ui_texture) {
|
||||
self.surface_outdated = true;
|
||||
return;
|
||||
}
|
||||
self.ui_texture = Some(ui_texture);
|
||||
self.update_bindgroup();
|
||||
}
|
||||
|
||||
pub(crate) fn set_viewport_scale(&mut self, scale: [f32; 2]) {
|
||||
self.surface_outdated = true;
|
||||
self.viewport_scale = scale;
|
||||
}
|
||||
|
||||
pub(crate) fn set_viewport_offset(&mut self, offset: [f32; 2]) {
|
||||
self.surface_outdated = true;
|
||||
self.viewport_offset = offset;
|
||||
}
|
||||
|
||||
pub(crate) fn set_overlays_scene(&mut self, scene: vello::Scene) {
|
||||
self.surface_outdated = true;
|
||||
self.overlays_scene = Some(scene);
|
||||
}
|
||||
|
||||
fn render_overlays(&mut self, scene: vello::Scene) {
|
||||
let Some(viewport_texture) = self.viewport_texture.as_ref() else {
|
||||
tracing::warn!("No viewport texture bound, cannot render overlays");
|
||||
return;
|
||||
};
|
||||
let size = glam::UVec2::new(viewport_texture.width(), viewport_texture.height());
|
||||
let result = self.executor.render_vello_scene(&scene, size, &Default::default(), None);
|
||||
match result {
|
||||
Ok(texture) => {
|
||||
self.overlays_texture = Some(texture.into());
|
||||
}
|
||||
Err(e) => {
|
||||
self.overlays_texture = None;
|
||||
tracing::error!("Error rendering overlays: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
self.update_bindgroup();
|
||||
}
|
||||
|
||||
pub(crate) fn render(&mut self, window: &Window) -> Result<(), RenderError> {
|
||||
if !self.surface_outdated {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Apply resize once per presented frame.
|
||||
if self.desired_width > 0 && self.desired_height > 0 && (self.config.width != self.desired_width || self.config.height != self.desired_height) {
|
||||
self.config.width = self.desired_width;
|
||||
self.config.height = self.desired_height;
|
||||
self.surface.configure(&self.executor.context().device, &self.config);
|
||||
}
|
||||
|
||||
let ui_scale = if let Some(ui_texture) = &self.ui_texture
|
||||
&& (self.desired_width != ui_texture.width() || self.desired_height != ui_texture.height())
|
||||
{
|
||||
Some([self.desired_width as f32 / ui_texture.width() as f32, self.desired_height as f32 / ui_texture.height() as f32])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(scene) = self.overlays_scene.take() {
|
||||
self.render_overlays(scene);
|
||||
}
|
||||
|
||||
let (surface_texture, suboptimal) = match self.surface.get_current_texture(&self.executor.context().queue) {
|
||||
WgpuCurrentSurfaceTexture::Success(t) => (t, false),
|
||||
WgpuCurrentSurfaceTexture::Suboptimal(t) => (t, true),
|
||||
WgpuCurrentSurfaceTexture::Occluded => return Ok(()),
|
||||
WgpuCurrentSurfaceTexture::Lost => return Err(RenderError::SurfaceLost),
|
||||
WgpuCurrentSurfaceTexture::Outdated => return Err(RenderError::SurfaceOutdated),
|
||||
WgpuCurrentSurfaceTexture::Timeout => return Err(RenderError::SurfaceTimeout),
|
||||
WgpuCurrentSurfaceTexture::Validation => return Err(RenderError::SurfaceValidation),
|
||||
};
|
||||
|
||||
let view = surface_texture.texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let mut encoder = self.executor.context().device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder") });
|
||||
|
||||
{
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Graphite Composition Render Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.01, g: 0.01, b: 0.01, a: 1. }),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
|
||||
render_pass.set_pipeline(&self.render_pipeline);
|
||||
render_pass.set_immediates(
|
||||
0,
|
||||
bytemuck::bytes_of(&Immediates {
|
||||
viewport_scale: self.viewport_scale,
|
||||
viewport_offset: self.viewport_offset,
|
||||
ui_scale: ui_scale.unwrap_or([1., 1.]),
|
||||
_pad: [0., 0.],
|
||||
background_color: [0x22 as f32 / 0xff as f32, 0x22 as f32 / 0xff as f32, 0x22 as f32 / 0xff as f32, 1.], // #222222
|
||||
}),
|
||||
);
|
||||
if let Some(bind_group) = &self.bind_group {
|
||||
render_pass.set_bind_group(0, bind_group, &[]);
|
||||
render_pass.draw(0..3, 0..1); // Draw 3 vertices for fullscreen triangle
|
||||
} else {
|
||||
tracing::warn!("No bind group available - showing clear color only");
|
||||
}
|
||||
}
|
||||
surface_texture.queue.submit(std::iter::once(encoder.finish()));
|
||||
window.pre_present_notify();
|
||||
surface_texture.present();
|
||||
|
||||
if suboptimal {
|
||||
self.surface.configure(&self.executor.context().device, &self.config);
|
||||
}
|
||||
|
||||
if ui_scale.is_some() {
|
||||
return Err(RenderError::OutdatedUITextureError);
|
||||
}
|
||||
self.surface_outdated = false;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_bindgroup(&mut self) {
|
||||
self.surface_outdated = true;
|
||||
let viewport_texture_view = self.viewport_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let overlays_texture_view = self.overlays_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let ui_texture_view = self.ui_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let bind_group = self.executor.context().device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
layout: &self.render_pipeline.get_bind_group_layout(0),
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&viewport_texture_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(&overlays_texture_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: wgpu::BindingResource::TextureView(&ui_texture_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
||||
},
|
||||
],
|
||||
label: Some("texture_bind_group"),
|
||||
});
|
||||
|
||||
self.bind_group = Some(bind_group);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum RenderError {
|
||||
OutdatedUITextureError,
|
||||
SurfaceLost,
|
||||
SurfaceOutdated,
|
||||
SurfaceTimeout,
|
||||
SurfaceValidation,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Immediates {
|
||||
viewport_scale: [f32; 2],
|
||||
viewport_offset: [f32; 2],
|
||||
ui_scale: [f32; 2],
|
||||
_pad: [f32; 2],
|
||||
background_color: [f32; 4],
|
||||
}
|
||||
126
.jjconflict-base-0/desktop/src/socket.rs
Normal file
126
.jjconflict-base-0/desktop/src/socket.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use interprocess::local_socket::{GenericFilePath, GenericNamespaced, ListenerNonblockingMode, ListenerOptions, Name, prelude::*};
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::consts::APP_SOCKET_FILE_NAME;
|
||||
use crate::event::{AppEvent, AppEventScheduler};
|
||||
|
||||
// TODO: Needs to be integrated/replaced with the action system.
|
||||
// TODO: At that point this should just wrap the action, meaning all actions bindable by the user can also be accessed via the socket.
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) enum Message {
|
||||
OpenFiles(Vec<std::path::PathBuf>),
|
||||
}
|
||||
|
||||
fn handle_message(message: Message, app_event_scheduler: &AppEventScheduler) {
|
||||
match message {
|
||||
Message::OpenFiles(paths) => {
|
||||
app_event_scheduler.schedule(AppEvent::OpenFiles(paths));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn send(message: Message) -> std::io::Result<()> {
|
||||
let data = ron::ser::to_string(&message).map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
|
||||
let mut connection = interprocess::local_socket::Stream::connect(socket_name())?;
|
||||
connection.write_all(data.as_bytes())
|
||||
}
|
||||
|
||||
pub(crate) struct SocketHandle {
|
||||
thread: Option<thread::JoinHandle<()>>,
|
||||
shutdown_sender: mpsc::Sender<()>,
|
||||
}
|
||||
impl Drop for SocketHandle {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.shutdown_sender.send(());
|
||||
let _ = self.thread.take().expect("SocketHandle can only be dropped once").join();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn start(app_event_scheduler: AppEventScheduler) -> SocketHandle {
|
||||
let (shutdown_sender, shutdown_receiver) = mpsc::channel();
|
||||
|
||||
let thread = thread::Builder::new()
|
||||
.name("socket".to_string())
|
||||
.spawn(move || run(app_event_scheduler, shutdown_receiver))
|
||||
.expect("Failed to spawn socket thread");
|
||||
|
||||
SocketHandle {
|
||||
shutdown_sender,
|
||||
thread: Some(thread),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(app_event_scheduler: AppEventScheduler, shutdown_receiver: mpsc::Receiver<()>) {
|
||||
let listener = match ListenerOptions::new()
|
||||
.name(socket_name())
|
||||
.nonblocking(ListenerNonblockingMode::Accept)
|
||||
.try_overwrite(true)
|
||||
.max_spin_time(Duration::from_millis(100))
|
||||
.create_sync()
|
||||
{
|
||||
Ok(listener) => listener,
|
||||
Err(error) => {
|
||||
tracing::error!("Failed to bind socket: {}", error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let max_backoff = Duration::from_millis(100);
|
||||
let mut backoff = Duration::ZERO;
|
||||
|
||||
loop {
|
||||
if backoff.is_zero() {
|
||||
match shutdown_receiver.try_recv() {
|
||||
Ok(()) | Err(mpsc::TryRecvError::Disconnected) => break,
|
||||
Err(mpsc::TryRecvError::Empty) => {}
|
||||
}
|
||||
backoff = Duration::from_nanos(1);
|
||||
} else {
|
||||
match shutdown_receiver.recv_timeout(backoff) {
|
||||
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
||||
}
|
||||
backoff = (backoff * 2).min(max_backoff);
|
||||
}
|
||||
|
||||
match listener.accept() {
|
||||
Ok(mut connection) => {
|
||||
backoff = Duration::ZERO;
|
||||
|
||||
let app_event_scheduler = app_event_scheduler.clone();
|
||||
let spawn_result = thread::Builder::new().name("socket-connection".to_string()).spawn(move || {
|
||||
let mut data = String::new();
|
||||
if let Err(error) = connection.read_to_string(&mut data) {
|
||||
tracing::error!("Failed to read socket message: {}", error);
|
||||
return;
|
||||
}
|
||||
|
||||
match ron::de::from_str(&data) {
|
||||
Ok(message) => handle_message(message, &app_event_scheduler),
|
||||
Err(error) => tracing::error!("Failed to deserialize socket message: {}", error),
|
||||
}
|
||||
});
|
||||
if let Err(error) = spawn_result {
|
||||
tracing::error!("Failed to spawn socket connection thread: {}", error);
|
||||
}
|
||||
}
|
||||
Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {}
|
||||
Err(error) => {
|
||||
tracing::error!("Failed to accept socket connection: {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn socket_name() -> Name<'static> {
|
||||
if cfg!(target_os = "windows") {
|
||||
let user = std::env::var("USERNAME").unwrap_or_default();
|
||||
let name = format!("{user}-{app}-{APP_SOCKET_FILE_NAME}", app = crate::consts::APP_NAME);
|
||||
name.to_ns_name::<GenericNamespaced>().expect("valid named pipe name")
|
||||
} else {
|
||||
crate::dirs::app_data_dir().join(APP_SOCKET_FILE_NAME).to_fs_name::<GenericFilePath>().expect("valid socket path")
|
||||
}
|
||||
}
|
||||
235
.jjconflict-base-0/desktop/src/window.rs
Normal file
235
.jjconflict-base-0/desktop/src/window.rs
Normal file
@@ -0,0 +1,235 @@
|
||||
use crate::consts::APP_NAME;
|
||||
use crate::event::AppEventScheduler;
|
||||
use crate::ui::Cursor;
|
||||
use crate::wrapper::messages::MenuItem;
|
||||
use crate::wrapper::{WgpuInstance, WgpuSurface};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use winit::cursor::{CustomCursor, CustomCursorSource};
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
use winit::monitor::Fullscreen;
|
||||
use winit::window::{Window as WinitWindow, WindowAttributes};
|
||||
|
||||
pub(crate) trait NativeWindow {
|
||||
fn init() {}
|
||||
fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes;
|
||||
fn new(window: &dyn WinitWindow, app_event_scheduler: AppEventScheduler) -> Self;
|
||||
fn can_render(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn update_menu(&self, _entries: Vec<MenuItem>) {}
|
||||
fn hide(&self) {}
|
||||
fn hide_others(&self) {}
|
||||
fn show_all(&self) {}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(target_os = "linux")]
|
||||
use linux as native;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
#[cfg(target_os = "macos")]
|
||||
use mac as native;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win;
|
||||
#[cfg(target_os = "windows")]
|
||||
use win as native;
|
||||
|
||||
pub(crate) struct Window {
|
||||
winit_window: Arc<dyn winit::window::Window>,
|
||||
#[allow(dead_code)]
|
||||
native_handle: native::NativeWindowImpl,
|
||||
custom_cursors: HashMap<CustomCursorSource, CustomCursor>,
|
||||
clipboard: 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 {
|
||||
pub(crate) fn init() {
|
||||
native::NativeWindowImpl::init();
|
||||
}
|
||||
|
||||
pub(crate) fn new(event_loop: &dyn ActiveEventLoop, app_event_scheduler: AppEventScheduler) -> Self {
|
||||
let mut attributes = WindowAttributes::default()
|
||||
.with_title(APP_NAME)
|
||||
.with_min_surface_size(winit::dpi::LogicalSize::new(400, 300))
|
||||
.with_surface_size(winit::dpi::LogicalSize::new(1200, 800))
|
||||
.with_resizable(true)
|
||||
.with_visible(false)
|
||||
.with_theme(Some(winit::window::Theme::Dark));
|
||||
|
||||
attributes = native::NativeWindowImpl::configure(attributes, event_loop);
|
||||
|
||||
let winit_window = event_loop.create_window(attributes).unwrap();
|
||||
let native_handle = native::NativeWindowImpl::new(winit_window.as_ref(), app_event_scheduler);
|
||||
let clipboard = unsafe { window_clipboard::Clipboard::connect(&winit_window) }.ok();
|
||||
Self {
|
||||
winit_window: winit_window.into(),
|
||||
native_handle,
|
||||
custom_cursors: HashMap::new(),
|
||||
clipboard,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn show(&self) {
|
||||
self.winit_window.set_visible(true);
|
||||
self.winit_window.focus_window();
|
||||
}
|
||||
|
||||
pub(crate) fn request_redraw(&self) {
|
||||
self.winit_window.request_redraw();
|
||||
}
|
||||
|
||||
pub(crate) fn create_surface(&self, instance: &WgpuInstance) -> WgpuSurface {
|
||||
instance.create_surface(self.winit_window.clone()).expect("Failed to create surface")
|
||||
}
|
||||
|
||||
pub(crate) fn pre_present_notify(&self) {
|
||||
self.winit_window.pre_present_notify();
|
||||
}
|
||||
|
||||
pub(crate) fn can_render(&self) -> bool {
|
||||
self.native_handle.can_render()
|
||||
}
|
||||
|
||||
pub(crate) fn surface_size(&self) -> winit::dpi::PhysicalSize<u32> {
|
||||
self.winit_window.surface_size()
|
||||
}
|
||||
|
||||
pub(crate) fn scale_factor(&self) -> f64 {
|
||||
self.winit_window.scale_factor()
|
||||
}
|
||||
|
||||
pub(crate) fn minimize(&self) {
|
||||
self.winit_window.set_minimized(true);
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_maximize(&self) {
|
||||
if self.is_fullscreen() {
|
||||
return;
|
||||
}
|
||||
self.winit_window.set_maximized(!self.winit_window.is_maximized());
|
||||
}
|
||||
|
||||
pub(crate) fn is_maximized(&self) -> bool {
|
||||
self.winit_window.is_maximized()
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_fullscreen(&mut self) {
|
||||
if self.is_fullscreen() {
|
||||
self.winit_window.set_fullscreen(None);
|
||||
} else {
|
||||
self.winit_window.set_fullscreen(Some(Fullscreen::Borderless(None)));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_fullscreen(&self) -> bool {
|
||||
self.winit_window.fullscreen().is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn start_drag(&self) {
|
||||
if self.is_fullscreen() {
|
||||
return;
|
||||
}
|
||||
let _ = self.winit_window.drag_window();
|
||||
}
|
||||
|
||||
pub(crate) fn focus(&self) {
|
||||
self.winit_window.set_minimized(false);
|
||||
self.winit_window.focus_window();
|
||||
}
|
||||
|
||||
pub(crate) fn hide(&self) {
|
||||
self.native_handle.hide();
|
||||
}
|
||||
|
||||
pub(crate) fn hide_others(&self) {
|
||||
self.native_handle.hide_others();
|
||||
}
|
||||
|
||||
pub(crate) fn show_all(&self) {
|
||||
self.native_handle.show_all();
|
||||
}
|
||||
|
||||
pub(crate) fn set_cursor(&mut self, event_loop: &dyn ActiveEventLoop, cursor: Cursor) {
|
||||
let cursor = match cursor {
|
||||
Cursor::Icon(cursor_icon) => cursor_icon.into(),
|
||||
Cursor::Custom {
|
||||
rgba,
|
||||
width,
|
||||
height,
|
||||
hotspot_x,
|
||||
hotspot_y,
|
||||
} => {
|
||||
let Ok(custom_cursor_source) = CustomCursorSource::from_rgba(rgba, width, height, hotspot_x, hotspot_y) else {
|
||||
tracing::error!("Invalid custom cursor image");
|
||||
return;
|
||||
};
|
||||
let custom_cursor = match self.custom_cursors.get(&custom_cursor_source).cloned() {
|
||||
Some(cursor) => cursor,
|
||||
None => {
|
||||
let Ok(custom_cursor) = event_loop.create_custom_cursor(custom_cursor_source.clone()) else {
|
||||
tracing::error!("Failed to create custom cursor");
|
||||
return;
|
||||
};
|
||||
self.custom_cursors.insert(custom_cursor_source, custom_cursor.clone());
|
||||
custom_cursor
|
||||
}
|
||||
};
|
||||
custom_cursor.into()
|
||||
}
|
||||
Cursor::None => {
|
||||
self.winit_window.set_cursor_visible(false);
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.winit_window.set_cursor_visible(true);
|
||||
self.winit_window.set_cursor(cursor);
|
||||
}
|
||||
|
||||
pub(crate) fn start_pointer_lock(&self) {
|
||||
let _ = self.winit_window.set_cursor_grab(winit::window::CursorGrabMode::Locked);
|
||||
self.winit_window.set_cursor_visible(false);
|
||||
}
|
||||
|
||||
pub(crate) fn end_pointer_lock(&self) {
|
||||
let _ = self.winit_window.set_cursor_grab(winit::window::CursorGrabMode::None);
|
||||
self.winit_window.set_cursor_visible(true);
|
||||
}
|
||||
|
||||
pub(crate) fn update_menu(&self, entries: Vec<MenuItem>) {
|
||||
self.native_handle.update_menu(entries);
|
||||
}
|
||||
|
||||
pub(crate) fn clipboard_read(&self) -> Option<String> {
|
||||
let Some(clipboard) = &self.clipboard else {
|
||||
tracing::error!("Clipboard not available");
|
||||
return None;
|
||||
};
|
||||
match clipboard.read() {
|
||||
Ok(data) => Some(data),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to read from clipboard: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clipboard_write(&mut self, data: String) {
|
||||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
26
.jjconflict-base-0/desktop/src/window/linux.rs
Normal file
26
.jjconflict-base-0/desktop/src/window/linux.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
use winit::platform::wayland::ActiveEventLoopExtWayland;
|
||||
use winit::platform::wayland::WindowAttributesWayland;
|
||||
use winit::platform::x11::WindowAttributesX11;
|
||||
use winit::window::{Window, WindowAttributes};
|
||||
|
||||
use crate::consts::{APP_ID, APP_NAME};
|
||||
use crate::event::AppEventScheduler;
|
||||
|
||||
pub(super) struct NativeWindowImpl {}
|
||||
|
||||
impl super::NativeWindow for NativeWindowImpl {
|
||||
fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes {
|
||||
if event_loop.is_wayland() {
|
||||
let wayland_attributes = WindowAttributesWayland::default().with_name(APP_ID, "").with_prefer_csd(true);
|
||||
attributes.with_platform_attributes(Box::new(wayland_attributes))
|
||||
} else {
|
||||
let x11_attributes = WindowAttributesX11::default().with_name(APP_ID, APP_NAME);
|
||||
attributes.with_platform_attributes(Box::new(x11_attributes))
|
||||
}
|
||||
}
|
||||
|
||||
fn new(_window: &dyn Window, _app_event_scheduler: AppEventScheduler) -> Self {
|
||||
NativeWindowImpl {}
|
||||
}
|
||||
}
|
||||
49
.jjconflict-base-0/desktop/src/window/mac.rs
Normal file
49
.jjconflict-base-0/desktop/src/window/mac.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
use winit::platform::macos::WindowAttributesMacOS;
|
||||
use winit::window::{Window, WindowAttributes};
|
||||
|
||||
use crate::event::AppEventScheduler;
|
||||
use crate::wrapper::messages::MenuItem;
|
||||
|
||||
mod app;
|
||||
mod menu;
|
||||
|
||||
pub(super) struct NativeWindowImpl {
|
||||
menu: menu::Menu,
|
||||
}
|
||||
|
||||
impl super::NativeWindow for NativeWindowImpl {
|
||||
fn init() {
|
||||
app::init();
|
||||
}
|
||||
|
||||
fn configure(attributes: WindowAttributes, _event_loop: &dyn ActiveEventLoop) -> WindowAttributes {
|
||||
let mac_window = WindowAttributesMacOS::default()
|
||||
.with_titlebar_transparent(true)
|
||||
.with_fullsize_content_view(true)
|
||||
.with_title_hidden(true);
|
||||
attributes.with_platform_attributes(Box::new(mac_window))
|
||||
}
|
||||
|
||||
fn new(_window: &dyn Window, app_event_scheduler: AppEventScheduler) -> Self {
|
||||
app::setup(app_event_scheduler.clone());
|
||||
let menu = menu::Menu::new(app_event_scheduler);
|
||||
NativeWindowImpl { menu }
|
||||
}
|
||||
|
||||
fn update_menu(&self, entries: Vec<MenuItem>) {
|
||||
self.menu.update(entries);
|
||||
}
|
||||
|
||||
fn hide(&self) {
|
||||
app::hide();
|
||||
}
|
||||
|
||||
fn hide_others(&self) {
|
||||
app::hide_others();
|
||||
}
|
||||
|
||||
fn show_all(&self) {
|
||||
app::show_all();
|
||||
}
|
||||
}
|
||||
119
.jjconflict-base-0/desktop/src/window/mac/app.rs
Normal file
119
.jjconflict-base-0/desktop/src/window/mac/app.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use std::ffi::CStr;
|
||||
use std::ffi::OsStr;
|
||||
use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Mutex, Once};
|
||||
|
||||
use objc2::rc::Retained;
|
||||
use objc2::runtime::ProtocolObject;
|
||||
use objc2::{ClassType, MainThreadMarker, MainThreadOnly, define_class, msg_send};
|
||||
use objc2_app_kit::{NSApplication, NSApplicationDelegate, NSEvent, NSEventType, NSResponder};
|
||||
use objc2_foundation::{NSArray, NSObject, NSObjectProtocol, NSURL};
|
||||
|
||||
use crate::event::{AppEvent, AppEventScheduler};
|
||||
|
||||
static APP_EVENT_SCHEDULER: Mutex<Option<AppEventScheduler>> = Mutex::new(None);
|
||||
static PENDING_EVENTS: Mutex<Option<Vec<AppEvent>>> = Mutex::new(Some(Vec::new()));
|
||||
|
||||
fn dispatch_event(event: AppEvent) {
|
||||
let app_event_scheduler_guard = APP_EVENT_SCHEDULER.lock().unwrap();
|
||||
if let Some(app_event_scheduler) = app_event_scheduler_guard.deref() {
|
||||
app_event_scheduler.schedule(event);
|
||||
} else if let Some(pending_events) = PENDING_EVENTS.lock().unwrap().deref_mut() {
|
||||
pending_events.push(event);
|
||||
} else {
|
||||
tracing::error!("Failed to dispatch event");
|
||||
}
|
||||
}
|
||||
|
||||
fn instance() -> objc2::rc::Retained<NSApplication> {
|
||||
unsafe { msg_send![GraphiteApplication::class(), sharedApplication] }
|
||||
}
|
||||
|
||||
static INSTALL_DELEGATE: Once = Once::new();
|
||||
|
||||
pub(super) fn init() {
|
||||
let _ = instance();
|
||||
|
||||
INSTALL_DELEGATE.call_once(|| {
|
||||
let mtm = MainThreadMarker::new().expect("should only ever be called from main thread");
|
||||
let delegate: Retained<GraphiteApplicationDelegate> = unsafe { msg_send![super(GraphiteApplicationDelegate::alloc(mtm).set_ivars(())), init] };
|
||||
instance().setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
|
||||
std::mem::forget(delegate);
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn setup(app_event_scheduler: AppEventScheduler) {
|
||||
let mut app_event_scheduler_guard = APP_EVENT_SCHEDULER.lock().unwrap();
|
||||
|
||||
if let Some(mut pending_events) = PENDING_EVENTS.lock().unwrap().take() {
|
||||
pending_events.drain(..).for_each(|event| {
|
||||
app_event_scheduler.schedule(event);
|
||||
});
|
||||
} else {
|
||||
tracing::error!("Failed to take PENDING_EVENTS and schedule them. This a bug.");
|
||||
}
|
||||
|
||||
*app_event_scheduler_guard = Some(app_event_scheduler);
|
||||
}
|
||||
|
||||
pub(super) fn hide() {
|
||||
instance().hide(None);
|
||||
}
|
||||
|
||||
pub(super) fn hide_others() {
|
||||
instance().hideOtherApplications(None);
|
||||
}
|
||||
|
||||
pub(super) fn show_all() {
|
||||
instance().unhideAllApplications(None);
|
||||
}
|
||||
|
||||
define_class!(
|
||||
#[unsafe(super(NSApplication, NSResponder, NSObject))]
|
||||
#[name = "GraphiteApplication"]
|
||||
pub(super) struct GraphiteApplication;
|
||||
|
||||
impl GraphiteApplication {
|
||||
#[unsafe(method(sendEvent:))]
|
||||
fn send_event(&self, event: &NSEvent) {
|
||||
// Route keyDown events straight to the key window to skip native menu shortcut handling.
|
||||
if event.r#type() == NSEventType::KeyDown && let Some(key_window) = self.keyWindow() {
|
||||
unsafe { msg_send![&key_window, sendEvent: event] }
|
||||
} else {
|
||||
unsafe { msg_send![super(self), sendEvent: event] }
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
define_class!(
|
||||
#[unsafe(super(NSObject))]
|
||||
#[thread_kind = MainThreadOnly]
|
||||
#[name = "GraphiteApplicationDelegate"]
|
||||
struct GraphiteApplicationDelegate;
|
||||
|
||||
unsafe impl NSObjectProtocol for GraphiteApplicationDelegate {}
|
||||
|
||||
unsafe impl NSApplicationDelegate for GraphiteApplicationDelegate {
|
||||
#[unsafe(method(application:openURLs:))]
|
||||
fn application_open_urls(&self, _application: &NSApplication, urls: &NSArray<NSURL>) {
|
||||
let paths = (0..urls.count())
|
||||
.filter_map(|index| {
|
||||
let url = urls.objectAtIndex(index);
|
||||
if !url.isFileURL() {
|
||||
tracing::error!("Ignoring open URL event for non-file URL: {:?}", url);
|
||||
return None;
|
||||
}
|
||||
let cstr = unsafe { CStr::from_ptr(url.fileSystemRepresentation().as_ptr()) };
|
||||
let path = PathBuf::from(OsStr::from_bytes(cstr.to_bytes()));
|
||||
Some(path)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
dispatch_event(AppEvent::OpenFiles(paths));
|
||||
}
|
||||
}
|
||||
);
|
||||
146
.jjconflict-base-0/desktop/src/window/mac/menu.rs
Normal file
146
.jjconflict-base-0/desktop/src/window/mac/menu.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use muda::Menu as MudaMenu;
|
||||
use muda::accelerator::Accelerator;
|
||||
use muda::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, MenuItemKind, PredefinedMenuItem, Result, Submenu};
|
||||
|
||||
use crate::event::{AppEvent, AppEventScheduler};
|
||||
use crate::wrapper::messages::MenuItem as WrapperMenuItem;
|
||||
|
||||
pub(super) struct Menu {
|
||||
inner: MudaMenu,
|
||||
}
|
||||
|
||||
impl Menu {
|
||||
pub(super) fn new(event_scheduler: AppEventScheduler) -> Self {
|
||||
// TODO: Remove as much app submenu special handling as possible
|
||||
let app_submenu = Submenu::with_items("", true, &[]).unwrap();
|
||||
|
||||
let menu = MudaMenu::new();
|
||||
menu.prepend(&app_submenu).unwrap();
|
||||
|
||||
menu.init_for_nsapp();
|
||||
|
||||
MenuEvent::set_event_handler(Some(move |event: MenuEvent| {
|
||||
let mtm = objc2::MainThreadMarker::new().expect("only ever called from main thread");
|
||||
let is_shortcut_triggered = objc2_app_kit::NSApplication::sharedApplication(mtm)
|
||||
.mainMenu()
|
||||
.map(|m| m.highlightedItem().is_some())
|
||||
.unwrap_or_default();
|
||||
if is_shortcut_triggered {
|
||||
tracing::error!("A keyboard input triggered a menu event. This is most likely a bug. Please report!");
|
||||
return;
|
||||
}
|
||||
|
||||
let id = event.id().0.clone();
|
||||
event_scheduler.schedule(AppEvent::MenuEvent { id });
|
||||
}));
|
||||
|
||||
Menu { inner: menu }
|
||||
}
|
||||
|
||||
pub(super) fn update(&self, entries: Vec<WrapperMenuItem>) {
|
||||
let new_entries = menu_items_from_wrapper(entries);
|
||||
let existing_entries = self.inner.items();
|
||||
|
||||
let mut new_entries_iter = new_entries.iter();
|
||||
let mut existing_entries_iter = existing_entries.iter();
|
||||
|
||||
let incremental_update_ok = std::iter::from_fn(move || match (existing_entries_iter.next(), new_entries_iter.next()) {
|
||||
(Some(MenuItemKind::Submenu(old)), Some(MenuItemKind::Submenu(new))) if old.text() == new.text() => {
|
||||
replace_children(old, new.items());
|
||||
Some(true)
|
||||
}
|
||||
(None, None) => None,
|
||||
_ => Some(false),
|
||||
})
|
||||
.all(|b| b);
|
||||
|
||||
if !incremental_update_ok {
|
||||
// Fallback to full replace
|
||||
replace_children(&self.inner, new_entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn menu_items_from_wrapper(entries: Vec<WrapperMenuItem>) -> Vec<MenuItemKind> {
|
||||
let mut menu_items: Vec<MenuItemKind> = Vec::new();
|
||||
for entry in entries {
|
||||
match entry {
|
||||
WrapperMenuItem::Action { id, text, enabled, shortcut } => {
|
||||
let accelerator = shortcut.map(|s| Accelerator::new(Some(s.modifiers), s.key));
|
||||
let item = MenuItem::with_id(id, text, enabled, accelerator);
|
||||
menu_items.push(MenuItemKind::MenuItem(item));
|
||||
}
|
||||
WrapperMenuItem::Checkbox { id, text, enabled, shortcut, checked } => {
|
||||
let accelerator = shortcut.map(|s| Accelerator::new(Some(s.modifiers), s.key));
|
||||
let check = CheckMenuItem::with_id(id, text, enabled, checked, accelerator);
|
||||
menu_items.push(MenuItemKind::Check(check));
|
||||
}
|
||||
WrapperMenuItem::SubMenu { text: name, items, .. } => {
|
||||
let items = menu_items_from_wrapper(items);
|
||||
let items = items.iter().map(menu_item_kind_to_dyn).collect::<Vec<&dyn IsMenuItem>>();
|
||||
let submenu = Submenu::with_items(name, true, &items).unwrap();
|
||||
menu_items.push(MenuItemKind::Submenu(submenu));
|
||||
}
|
||||
WrapperMenuItem::Separator => {
|
||||
let separator = PredefinedMenuItem::separator();
|
||||
menu_items.push(MenuItemKind::Predefined(separator));
|
||||
}
|
||||
}
|
||||
}
|
||||
menu_items
|
||||
}
|
||||
|
||||
fn menu_item_kind_to_dyn(item: &MenuItemKind) -> &dyn IsMenuItem {
|
||||
match item {
|
||||
MenuItemKind::MenuItem(i) => i,
|
||||
MenuItemKind::Submenu(i) => i,
|
||||
MenuItemKind::Predefined(i) => i,
|
||||
MenuItemKind::Check(i) => i,
|
||||
MenuItemKind::Icon(i) => i,
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_children<'a, T: Into<MenuContainer<'a>>>(menu: T, new_items: Vec<MenuItemKind>) {
|
||||
let menu: MenuContainer = menu.into();
|
||||
let items = menu.items();
|
||||
for item in items.iter() {
|
||||
menu.remove(menu_item_kind_to_dyn(item)).unwrap();
|
||||
}
|
||||
let items = new_items.iter().map(menu_item_kind_to_dyn).collect::<Vec<&dyn IsMenuItem>>();
|
||||
menu.append_items(items.as_ref()).unwrap();
|
||||
}
|
||||
|
||||
enum MenuContainer<'a> {
|
||||
Menu(&'a MudaMenu),
|
||||
Submenu(&'a Submenu),
|
||||
}
|
||||
impl<'a> MenuContainer<'a> {
|
||||
fn items(&self) -> Vec<MenuItemKind> {
|
||||
match self {
|
||||
MenuContainer::Menu(menu) => menu.items(),
|
||||
MenuContainer::Submenu(submenu) => submenu.items(),
|
||||
}
|
||||
}
|
||||
fn remove(&self, item: &dyn IsMenuItem) -> Result<()> {
|
||||
match self {
|
||||
MenuContainer::Menu(menu) => menu.remove(item),
|
||||
MenuContainer::Submenu(submenu) => submenu.remove(item),
|
||||
}
|
||||
}
|
||||
fn append_items(&self, items: &[&dyn IsMenuItem]) -> Result<()> {
|
||||
match self {
|
||||
MenuContainer::Menu(menu) => menu.append_items(items),
|
||||
MenuContainer::Submenu(submenu) => submenu.append_items(items),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a> From<&'a MudaMenu> for MenuContainer<'a> {
|
||||
fn from(menu: &'a MudaMenu) -> Self {
|
||||
MenuContainer::Menu(menu)
|
||||
}
|
||||
}
|
||||
impl<'a> From<&'a Submenu> for MenuContainer<'a> {
|
||||
fn from(submenu: &'a Submenu) -> Self {
|
||||
MenuContainer::Submenu(submenu)
|
||||
}
|
||||
}
|
||||
50
.jjconflict-base-0/desktop/src/window/win.rs
Normal file
50
.jjconflict-base-0/desktop/src/window/win.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use windows::Win32::System::Com::{COINIT_APARTMENTTHREADED, CoInitializeEx};
|
||||
use windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole};
|
||||
use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
|
||||
use windows::core::HSTRING;
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
use winit::window::{Window, WindowAttributes};
|
||||
|
||||
use crate::consts::APP_ID;
|
||||
use crate::event::AppEventScheduler;
|
||||
|
||||
pub(super) struct NativeWindowImpl {
|
||||
native_handle: native_handle::NativeWindowHandle,
|
||||
}
|
||||
|
||||
impl super::NativeWindow for NativeWindowImpl {
|
||||
fn init() {
|
||||
// Attach to parent console if launched from a terminal (no-op otherwise)
|
||||
unsafe {
|
||||
let _ = AttachConsole(ATTACH_PARENT_PROCESS);
|
||||
}
|
||||
|
||||
// Set stable app ID
|
||||
let app_id = HSTRING::from(APP_ID);
|
||||
unsafe {
|
||||
let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok();
|
||||
SetCurrentProcessExplicitAppUserModelID(&app_id).ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn configure(attributes: WindowAttributes, _event_loop: &dyn ActiveEventLoop) -> WindowAttributes {
|
||||
attributes
|
||||
}
|
||||
|
||||
fn new(window: &dyn Window, _app_event_scheduler: AppEventScheduler) -> Self {
|
||||
let native_handle = native_handle::NativeWindowHandle::new(window);
|
||||
NativeWindowImpl { native_handle }
|
||||
}
|
||||
|
||||
fn can_render(&self) -> bool {
|
||||
self.native_handle.can_render()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NativeWindowImpl {
|
||||
fn drop(&mut self) {
|
||||
self.native_handle.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
mod native_handle;
|
||||
410
.jjconflict-base-0/desktop/src/window/win/native_handle.rs
Normal file
410
.jjconflict-base-0/desktop/src/window/win/native_handle.rs
Normal file
@@ -0,0 +1,410 @@
|
||||
//! Implements a Windows-specific custom window frame (no titlebar, but native boarder, shadows and resize).
|
||||
//! Look and feel should be similar to a standard window.
|
||||
//!
|
||||
//! Implementation notes:
|
||||
//! - Windows that don't use standard decorations don't get native resize handles or shadows by default.
|
||||
//! - We implement resize handles (outside the main window) by creating an invisible "helper" window that
|
||||
//! is a little larger than the main window and positioned on top of it. The helper window does hit-testing
|
||||
//! and triggers native resize operations on the main window when the user clicks and drags a resize area.
|
||||
//! - The helper window is a invisible window that never activates, so it doesn't steal focus from the main window.
|
||||
//! - The main window needs to update the helper window's position and size whenever it moves or resizes.
|
||||
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
use std::time::Instant;
|
||||
use wgpu::rwh::{HasWindowHandle, RawWindowHandle};
|
||||
use windows::Win32::Foundation::*;
|
||||
use windows::Win32::Graphics::Dwm::*;
|
||||
use windows::Win32::Graphics::Gdi::*;
|
||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows::Win32::UI::Controls::MARGINS;
|
||||
use windows::Win32::UI::HiDpi::*;
|
||||
use windows::Win32::UI::WindowsAndMessaging::*;
|
||||
use windows::core::PCWSTR;
|
||||
use winit::window::Window;
|
||||
|
||||
#[derive(Default)]
|
||||
struct NativeWindowState {
|
||||
can_render: bool,
|
||||
can_render_since: Option<Instant>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct NativeWindowHandle {
|
||||
main: HWND,
|
||||
helper: HWND,
|
||||
prev_window_message_handler: isize,
|
||||
state: Arc<Mutex<NativeWindowState>>,
|
||||
}
|
||||
impl NativeWindowHandle {
|
||||
pub(super) fn new(window: &dyn Window) -> NativeWindowHandle {
|
||||
// Extract Win32 HWND from winit.
|
||||
let main = match window.window_handle().expect("No window handle").as_raw() {
|
||||
RawWindowHandle::Win32(h) => HWND(h.hwnd.get() as *mut std::ffi::c_void),
|
||||
_ => panic!("Not a Win32 window"),
|
||||
};
|
||||
|
||||
// Register the invisible helper (resize ring) window class.
|
||||
unsafe { ensure_helper_class() };
|
||||
|
||||
// Create the helper as a popup tool window that never activates.
|
||||
// WS_EX_NOACTIVATE keeps focus on the main window; WS_EX_TOOLWINDOW hides it from Alt+Tab.
|
||||
// https://learn.microsoft.com/windows/win32/winmsg/extended-window-styles
|
||||
let ex = WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW;
|
||||
let style = WS_POPUP;
|
||||
let helper = unsafe {
|
||||
CreateWindowExW(
|
||||
ex,
|
||||
PCWSTR(HELPER_CLASS_NAME.encode_utf16().collect::<Vec<_>>().as_ptr()),
|
||||
PCWSTR::null(),
|
||||
style,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
Some(main),
|
||||
None,
|
||||
None,
|
||||
// Pass the main window's HWND to WM_NCCREATE so the helper can store it.
|
||||
Some(&main as *const _ as _),
|
||||
)
|
||||
}
|
||||
.expect("CreateWindowExW failed");
|
||||
|
||||
// Subclass the main window.
|
||||
// https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-setwindowlongptra
|
||||
let prev_window_message_handler = unsafe { SetWindowLongPtrW(main, GWLP_WNDPROC, main_window_handle_message as *const () as isize) };
|
||||
if prev_window_message_handler == 0 {
|
||||
let _ = unsafe { DestroyWindow(helper) };
|
||||
panic!("SetWindowLongPtrW failed");
|
||||
}
|
||||
|
||||
let native_handle = NativeWindowHandle {
|
||||
main,
|
||||
helper,
|
||||
prev_window_message_handler,
|
||||
state: Arc::new(Mutex::new(NativeWindowState::default())),
|
||||
};
|
||||
registry::insert(&native_handle);
|
||||
|
||||
// Place the helper over the main window and show it without activation.
|
||||
unsafe { position_helper(main, helper) };
|
||||
let _ = unsafe { ShowWindow(helper, SW_SHOWNOACTIVATE) };
|
||||
|
||||
// DwmExtendFrameIntoClientArea is needed to keep native window frame (but no titlebar).
|
||||
// https://learn.microsoft.com/windows/win32/api/dwmapi/nf-dwmapi-dwmextendframeintoclientarea
|
||||
// https://learn.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
|
||||
let mut boarder_size: u32 = 1;
|
||||
let _ = unsafe { DwmGetWindowAttribute(main, DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, &mut boarder_size as *mut _ as *mut _, size_of::<u32>() as u32) };
|
||||
let margins = MARGINS {
|
||||
cxLeftWidth: 0,
|
||||
cxRightWidth: 0,
|
||||
cyBottomHeight: 0,
|
||||
cyTopHeight: boarder_size as i32,
|
||||
};
|
||||
let _ = unsafe { DwmExtendFrameIntoClientArea(main, &margins) };
|
||||
|
||||
let hinst: HINSTANCE = unsafe { GetModuleHandleW(None) }.unwrap().into();
|
||||
|
||||
// Set taskbar icon
|
||||
if let Ok(big) = unsafe {
|
||||
LoadImageW(
|
||||
Some(hinst),
|
||||
PCWSTR(1usize as *const u16),
|
||||
IMAGE_ICON,
|
||||
GetSystemMetrics(SM_CXICON),
|
||||
GetSystemMetrics(SM_CYICON),
|
||||
LR_SHARED,
|
||||
)
|
||||
} {
|
||||
unsafe { SetClassLongPtrW(main, GCLP_HICON, big.0 as isize) };
|
||||
unsafe { SendMessageW(main, WM_SETICON, Some(WPARAM(ICON_BIG as usize)), Some(LPARAM(big.0 as isize))) };
|
||||
}
|
||||
|
||||
// Set window icon
|
||||
if let Ok(small) = unsafe {
|
||||
LoadImageW(
|
||||
Some(hinst),
|
||||
PCWSTR(1usize as *const u16),
|
||||
IMAGE_ICON,
|
||||
GetSystemMetrics(SM_CXSMICON),
|
||||
GetSystemMetrics(SM_CYSMICON),
|
||||
LR_SHARED,
|
||||
)
|
||||
} {
|
||||
unsafe { SetClassLongPtrW(main, GCLP_HICONSM, small.0 as isize) };
|
||||
unsafe { SendMessageW(main, WM_SETICON, Some(WPARAM(ICON_SMALL as usize)), Some(LPARAM(small.0 as isize))) };
|
||||
}
|
||||
|
||||
// Force window update
|
||||
let _ = unsafe { SetWindowPos(main, None, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE) };
|
||||
|
||||
native_handle
|
||||
}
|
||||
|
||||
pub(super) fn destroy(&self) {
|
||||
registry::remove_by_main(self.main);
|
||||
|
||||
// Undo subclassing and destroy the helper window.
|
||||
let _ = unsafe { SetWindowLongPtrW(self.main, GWLP_WNDPROC, self.prev_window_message_handler) };
|
||||
if !self.helper.is_invalid() {
|
||||
let _ = unsafe { DestroyWindow(self.helper) };
|
||||
}
|
||||
}
|
||||
|
||||
// Rendering should be disabled when window is minimized
|
||||
// Rendering also needs to be disabled during minimize and restore animations
|
||||
// Reenabling rendering is done after a small delay to account for restore animation
|
||||
// TODO: Find a cleaner solution that doesn't depend on a timeout
|
||||
pub(super) fn can_render(&self) -> bool {
|
||||
let can_render = !unsafe { IsIconic(self.main).into() } && unsafe { IsWindowVisible(self.main).into() };
|
||||
let Ok(mut state) = self.state.lock() else {
|
||||
tracing::error!("Failed to lock NativeWindowState");
|
||||
return true;
|
||||
};
|
||||
match (can_render, state.can_render, state.can_render_since) {
|
||||
(true, false, None) => {
|
||||
state.can_render_since = Some(Instant::now());
|
||||
}
|
||||
(true, false, Some(can_render_since)) if can_render_since.elapsed().as_millis() > 50 => {
|
||||
state.can_render = true;
|
||||
state.can_render_since = None;
|
||||
}
|
||||
(false, true, _) => {
|
||||
state.can_render = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
state.can_render
|
||||
}
|
||||
}
|
||||
|
||||
mod registry {
|
||||
use std::cell::RefCell;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
|
||||
use super::NativeWindowHandle;
|
||||
|
||||
thread_local! {
|
||||
static STORE: RefCell<Vec<NativeWindowHandle>> = RefCell::new(Vec::new());
|
||||
}
|
||||
|
||||
pub(super) fn find_by_main(main: HWND) -> Option<NativeWindowHandle> {
|
||||
STORE.with_borrow(|vec| vec.iter().find(|h| h.main == main).cloned())
|
||||
}
|
||||
pub(super) fn remove_by_main(main: HWND) {
|
||||
STORE.with_borrow_mut(|vec| {
|
||||
vec.retain(|h| h.main != main);
|
||||
});
|
||||
}
|
||||
pub(super) fn insert(handle: &NativeWindowHandle) {
|
||||
STORE.with_borrow_mut(|vec| {
|
||||
vec.push(handle.clone());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const HELPER_CLASS_NAME: &str = "Helper\0";
|
||||
|
||||
static HELPER_CLASS_LOCK: OnceLock<u16> = OnceLock::new();
|
||||
unsafe fn ensure_helper_class() {
|
||||
// Register a window class for the invisible resize helper.
|
||||
let _ = *HELPER_CLASS_LOCK.get_or_init(|| {
|
||||
let class_name: Vec<u16> = HELPER_CLASS_NAME.encode_utf16().collect();
|
||||
let wc = WNDCLASSW {
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(helper_window_handle_message),
|
||||
hInstance: unsafe { GetModuleHandleW(None).unwrap().into() },
|
||||
hIcon: HICON::default(),
|
||||
hCursor: unsafe { LoadCursorW(None, IDC_ARROW).unwrap() },
|
||||
// No painting; the ring is invisible.
|
||||
hbrBackground: HBRUSH::default(),
|
||||
lpszClassName: PCWSTR(class_name.as_ptr()),
|
||||
..Default::default()
|
||||
};
|
||||
unsafe { RegisterClassW(&wc) }
|
||||
});
|
||||
}
|
||||
|
||||
// Main window message handler, called on the UI thread for every message the main window receives.
|
||||
unsafe extern "system" fn main_window_handle_message(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||
if msg == WM_NCCALCSIZE && wparam.0 != 0 {
|
||||
let params = unsafe { &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS) };
|
||||
|
||||
// When maximized, shrink to visible frame so content doesn't extend beyond it.
|
||||
if unsafe { IsZoomed(hwnd).as_bool() } && !is_effectively_fullscreen(params.rgrc[0]) {
|
||||
let dpi = unsafe { GetDpiForWindow(hwnd) };
|
||||
let size = unsafe { GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) };
|
||||
let pad = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) };
|
||||
let inset = (size + pad) as i32;
|
||||
|
||||
params.rgrc[0].left += inset;
|
||||
params.rgrc[0].top += inset;
|
||||
params.rgrc[0].right -= inset;
|
||||
params.rgrc[0].bottom -= inset;
|
||||
}
|
||||
|
||||
// Return 0 to to tell Windows to skip the default non-client area calculation and drawing.
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
let Some(handle) = registry::find_by_main(hwnd) else {
|
||||
return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
|
||||
};
|
||||
|
||||
match msg {
|
||||
// Keep the invisible resize helper in sync with moves/resizes/visibility.
|
||||
WM_MOVE | WM_MOVING | WM_SIZE | WM_SIZING | WM_WINDOWPOSCHANGED | WM_SHOWWINDOW => {
|
||||
if msg == WM_SHOWWINDOW {
|
||||
if wparam.0 == 0 {
|
||||
let _ = unsafe { ShowWindow(handle.helper, SW_HIDE) };
|
||||
} else {
|
||||
let _ = unsafe { ShowWindow(handle.helper, SW_SHOWNOACTIVATE) };
|
||||
}
|
||||
}
|
||||
unsafe { position_helper(hwnd, handle.helper) };
|
||||
}
|
||||
|
||||
// If the main window is destroyed, destroy the helper too.
|
||||
// Should only be needed if windows forcefully destroys the main window.
|
||||
WM_DESTROY => {
|
||||
let _ = unsafe { DestroyWindow(handle.helper) };
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Ensure the previous window message handler is not null.
|
||||
assert_ne!(handle.prev_window_message_handler, 0);
|
||||
|
||||
// Call the previous window message handler, this is a standard subclassing pattern.
|
||||
let prev_window_message_handler_fn_ptr: *const () = std::ptr::without_provenance(handle.prev_window_message_handler as usize);
|
||||
let prev_window_message_handler_fn = unsafe { std::mem::transmute::<_, _>(prev_window_message_handler_fn_ptr) };
|
||||
unsafe { CallWindowProcW(Some(prev_window_message_handler_fn), hwnd, msg, wparam, lparam) }
|
||||
}
|
||||
|
||||
// Helper window message handler, called on the UI thread for every message the helper window receives.
|
||||
unsafe extern "system" fn helper_window_handle_message(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||
match msg {
|
||||
// Helper window creation, should be the first message that the helper window receives.
|
||||
WM_NCCREATE => {
|
||||
// Main window HWND is provided when creating the helper window with `CreateWindowExW`
|
||||
// Save main window HWND in GWLP_USERDATA so we can extract it later
|
||||
let crate_struct = lparam.0 as *const CREATESTRUCTW;
|
||||
let create_param = unsafe { (*crate_struct).lpCreateParams as *const HWND };
|
||||
unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, (*create_param).0 as isize) };
|
||||
return LRESULT(1);
|
||||
}
|
||||
|
||||
// Invisible; no background erase.
|
||||
WM_ERASEBKGND => return LRESULT(1),
|
||||
|
||||
// Tell windows what resize areas we are hitting, this is used to decide what cursor to show.
|
||||
WM_NCHITTEST => {
|
||||
let ht = unsafe { calculate_hit(hwnd, lparam) };
|
||||
return LRESULT(ht as isize);
|
||||
}
|
||||
|
||||
// This starts the system's resize loop for the main window if a resize area is hit.
|
||||
// Helper window button down translates to SC_SIZE | WMSZ_* on the main window.
|
||||
WM_NCLBUTTONDOWN | WM_NCRBUTTONDOWN | WM_NCMBUTTONDOWN => {
|
||||
// Extract the main window's HWND from GWLP_USERDATA that we saved earlier.
|
||||
let main_ptr = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) } as *mut std::ffi::c_void;
|
||||
let main = HWND(main_ptr);
|
||||
if unsafe { IsWindow(Some(main)).as_bool() } {
|
||||
let Some(wmsz) = (unsafe { calculate_resize_direction(hwnd, lparam) }) else {
|
||||
return LRESULT(0);
|
||||
};
|
||||
|
||||
// Ensure that the main window can receive WM_SYSCOMMAND.
|
||||
let _ = unsafe { SetForegroundWindow(main) };
|
||||
|
||||
// Start sizing on the main window in the calculated direction. (SC_SIZE + WMSZ_*)
|
||||
let _ = unsafe { PostMessageW(Some(main), WM_SYSCOMMAND, WPARAM((SC_SIZE + wmsz) as usize), lparam) };
|
||||
}
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
// Never activate the helper window, allows all inputs that don't hit the resize areas to pass through.
|
||||
WM_MOUSEACTIVATE => return LRESULT(MA_NOACTIVATE as isize),
|
||||
_ => {}
|
||||
}
|
||||
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
|
||||
}
|
||||
|
||||
const RESIZE_BAND_THICKNESS: i32 = 8;
|
||||
|
||||
// Position the helper window to match the main window's location and size (plus the resize band size).
|
||||
unsafe fn position_helper(main: HWND, helper: HWND) {
|
||||
let mut r = RECT::default();
|
||||
let _ = unsafe { GetWindowRect(main, &mut r) };
|
||||
|
||||
let x = r.left - RESIZE_BAND_THICKNESS;
|
||||
let y = r.top - RESIZE_BAND_THICKNESS;
|
||||
let w = (r.right - r.left) + RESIZE_BAND_THICKNESS * 2;
|
||||
let h = (r.bottom - r.top) + RESIZE_BAND_THICKNESS * 2;
|
||||
|
||||
let _ = unsafe { SetWindowPos(helper, Some(main), x, y, w, h, SWP_NOACTIVATE | SWP_NOSENDCHANGING) };
|
||||
}
|
||||
|
||||
unsafe fn calculate_hit(helper: HWND, lparam: LPARAM) -> u32 {
|
||||
let x = (lparam.0 & 0xFFFF) as i16 as i32;
|
||||
let y = ((lparam.0 >> 16) & 0xFFFF) as i16 as i32;
|
||||
|
||||
let mut r = RECT::default();
|
||||
let _ = unsafe { GetWindowRect(helper, &mut r) };
|
||||
|
||||
let on_top = y < (r.top + RESIZE_BAND_THICKNESS) as i32;
|
||||
let on_right = x >= (r.right - RESIZE_BAND_THICKNESS) as i32;
|
||||
let on_bottom = y >= (r.bottom - RESIZE_BAND_THICKNESS) as i32;
|
||||
let on_left = x < (r.left + RESIZE_BAND_THICKNESS) as i32;
|
||||
|
||||
match (on_top, on_right, on_bottom, on_left) {
|
||||
(true, _, _, true) => HTTOPLEFT,
|
||||
(true, true, _, _) => HTTOPRIGHT,
|
||||
(_, true, true, _) => HTBOTTOMRIGHT,
|
||||
(_, _, true, true) => HTBOTTOMLEFT,
|
||||
(true, _, _, _) => HTTOP,
|
||||
(_, true, _, _) => HTRIGHT,
|
||||
(_, _, true, _) => HTBOTTOM,
|
||||
(_, _, _, true) => HTLEFT,
|
||||
_ => HTTRANSPARENT as u32,
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn calculate_resize_direction(helper: HWND, lparam: LPARAM) -> Option<u32> {
|
||||
match unsafe { calculate_hit(helper, lparam) } {
|
||||
HTLEFT => Some(WMSZ_LEFT),
|
||||
HTRIGHT => Some(WMSZ_RIGHT),
|
||||
HTTOP => Some(WMSZ_TOP),
|
||||
HTBOTTOM => Some(WMSZ_BOTTOM),
|
||||
HTTOPLEFT => Some(WMSZ_TOPLEFT),
|
||||
HTTOPRIGHT => Some(WMSZ_TOPRIGHT),
|
||||
HTBOTTOMLEFT => Some(WMSZ_BOTTOMLEFT),
|
||||
HTBOTTOMRIGHT => Some(WMSZ_BOTTOMRIGHT),
|
||||
_ => 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
|
||||
}
|
||||
53
.jjconflict-base-0/desktop/ui/Cargo.toml
Normal file
53
.jjconflict-base-0/desktop/ui/Cargo.toml
Normal file
@@ -0,0 +1,53 @@
|
||||
[package]
|
||||
name = "graphite-desktop-ui"
|
||||
description = "Renders the Graphite editor frontend UI into wgpu textures"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
embedded_resources = ["dep:graphite-desktop-embedded-resources"]
|
||||
accelerated_paint = ["dep:ash", "dep:bytemuck", "dep:objc2-io-surface", "dep:objc2-metal", "dep:mach2"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
graphite-desktop-embedded-resources = { path = "../embedded-resources", optional = true }
|
||||
|
||||
wgpu = { workspace = true }
|
||||
wgpu-sync = { workspace = true }
|
||||
winit = { workspace = true, features = ["serde"] }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
rand = { workspace = true, features = ["thread_rng"] }
|
||||
cef = { workspace = true }
|
||||
bytemuck = { workspace = true, optional = true }
|
||||
ipc-channel = "0.22"
|
||||
|
||||
# Linux-specific dependencies
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc = "0.2"
|
||||
ash = { version = "0.38", optional = true }
|
||||
|
||||
# Windows-specific dependencies
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.62.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Direct3D12",
|
||||
"Win32_Security",
|
||||
"Win32_System_JobObjects",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
# Mac-specific dependencies
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
libc = "0.2"
|
||||
mach2 = { version = "0.4", optional = true }
|
||||
objc2 = { version = "0.6.1", default-features = false }
|
||||
objc2-foundation = { version = "0.3.2", default-features = false }
|
||||
objc2-app-kit = { version = "0.3.2", default-features = false }
|
||||
objc2-io-surface = { version = "0.3.2", optional = true }
|
||||
objc2-metal = { version = "0.3", features = ["objc2-io-surface"], optional = true }
|
||||
37
.jjconflict-base-0/desktop/ui/src/consts.rs
Normal file
37
.jjconflict-base-0/desktop/ui/src/consts.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) const RESOURCE_SCHEME: &str = "resources";
|
||||
pub(crate) const RESOURCE_DOMAIN: &str = "resources";
|
||||
|
||||
pub(crate) const BROWSER_HOST_CONFIG_FLAG: &str = "--graphite-browser-host=";
|
||||
|
||||
pub(crate) const WINDOWLESS_FRAME_RATE: i32 = 60;
|
||||
pub(crate) const FRAMES_IN_FLIGHT_LIMIT: u64 = 3;
|
||||
pub(crate) const FRAME_SEGMENT_POOL_SIZE: u64 = FRAMES_IN_FLIGHT_LIMIT + 1; // allow one extra staged frame
|
||||
pub(crate) const FRAME_SEGMENT_GRANULARITY: usize = 2 * 1024 * 1024; // 2 MiB
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) const FRAME_ACK_TIMEOUT: Duration = Duration::from_millis(250);
|
||||
|
||||
pub(crate) const HOST_HELLO_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
pub(crate) const HOST_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) const IPC_BOOTSTRAP_PREFIX: &str = "art.graphite.Graphite.ipc.";
|
||||
|
||||
pub(crate) const SCROLL_LINE_HEIGHT: usize = 40;
|
||||
pub(crate) const SCROLL_LINE_WIDTH: usize = 40;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) const SCROLL_SPEED_X: f32 = 3.;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) const SCROLL_SPEED_Y: f32 = 3.;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) const SCROLL_SPEED_X: f32 = 1.;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) const SCROLL_SPEED_Y: f32 = 1.;
|
||||
|
||||
pub(crate) const PINCH_ZOOM_SPEED: f64 = 300.;
|
||||
|
||||
pub(crate) const MULTICLICK_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
pub(crate) const MULTICLICK_ALLOWED_TRAVEL: usize = 4;
|
||||
350
.jjconflict-base-0/desktop/ui/src/context.rs
Normal file
350
.jjconflict-base-0/desktop/ui/src/context.rs
Normal file
@@ -0,0 +1,350 @@
|
||||
use cef::args::Args;
|
||||
use cef::sys::{CEF_API_VERSION_LAST, cef_log_severity_t, cef_thread_id_t};
|
||||
use cef::{
|
||||
App, Browser, BrowserSettings, CefString, Client, DictionaryValue, ImplBrowser, ImplBrowserHost, ImplCommandLine, ImplRequestContext, LogSeverity, RequestContextSettings, SchemeHandlerFactory,
|
||||
Settings, Task, ThreadId, WindowInfo, api_hash, browser_host_create_browser_sync, execute_process, post_task,
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
use std::marker::PhantomData;
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME, WINDOWLESS_FRAME_RATE};
|
||||
use crate::delegate::BrowserDelegate;
|
||||
use crate::dirs::TempDir;
|
||||
use crate::frames::FrameStreamer;
|
||||
use crate::input::{self, InputEvent};
|
||||
use crate::internal::task::ClosureTask;
|
||||
use crate::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderProcessAppImpl, SchemeHandlerFactoryImpl};
|
||||
use crate::ipc::{MessageType, SendMessage};
|
||||
use crate::view::ViewInfoUpdate;
|
||||
|
||||
thread_local! {
|
||||
static CONTEXT: RefCell<Option<BrowserContext>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
pub(crate) struct CefContext {
|
||||
_not_send: PhantomData<*const ()>, // impl !Send for CefContext
|
||||
}
|
||||
|
||||
impl CefContext {
|
||||
pub(crate) fn create(delegate: BrowserDelegate, frames: FrameStreamer, view_info_sender: Sender<ViewInfoUpdate>, accelerated_paint: bool) -> Result<Self, InitError> {
|
||||
let args = bootstrap(false);
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::platform::mac::install_application();
|
||||
|
||||
let instance_dir = TempDir::new().map_err(|e| InitError::InstanceDirectoryCreationFailed(e.to_string()))?;
|
||||
initialize(&args, instance_dir.as_ref(), accelerated_paint)?;
|
||||
|
||||
let (created_tx, created_rx) = std::sync::mpsc::channel();
|
||||
let install_browser = move || {
|
||||
let result = create_browser(delegate, frames, view_info_sender, instance_dir, accelerated_paint).map(|context| CONTEXT.with(|b| *b.borrow_mut() = Some(context)));
|
||||
let _ = created_tx.send(result);
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
install_browser();
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
run_on_ui_thread(install_browser);
|
||||
|
||||
created_rx.recv().unwrap_or(Err(InitError::BrowserCreationFailed))?;
|
||||
Ok(Self { _not_send: PhantomData })
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub(crate) fn run<R: Send + 'static>(self, control: impl FnOnce(CefContextHandle) -> R + Send + 'static) -> R {
|
||||
let result = control(CefContextHandle);
|
||||
let (dropped_sender, dropped_receiver) = std::sync::mpsc::channel();
|
||||
run_on_ui_thread(move || {
|
||||
drop(CONTEXT.take());
|
||||
let _ = dropped_sender.send(());
|
||||
});
|
||||
let _ = dropped_receiver.recv();
|
||||
cef::shutdown();
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) fn run<R: Send + 'static>(self, control: impl FnOnce(CefContextHandle) -> R + Send + 'static) -> R {
|
||||
let (result_sender, result_receiver) = std::sync::mpsc::channel();
|
||||
let control_thread = std::thread::Builder::new()
|
||||
.name("cef-host-control".to_string())
|
||||
.spawn(move || {
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| control(CefContextHandle)));
|
||||
with_context(|context| {
|
||||
if let Some(host) = context.browser.host() {
|
||||
host.close_browser(1);
|
||||
}
|
||||
});
|
||||
run_on_ui_thread(cef::quit_message_loop);
|
||||
match result {
|
||||
Ok(result) => {
|
||||
let _ = result_sender.send(result);
|
||||
}
|
||||
Err(panic) => std::panic::resume_unwind(panic),
|
||||
}
|
||||
})
|
||||
.expect("Failed to spawn the CEF control thread");
|
||||
cef::run_message_loop();
|
||||
drop(CONTEXT.take());
|
||||
cef::shutdown();
|
||||
if let Err(panic) = control_thread.join() {
|
||||
std::panic::resume_unwind(panic);
|
||||
}
|
||||
result_receiver.recv().expect("The CEF control thread ended without a result")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn execute_helper_process() -> std::process::ExitCode {
|
||||
let args = bootstrap(true);
|
||||
assert_eq!(args.as_cmd_line().unwrap().has_switch(Some(&"type".into())), 1, "Not a CEF helper process");
|
||||
let mut app = RenderProcessAppImpl::app();
|
||||
let code = execute_process(Some(args.as_main_args()), Some(&mut app), std::ptr::null_mut());
|
||||
std::process::ExitCode::from(code as u8)
|
||||
}
|
||||
|
||||
fn bootstrap(helper: bool) -> Args {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let loader = cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), helper);
|
||||
assert!(loader.load());
|
||||
// LibraryLoader unloads the framework on drop
|
||||
std::mem::forget(loader);
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let _ = helper;
|
||||
|
||||
let _ = api_hash(CEF_API_VERSION_LAST, 0);
|
||||
Args::new()
|
||||
}
|
||||
|
||||
fn initialize(args: &Args, instance_dir: &Path, accelerated_paint: bool) -> Result<(), InitError> {
|
||||
let mut app = App::new(BrowserProcessAppImpl::new(accelerated_paint));
|
||||
if cef::initialize(Some(args.as_main_args()), Some(&platform_settings(instance_dir)?), Some(&mut app), std::ptr::null_mut()) != 1 {
|
||||
return Err(InitError::InitializationFailureCode(cef::get_exit_code() as u32));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn platform_settings(instance_dir: &Path) -> Result<Settings, InitError> {
|
||||
let log_severity = match std::env::var("GRAPHITE_BROWSER_LOG").unwrap_or_default().to_lowercase().as_str() {
|
||||
"debug" => cef_log_severity_t::LOGSEVERITY_VERBOSE,
|
||||
"info" => cef_log_severity_t::LOGSEVERITY_INFO,
|
||||
"warn" => cef_log_severity_t::LOGSEVERITY_WARNING,
|
||||
"error" => cef_log_severity_t::LOGSEVERITY_ERROR,
|
||||
"none" => cef_log_severity_t::LOGSEVERITY_DISABLE,
|
||||
_ => cef_log_severity_t::LOGSEVERITY_FATAL,
|
||||
};
|
||||
|
||||
let Some(root_cache_path) = instance_dir.to_str().map(CefString::from) else {
|
||||
return Err(InitError::PathResolutionFailed(format!("non-UTF-8 instance directory path: {}", instance_dir.display())));
|
||||
};
|
||||
let base = Settings {
|
||||
windowless_rendering_enabled: 1,
|
||||
root_cache_path,
|
||||
cache_path: "".into(),
|
||||
disable_signal_handlers: 1,
|
||||
log_severity: LogSeverity::from(log_severity),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let exe = std::env::current_exe().map_err(|e| InitError::PathResolutionFailed(format!("cannot get current exe path: {e}")))?;
|
||||
let app_root = exe
|
||||
.parent()
|
||||
.and_then(|p| p.parent())
|
||||
.and_then(|p| p.parent())
|
||||
.ok_or_else(|| InitError::PathResolutionFailed(format!("executable is not inside an app bundle: {}", exe.display())))?;
|
||||
let Some(main_bundle_path) = app_root.to_str().map(CefString::from) else {
|
||||
return Err(InitError::PathResolutionFailed(format!("invalid app bundle path: {}", app_root.display())));
|
||||
};
|
||||
Ok(Settings {
|
||||
main_bundle_path,
|
||||
multi_threaded_message_loop: 0,
|
||||
external_message_pump: 0,
|
||||
no_sandbox: 1, // GPU helper crashes when running with sandbox
|
||||
..base
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
Ok(Settings {
|
||||
multi_threaded_message_loop: 1,
|
||||
#[cfg(target_os = "linux")]
|
||||
no_sandbox: 1,
|
||||
..base
|
||||
})
|
||||
}
|
||||
|
||||
fn create_browser(delegate: BrowserDelegate, frames: FrameStreamer, view_info_sender: Sender<ViewInfoUpdate>, instance_dir: TempDir, accelerated_paint: bool) -> Result<BrowserContext, InitError> {
|
||||
#[cfg(not(feature = "accelerated_paint"))]
|
||||
let _ = accelerated_paint;
|
||||
let mut client = Client::new(BrowserProcessClientImpl::new(&delegate, frames));
|
||||
|
||||
let window_info = WindowInfo {
|
||||
windowless_rendering_enabled: 1,
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
shared_texture_enabled: accelerated_paint as i32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let settings = BrowserSettings {
|
||||
windowless_frame_rate: WINDOWLESS_FRAME_RATE,
|
||||
background_color: 0x0,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let Some(mut incognito_request_context) = cef::request_context_create_context(
|
||||
Some(&RequestContextSettings {
|
||||
persist_session_cookies: 0,
|
||||
cache_path: "".into(),
|
||||
..Default::default()
|
||||
}),
|
||||
Option::<&mut cef::RequestContextHandler>::None,
|
||||
) else {
|
||||
return Err(InitError::RequestContextCreationFailed);
|
||||
};
|
||||
|
||||
let mut scheme_handler_factory = SchemeHandlerFactory::new(SchemeHandlerFactoryImpl::new(delegate.clone()));
|
||||
incognito_request_context.clear_scheme_handler_factories();
|
||||
if incognito_request_context.register_scheme_handler_factory(Some(&RESOURCE_SCHEME.into()), Some(&RESOURCE_DOMAIN.into()), Some(&mut scheme_handler_factory)) != 1 {
|
||||
return Err(InitError::SchemeHandlerRegistrationFailed);
|
||||
}
|
||||
|
||||
let url = format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/");
|
||||
browser_host_create_browser_sync(
|
||||
Some(&window_info),
|
||||
Some(&mut client),
|
||||
Some(&url.as_str().into()),
|
||||
Some(&settings),
|
||||
Option::<&mut DictionaryValue>::None,
|
||||
Some(&mut incognito_request_context),
|
||||
)
|
||||
.map(|browser| BrowserContext {
|
||||
delegate,
|
||||
browser,
|
||||
view_info_sender,
|
||||
_instance_dir: instance_dir,
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
tracing::error!("Failed to create browser");
|
||||
InitError::BrowserCreationFailed
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) enum InitError {
|
||||
#[error("Failed to create the instance directory: {0}")]
|
||||
InstanceDirectoryCreationFailed(String),
|
||||
#[error("Initialization failed with code: {0}")]
|
||||
InitializationFailureCode(u32),
|
||||
#[error("Browser creation failed")]
|
||||
BrowserCreationFailed,
|
||||
#[error("Request context creation failed")]
|
||||
RequestContextCreationFailed,
|
||||
#[error("Failed to resolve a required path: {0}")]
|
||||
PathResolutionFailed(String),
|
||||
#[error("Scheme handler registration failed")]
|
||||
SchemeHandlerRegistrationFailed,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CefContextHandle;
|
||||
|
||||
impl CefContextHandle {
|
||||
pub(crate) fn apply_input(&self, events: Vec<InputEvent>) {
|
||||
with_context(move |context| {
|
||||
for event in &events {
|
||||
input::apply(&context.browser, event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn update_view_info(&self, update: ViewInfoUpdate) {
|
||||
with_context(move |context| context.update_view_info(update));
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_view_info(&self) {
|
||||
with_context(|context| context.refresh_view_info());
|
||||
}
|
||||
|
||||
pub(crate) fn send_web_message(&self, message: Vec<u8>) {
|
||||
with_context(move |context| context.send_web_message(message));
|
||||
}
|
||||
}
|
||||
|
||||
struct BrowserContext {
|
||||
delegate: BrowserDelegate,
|
||||
browser: Browser,
|
||||
view_info_sender: Sender<ViewInfoUpdate>,
|
||||
_instance_dir: TempDir,
|
||||
}
|
||||
|
||||
impl BrowserContext {
|
||||
fn update_view_info(&self, update: ViewInfoUpdate) {
|
||||
let _ = self.view_info_sender.send(update);
|
||||
}
|
||||
|
||||
fn refresh_view_info(&self) {
|
||||
let view_info = self.delegate.view_info();
|
||||
let Some(host) = self.browser.host() else {
|
||||
tracing::error!("Browser host is not available, cannot refresh view info");
|
||||
return;
|
||||
};
|
||||
host.set_zoom_level(view_info.zoom());
|
||||
host.was_resized();
|
||||
|
||||
// Fix for CEF not updating the view after resize
|
||||
// TODO: remove once https://github.com/chromiumembedded/cef/issues/3822 is fixed
|
||||
host.invalidate(cef::PaintElementType::default());
|
||||
}
|
||||
|
||||
fn send_web_message(&self, message: Vec<u8>) {
|
||||
self.send_message(MessageType::SendToJS, &message);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BrowserContext {
|
||||
fn drop(&mut self) {
|
||||
tracing::debug!("Shutting down CEF");
|
||||
if let Some(host) = self.browser.host() {
|
||||
host.close_browser(1);
|
||||
} else {
|
||||
tracing::error!("Browser host is not available, cannot close browser");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SendMessage for BrowserContext {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||
let Some(frame) = self.browser.main_frame() else {
|
||||
tracing::error!("Main frame is not available, cannot send message");
|
||||
return;
|
||||
};
|
||||
frame.send_message(message_type, message);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_on_ui_thread<F>(closure: F)
|
||||
where
|
||||
F: FnOnce() + Send + 'static,
|
||||
{
|
||||
let closure_task = ClosureTask::new(closure);
|
||||
let mut task = Task::new(closure_task);
|
||||
if post_task(ThreadId::from(cef_thread_id_t::TID_UI), Some(&mut task)) != 1 {
|
||||
tracing::error!("Failed to post a task to the CEF UI thread");
|
||||
}
|
||||
}
|
||||
|
||||
fn with_context<F>(closure: F)
|
||||
where
|
||||
F: FnOnce(&mut BrowserContext) + Send + 'static,
|
||||
{
|
||||
run_on_ui_thread(move || {
|
||||
CONTEXT.with(|b| {
|
||||
if let Some(context) = b.borrow_mut().as_mut() {
|
||||
closure(context);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
59
.jjconflict-base-0/desktop/ui/src/delegate.rs
Normal file
59
.jjconflict-base-0/desktop/ui/src/delegate.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::remote::messages::EventMessage;
|
||||
use super::view::{ViewInfo, ViewInfoReceiver, ViewInfoUpdate};
|
||||
use crate::Cursor;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct BrowserDelegate(Arc<Inner>);
|
||||
|
||||
struct Inner {
|
||||
sender: Arc<Mutex<IpcSender<EventMessage>>>,
|
||||
view_info: Mutex<ViewInfoReceiver>,
|
||||
}
|
||||
|
||||
impl BrowserDelegate {
|
||||
pub(crate) fn new(sender: Arc<Mutex<IpcSender<EventMessage>>>, view_info_receiver: Receiver<ViewInfoUpdate>) -> Self {
|
||||
Self(Arc::new(Inner {
|
||||
sender,
|
||||
view_info: Mutex::new(ViewInfoReceiver::new(view_info_receiver)),
|
||||
}))
|
||||
}
|
||||
|
||||
fn send(&self, message: EventMessage) {
|
||||
let Ok(sender) = self.0.sender.lock() else {
|
||||
tracing::error!("Failed to lock host message sender");
|
||||
return;
|
||||
};
|
||||
if let Err(e) = sender.send(message) {
|
||||
tracing::debug!("Failed to send message to main process: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn view_info(&self) -> ViewInfo {
|
||||
let Ok(mut guard) = self.0.view_info.lock() else {
|
||||
tracing::error!("Failed to lock the view info mirror");
|
||||
return ViewInfo::new();
|
||||
};
|
||||
guard.current()
|
||||
}
|
||||
|
||||
pub(crate) fn load_resource(&self, path: PathBuf) -> Option<crate::resources::Resource> {
|
||||
crate::resources::load(path)
|
||||
}
|
||||
|
||||
pub(crate) fn cursor_change(&self, cursor: Cursor) {
|
||||
self.send(EventMessage::CursorChange(cursor));
|
||||
}
|
||||
|
||||
pub(crate) fn initialized_web_communication(&self) {
|
||||
self.send(EventMessage::WebCommunicationInitialized);
|
||||
}
|
||||
|
||||
pub(crate) fn receive_web_message(&self, message: &[u8]) {
|
||||
self.send(EventMessage::WebMessage(message.to_vec()));
|
||||
}
|
||||
}
|
||||
50
.jjconflict-base-0/desktop/ui/src/dirs.rs
Normal file
50
.jjconflict-base-0/desktop/ui/src/dirs.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const APP_DIRECTORY_NAME: &str = "graphite";
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
const APP_DIRECTORY_NAME: &str = "Graphite";
|
||||
|
||||
pub(crate) fn app_tmp_dir() -> PathBuf {
|
||||
let path = std::env::temp_dir().join(APP_DIRECTORY_NAME);
|
||||
if let Err(e) = fs::create_dir_all(&path) {
|
||||
tracing::error!("Failed to create temp directory at {path:?}: {e}");
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
/// Temporary directory that is automatically deleted when dropped.
|
||||
pub struct TempDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TempDir {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
Self::new_with_parent(app_tmp_dir())
|
||||
}
|
||||
|
||||
pub fn new_with_parent(parent: impl AsRef<Path>) -> io::Result<Self> {
|
||||
let random_suffix = format!("{:032x}", rand::random::<u128>());
|
||||
let name = format!("{}_{}", std::process::id(), random_suffix);
|
||||
let path = parent.as_ref().join(name);
|
||||
fs::create_dir_all(&path)?;
|
||||
Ok(Self { path })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
let result = fs::remove_dir_all(&self.path);
|
||||
if let Err(e) = result {
|
||||
tracing::error!("Failed to remove temporary directory at {:?}: {}", self.path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for TempDir {
|
||||
fn as_ref(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
41
.jjconflict-base-0/desktop/ui/src/events.rs
Normal file
41
.jjconflict-base-0/desktop/ui/src/events.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
use crate::UiEvent;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct EventQueue {
|
||||
sender: std::sync::mpsc::Sender<UiEvent>,
|
||||
terminated: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl EventQueue {
|
||||
pub(crate) fn new() -> (Self, Receiver<UiEvent>) {
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
(
|
||||
Self {
|
||||
sender,
|
||||
terminated: Arc::new(AtomicBool::new(false)),
|
||||
},
|
||||
receiver,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn send(&self, event: UiEvent) {
|
||||
let _ = self.sender.send(event);
|
||||
}
|
||||
|
||||
pub(crate) fn terminate(&self, event: UiEvent) {
|
||||
let _ = self.sender.send(event);
|
||||
self.terminated.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub(crate) fn mark_terminated(&self) {
|
||||
self.terminated.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub(crate) fn is_terminated(&self) -> bool {
|
||||
self.terminated.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
14
.jjconflict-base-0/desktop/ui/src/frames.rs
Normal file
14
.jjconflict-base-0/desktop/ui/src/frames.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) mod import;
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
pub(crate) mod plane;
|
||||
pub(crate) mod receive;
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
mod resample;
|
||||
pub(crate) mod sequence;
|
||||
pub(crate) mod sink;
|
||||
mod streamer;
|
||||
mod surface;
|
||||
|
||||
pub(crate) use streamer::FrameStreamer;
|
||||
pub(crate) use surface::FrameSurface;
|
||||
117
.jjconflict-base-0/desktop/ui/src/frames/import.rs
Normal file
117
.jjconflict-base-0/desktop/ui/src/frames/import.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use cef::sys::cef_color_type_t;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) mod d3d11;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) mod dmabuf;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) mod iosurface;
|
||||
|
||||
pub(crate) type TextureImportResult = Result<wgpu::Texture, TextureImportError>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum TextureImportError {
|
||||
#[error("Invalid texture handle: {0}")]
|
||||
InvalidHandle(String),
|
||||
#[error("Unsupported texture format: {format:?}")]
|
||||
UnsupportedFormat { format: cef_color_type_t },
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[error("Hardware acceleration not available: {reason}")]
|
||||
HardwareUnavailable { reason: String },
|
||||
#[error("Vulkan operation failed: {operation}")]
|
||||
#[cfg(target_os = "linux")]
|
||||
VulkanError { operation: String },
|
||||
#[error("Platform-specific error: {message}")]
|
||||
PlatformError { message: String },
|
||||
}
|
||||
|
||||
impl From<wgpu::hal::DeviceError> for TextureImportError {
|
||||
fn from(e: wgpu::hal::DeviceError) -> Self {
|
||||
TextureImportError::PlatformError {
|
||||
message: format!("wgpu-hal DeviceError: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct ContentRect {
|
||||
pub(crate) x: u32,
|
||||
pub(crate) y: u32,
|
||||
pub(crate) width: u32,
|
||||
pub(crate) height: u32,
|
||||
pub(crate) source_width: u32,
|
||||
pub(crate) source_height: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum ContentMapping {
|
||||
Identity,
|
||||
Scaled(ContentRect),
|
||||
}
|
||||
|
||||
impl ContentRect {
|
||||
pub(crate) fn mapping(self, width: u32, height: u32) -> ContentMapping {
|
||||
let valid = self.width > 0
|
||||
&& self.height > 0
|
||||
&& self.source_width > 0
|
||||
&& self.source_height > 0
|
||||
&& self.x.checked_add(self.width).is_some_and(|right| right <= width)
|
||||
&& self.y.checked_add(self.height).is_some_and(|bottom| bottom <= height);
|
||||
let full = self.x == 0 && self.y == 0 && (self.width, self.height) == (width, height) && (self.source_width, self.source_height) == (width, height);
|
||||
if valid && !full { ContentMapping::Scaled(self) } else { ContentMapping::Identity }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&cef::AcceleratedPaintInfo> for ContentRect {
|
||||
type Error = TextureImportError;
|
||||
|
||||
fn try_from(info: &cef::AcceleratedPaintInfo) -> Result<Self, Self::Error> {
|
||||
let invalid = || TextureImportError::InvalidHandle("Failed to create content rect".into());
|
||||
let content = &info.extra.content_rect;
|
||||
let width = u32::try_from(content.width).ok().filter(|&width| width > 0).ok_or_else(invalid)?;
|
||||
let height = u32::try_from(content.height).ok().filter(|&height| height > 0).ok_or_else(invalid)?;
|
||||
let source = &info.extra.source_size;
|
||||
let (source_width, source_height) = if info.extra.has_source_size != 0 && source.width > 0 && source.height > 0 {
|
||||
(source.width as u32, source.height as u32)
|
||||
} else {
|
||||
(width, height)
|
||||
};
|
||||
Ok(Self {
|
||||
x: content.x.max(0) as u32,
|
||||
y: content.y.max(0) as u32,
|
||||
width,
|
||||
height,
|
||||
source_width,
|
||||
source_height,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait TextureImporter {
|
||||
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult;
|
||||
}
|
||||
|
||||
fn wgpu_format(format: cef_color_type_t) -> Result<wgpu::TextureFormat, TextureImportError> {
|
||||
match format {
|
||||
cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(wgpu::TextureFormat::Bgra8Unorm),
|
||||
cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(wgpu::TextureFormat::Rgba8Unorm),
|
||||
_ => Err(TextureImportError::UnsupportedFormat { format }),
|
||||
}
|
||||
}
|
||||
|
||||
fn texture_descriptor(width: u32, height: u32, format: cef_color_type_t, label: &'static str) -> Result<wgpu::TextureDescriptor<'static>, TextureImportError> {
|
||||
Ok(wgpu::TextureDescriptor {
|
||||
label: Some(label),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu_format(format)?,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
136
.jjconflict-base-0/desktop/ui/src/frames/import/d3d11.rs
Normal file
136
.jjconflict-base-0/desktop/ui/src/frames/import/d3d11.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use super::{TextureImportError, TextureImportResult, TextureImporter, texture_descriptor, wgpu_format};
|
||||
use cef::sys::cef_color_type_t;
|
||||
use std::os::raw::c_void;
|
||||
use wgpu::hal::api;
|
||||
|
||||
pub struct D3D11Importer {
|
||||
pub handle: *mut c_void,
|
||||
pub format: cef_color_type_t,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl TextureImporter for D3D11Importer {
|
||||
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
if self.handle.is_null() {
|
||||
return Err(TextureImportError::InvalidHandle("Null D3D11 shared texture handle".to_string()));
|
||||
}
|
||||
|
||||
let is_d3d12_backend = unsafe { device.as_hal::<api::Dx12>().is_some() };
|
||||
|
||||
if is_d3d12_backend {
|
||||
let texture = self.import_via_d3d12(device)?;
|
||||
return Ok(texture);
|
||||
}
|
||||
|
||||
let texture = self.import_via_vulkan(device)?;
|
||||
tracing::trace!("Successfully imported D3D11 shared texture via Vulkan");
|
||||
Ok(texture)
|
||||
}
|
||||
}
|
||||
|
||||
impl D3D11Importer {
|
||||
pub fn from_parts(handle: u64, width: u32, height: u32, format: cef_color_type_t) -> Self {
|
||||
Self {
|
||||
handle: handle as *mut c_void,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
fn import_via_d3d12(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
use wgpu::hal::api;
|
||||
let hal_texture = unsafe {
|
||||
let hal_device_guard = device.as_hal::<api::Dx12>();
|
||||
let Some(hal_device) = hal_device_guard else {
|
||||
return Err(TextureImportError::HardwareUnavailable {
|
||||
reason: "Device is not using D3D12 backend".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let d3d12_resource = self.import_d3d11_handle_to_d3d12(&hal_device)?;
|
||||
|
||||
let hal_texture = <api::Dx12 as wgpu::hal::Api>::Device::texture_from_raw(
|
||||
d3d12_resource,
|
||||
wgpu_format(self.format)?,
|
||||
wgpu::TextureDimension::D2,
|
||||
wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
1, // mip_level_count
|
||||
1, // sample_count
|
||||
);
|
||||
|
||||
Ok::<_, TextureImportError>(hal_texture)
|
||||
}?;
|
||||
|
||||
let texture = unsafe { device.create_texture_from_hal::<api::Dx12>(hal_texture, &texture_descriptor(self.width, self.height, self.format, "CEF D3D11→D3D12 Shared Texture")?) };
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
fn import_via_vulkan(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
use wgpu::{TextureUses, wgc::api::Vulkan};
|
||||
let hal_texture = unsafe {
|
||||
let hal_device_guard = device.as_hal::<Vulkan>();
|
||||
let Some(hal_device) = hal_device_guard else {
|
||||
return Err(TextureImportError::HardwareUnavailable {
|
||||
reason: "Device is not using Vulkan backend".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let hal_texture = <Vulkan as wgpu::hal::Api>::Device::texture_from_d3d11_shared_handle(
|
||||
&hal_device,
|
||||
windows::Win32::Foundation::HANDLE(self.handle),
|
||||
&wgpu::hal::TextureDescriptor {
|
||||
label: Some("CEF D3D11 Shared Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu_format(self.format)?,
|
||||
usage: TextureUses::COPY_DST | TextureUses::COPY_SRC | TextureUses::RESOURCE,
|
||||
memory_flags: wgpu::hal::MemoryFlags::empty(),
|
||||
view_formats: vec![],
|
||||
},
|
||||
)
|
||||
.map_err(|e| TextureImportError::PlatformError {
|
||||
message: format!("Failed to import D3D11 shared handle into Vulkan: {:?}", e),
|
||||
})?;
|
||||
|
||||
Ok::<_, TextureImportError>(hal_texture)
|
||||
}?;
|
||||
|
||||
let texture = unsafe { device.create_texture_from_hal::<Vulkan>(hal_texture, &texture_descriptor(self.width, self.height, self.format, "CEF D3D11 Shared Texture")?) };
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
fn import_d3d11_handle_to_d3d12(&self, hal_device: &<wgpu::hal::api::Dx12 as wgpu::hal::Api>::Device) -> Result<windows::Win32::Graphics::Direct3D12::ID3D12Resource, TextureImportError> {
|
||||
use windows::Win32::Graphics::Direct3D12::*;
|
||||
|
||||
let d3d12_device = hal_device.raw_device();
|
||||
|
||||
if self.width == 0 || self.height == 0 {
|
||||
return Err(TextureImportError::InvalidHandle("Invalid D3D11 texture dimensions".to_string()));
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let mut shared_resource: Option<ID3D12Resource> = None;
|
||||
d3d12_device
|
||||
.OpenSharedHandle(windows::Win32::Foundation::HANDLE(self.handle), &mut shared_resource)
|
||||
.map_err(|e| TextureImportError::PlatformError {
|
||||
message: format!("Failed to open D3D11 shared handle on D3D12: {:?}", e),
|
||||
})?;
|
||||
|
||||
shared_resource.ok_or_else(|| TextureImportError::InvalidHandle("Failed to get D3D12 resource from shared handle".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
226
.jjconflict-base-0/desktop/ui/src/frames/import/dmabuf.rs
Normal file
226
.jjconflict-base-0/desktop/ui/src/frames/import/dmabuf.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use super::{TextureImportError, TextureImportResult, TextureImporter, texture_descriptor, wgpu_format};
|
||||
use ash::vk;
|
||||
use cef::sys::cef_color_type_t;
|
||||
use wgpu::hal::api;
|
||||
|
||||
pub struct DmaBufImporter {
|
||||
fds: Vec<std::os::fd::OwnedFd>,
|
||||
format: cef_color_type_t,
|
||||
modifier: u64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
strides: Vec<u32>,
|
||||
offsets: Vec<u32>,
|
||||
}
|
||||
|
||||
impl TextureImporter for DmaBufImporter {
|
||||
fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
if self.fds.len() != 1 {
|
||||
return Err(TextureImportError::InvalidHandle(format!("Expected exactly one DMA-BUF plane fd, got {}", self.fds.len())));
|
||||
}
|
||||
|
||||
if self.strides.len() != self.fds.len() || self.offsets.len() != self.fds.len() {
|
||||
return Err(TextureImportError::InvalidHandle(format!(
|
||||
"DMA-BUF plane count mismatch: {} fds, {} strides, {} offsets",
|
||||
self.fds.len(),
|
||||
self.strides.len(),
|
||||
self.offsets.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let texture = self.import_via_vulkan(device)?;
|
||||
tracing::trace!("Successfully imported DMA-BUF texture via Vulkan");
|
||||
Ok(texture)
|
||||
}
|
||||
}
|
||||
|
||||
impl DmaBufImporter {
|
||||
pub fn from_parts(fds: Vec<std::os::fd::OwnedFd>, strides: Vec<u32>, offsets: Vec<u32>, modifier: u64, width: u32, height: u32, format: cef_color_type_t) -> Self {
|
||||
Self {
|
||||
fds,
|
||||
format,
|
||||
modifier,
|
||||
width,
|
||||
height,
|
||||
strides,
|
||||
offsets,
|
||||
}
|
||||
}
|
||||
|
||||
fn import_via_vulkan(&self, device: &wgpu::Device) -> TextureImportResult {
|
||||
use wgpu::{TextureUses, wgc::api::Vulkan};
|
||||
let hal_texture = unsafe {
|
||||
let hal_device_guard = device.as_hal::<api::Vulkan>();
|
||||
let Some(hal_device) = hal_device_guard else {
|
||||
return Err(TextureImportError::HardwareUnavailable {
|
||||
reason: "Device is not using Vulkan backend".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let (vk_image, device_memory) = self.create_vulkan_image_from_dmabuf(&hal_device)?;
|
||||
|
||||
let hal_texture = <api::Vulkan as wgpu::hal::Api>::Device::texture_from_raw(
|
||||
&hal_device,
|
||||
vk_image,
|
||||
&wgpu::hal::TextureDescriptor {
|
||||
label: Some("CEF DMA-BUF Texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu_format(self.format)?,
|
||||
usage: TextureUses::COPY_DST | TextureUses::COPY_SRC | TextureUses::RESOURCE,
|
||||
memory_flags: wgpu::hal::MemoryFlags::empty(),
|
||||
view_formats: vec![],
|
||||
},
|
||||
None,
|
||||
wgpu::hal::vulkan::TextureMemory::Dedicated(device_memory),
|
||||
);
|
||||
|
||||
Ok::<_, TextureImportError>(hal_texture)
|
||||
}?;
|
||||
|
||||
let texture = unsafe { device.create_texture_from_hal::<Vulkan>(hal_texture, &texture_descriptor(self.width, self.height, self.format, "CEF DMA-BUF Texture")?) };
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
fn create_vulkan_image_from_dmabuf(&self, hal_device: &<api::Vulkan as wgpu::hal::Api>::Device) -> Result<(vk::Image, vk::DeviceMemory), TextureImportError> {
|
||||
let device = hal_device.raw_device();
|
||||
let instance = hal_device.shared_instance().raw_instance();
|
||||
|
||||
if self.width == 0 || self.height == 0 {
|
||||
return Err(TextureImportError::InvalidHandle("Invalid DMA-BUF dimensions".to_string()));
|
||||
}
|
||||
|
||||
let image_create_info = vk::ImageCreateInfo::default()
|
||||
.image_type(vk::ImageType::TYPE_2D)
|
||||
.format(vulkan_format(self.format)?)
|
||||
.extent(vk::Extent3D {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
depth: 1,
|
||||
})
|
||||
.mip_levels(1)
|
||||
.array_layers(1)
|
||||
.samples(vk::SampleCountFlags::TYPE_1)
|
||||
.tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT)
|
||||
.usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_SRC)
|
||||
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
||||
|
||||
let plane_layouts = self
|
||||
.offsets
|
||||
.iter()
|
||||
.zip(&self.strides)
|
||||
.map(|(&offset, &stride)| vk::SubresourceLayout {
|
||||
offset: offset as u64,
|
||||
size: 0, // Will be calculated by driver
|
||||
row_pitch: stride as u64,
|
||||
array_pitch: 0,
|
||||
depth_pitch: 0,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut drm_format_modifier = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default()
|
||||
.drm_format_modifier(self.modifier)
|
||||
.plane_layouts(&plane_layouts);
|
||||
|
||||
let mut external_memory_info = vk::ExternalMemoryImageCreateInfo::default().handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT);
|
||||
|
||||
let image_create_info = image_create_info.push_next(&mut drm_format_modifier).push_next(&mut external_memory_info);
|
||||
|
||||
let image = unsafe {
|
||||
device.create_image(&image_create_info, None).map_err(|e| TextureImportError::VulkanError {
|
||||
operation: format!("Failed to create Vulkan image: {e:?}"),
|
||||
})?
|
||||
};
|
||||
|
||||
let memory_requirements = unsafe { device.get_image_memory_requirements(image) };
|
||||
|
||||
// Duplicate the file descriptor
|
||||
let dup_fd = unsafe { libc::dup(std::os::fd::AsRawFd::as_raw_fd(&self.fds[0])) };
|
||||
if dup_fd == -1 {
|
||||
// SAFETY: the image was created above and never bound or returned.
|
||||
unsafe { device.destroy_image(image, None) };
|
||||
return Err(TextureImportError::PlatformError {
|
||||
message: "Failed to duplicate DMA-BUF file descriptor".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let external_memory_fd = ash::khr::external_memory_fd::Device::new(instance, device);
|
||||
let mut fd_properties = vk::MemoryFdPropertiesKHR::default();
|
||||
if let Err(e) = unsafe { external_memory_fd.get_memory_fd_properties(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT, dup_fd, &mut fd_properties) } {
|
||||
// SAFETY: import failed and the fd is still ours, need to clean up the image and close the fd
|
||||
unsafe {
|
||||
device.destroy_image(image, None);
|
||||
libc::close(dup_fd);
|
||||
}
|
||||
return Err(TextureImportError::VulkanError {
|
||||
operation: format!("Failed to query DMA-BUF fd memory properties: {e:?}"),
|
||||
});
|
||||
}
|
||||
|
||||
let mut import_memory_fd = vk::ImportMemoryFdInfoKHR::default().handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT).fd(dup_fd);
|
||||
|
||||
let memory_properties = unsafe { instance.get_physical_device_memory_properties(hal_device.raw_physical_device()) };
|
||||
|
||||
let compatible_type_bits = memory_requirements.memory_type_bits & fd_properties.memory_type_bits;
|
||||
let Some(memory_type_index) = find_memory_type_index(compatible_type_bits, vk::MemoryPropertyFlags::empty(), &memory_properties) else {
|
||||
// SAFETY: import failed and the fd is still ours, need to clean up the image and close the fd
|
||||
unsafe {
|
||||
device.destroy_image(image, None);
|
||||
libc::close(dup_fd);
|
||||
}
|
||||
return Err(TextureImportError::VulkanError {
|
||||
operation: "Failed to find suitable memory type for DMA-BUF".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let allocate_info = vk::MemoryAllocateInfo::default()
|
||||
.allocation_size(memory_requirements.size)
|
||||
.memory_type_index(memory_type_index)
|
||||
.push_next(&mut import_memory_fd);
|
||||
|
||||
let device_memory = match unsafe { device.allocate_memory(&allocate_info, None) } {
|
||||
Ok(memory) => memory,
|
||||
Err(e) => {
|
||||
// SAFETY: import failed and the fd is still ours, need to clean up the image and close the fd
|
||||
unsafe {
|
||||
device.destroy_image(image, None);
|
||||
libc::close(dup_fd);
|
||||
}
|
||||
return Err(TextureImportError::VulkanError {
|
||||
operation: format!("Failed to allocate memory for DMA-BUF: {e:?}"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = unsafe { device.bind_image_memory(image, device_memory, 0) } {
|
||||
// SAFETY: import failed, need to clean up the image and free the memory
|
||||
unsafe {
|
||||
device.destroy_image(image, None);
|
||||
device.free_memory(device_memory, None);
|
||||
}
|
||||
return Err(TextureImportError::VulkanError {
|
||||
operation: format!("Failed to bind memory to image: {e:?}"),
|
||||
});
|
||||
}
|
||||
|
||||
Ok((image, device_memory))
|
||||
}
|
||||
}
|
||||
|
||||
fn vulkan_format(format: cef_color_type_t) -> Result<vk::Format, TextureImportError> {
|
||||
match format {
|
||||
cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(vk::Format::B8G8R8A8_UNORM),
|
||||
cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(vk::Format::R8G8B8A8_UNORM),
|
||||
_ => Err(TextureImportError::UnsupportedFormat { format }),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_memory_type_index(type_filter: u32, properties: vk::MemoryPropertyFlags, mem_properties: &vk::PhysicalDeviceMemoryProperties) -> Option<u32> {
|
||||
(0..mem_properties.memory_type_count).find(|&i| (type_filter & (1 << i)) != 0 && mem_properties.memory_types[i as usize].property_flags.contains(properties))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user