commit f0b5f8ff557bbeff2fa32bb10ad935562e92d0ed Author: Brandon Thomas Date: Wed Sep 30 05:17:08 2026 -0400 one-shot first ver diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..5592118f --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run -p xtask --" diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0d78b09d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*~ +plan/ +/target +/dist +/corpus +/log +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..e0fd683e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,32 @@ +# PrintCraft — instructions for agents + +PrintCraft is a clean-room, open-source, Rust-native PDF application targeting Adobe Acrobat Pro parity. It runs natively on macOS, Windows and Linux, and on the web via WASM. It is the sibling of `../photocraft` (a Photoshop-class editor) and follows the same conventions. + +## Start every session here +1. Read `plan/STATUS.md`: the current milestone, the next unchecked task and any blockers. +2. Read that task in `plan/execution-plan.md` §3, the relevant section of `plan/architecture.md`, and the README of the crate you're touching. +3. Follow the **autonomous operation protocol** in `plan/execution-plan.md` §7 (orient → plan → implement + test → verify → record → commit). Don't stop to ask unless §7 lists the decision as the user's. + +`plan/` is gitignored (local-only, like PhotoCraft). The machine-readable parity checklist lives in `parity/` (committed). + +## Non-negotiables +- **Clean-room.** + - Never read, disassemble or copy anything inside the Acrobat bundle (names and listings only). **Never open `Contents/Resources/JavaScripts/`.** + - Behaviour comes from public docs, specs (ISO 32000-2, the Arlington model) and black-box observation (`plan/acrobat/`). + - Never copy GPL/AGPL code. MuPDF, Ghostscript, Poppler, veraPDF and DSS run only as external oracle processes. + - See `plan/README.md` §Clean-room and `plan/adr/0001`. +- **Privacy.** When observing Acrobat, use synthetic fixtures only. Never capture the Home view, recent files or account info. Capture by window id (`plan/acrobat/tools/`). Never commit Acrobat outputs, corpus files or personal data. +- **Layering.** Nothing below L7 depends on egui/winit/eframe/rfd (`plan/architecture.md` §3). `cos`/`filters`/`crypt`/`arlington` stay standalone. +- **Fidelity.** The PDF object graph is the model. Preserve unknown data. Saves are incremental unless a full rewrite is required. Never silently drop or repair data without recording it. +- **Rust only** in the product and build (`xtask`). No handwritten JS/TS. +- **Quality gates** before every commit: `cargo fmt --check`, `cargo clippy --workspace -- -D warnings`, `cargo test --workspace`, and the wasm check once `xtask ci` exists (M0). +- **Commits:** one task id per commit (e.g. `M1.4: xref stream reader`). Commit only green states. End messages with the attribution line required by the environment. + +## Running and looking at the app +- `cargo run -p printcraft -- ` opens the desktop app. +- `cargo xtask demo-pdf` builds `dist/demo/printcraft-showcase.pdf` (needs Chrome) for visual checks. +- For UI work, **look at the result**: screenshot the window by id (`plan/acrobat/tools/winlist.swift` + `screencapture -l`) until the UI control channel (M3.9) exists, then use `ui.screenshot`. Compare against `plan/acrobat/02-ui-ux.md`. +- Parallel agents: use a separate `CARGO_TARGET_DIR` per agent and separate git worktrees. + +## Current bootstrap debt (tracked in STATUS.md) +- `printcraft-render` renders through the `hayro` crate directly and inspects documents through `lopdf`. Both get replaced by `cos` / `model` / the DisplayList device (M1–M2, ADR-0004). diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..497421e4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4919 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accesskit" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +dependencies = [ + "uuid", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8c61bee90b42a772d39d06a740207dc71a4e780004ace1db8d99fb1baaa954" +dependencies = [ + "accesskit", + "accesskit_consumer 0.36.0", + "atspi-common", + "phf", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_consumer" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e0d7e25d06f4dc21d1774d67146e9e80d6789216cbd4d1e88185b0095dba60" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_consumer" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" +dependencies = [ + "accesskit", + "accesskit_consumer 0.38.0", + "hashbrown 0.16.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b016ca8db0ea0ea2ceff29a9d6240391492d960716aa471967c00e8cc8cb197c" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" +dependencies = [ + "accesskit", + "accesskit_consumer 0.35.0", + "hashbrown 0.16.1", + "static_assertions", + "windows", + "windows-core", +] + +[[package]] +name = "accesskit_winit" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.2", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.21", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.5", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.5", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.5", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.2", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.13.2", + "polling", + "rustix 1.1.5", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.4", + "rustix 1.1.5", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "color" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.4", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "ecb" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f2a8b3e564eba0877223dc343703ad0385794e882e6d13f3a4dd5c6b1f41ac" +dependencies = [ + "cipher", +] + +[[package]] +name = "ecolor" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0656a7530b9e2841ee7a5f983965bdd9840c32f4012f759cfd44b912cced6b34" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "eframe" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ad41fac6e3149abb4866e93df37dbd33baa95db525c3368c944584e95a5dc2" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-wgpu", + "egui-winit", + "egui_glow", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "pollster 1.0.1", + "profiling", + "raw-window-handle", + "static_assertions", + "wasm-bindgen", + "web-sys", + "web-time", + "wgpu", + "windows-sys 0.61.2", + "winit", +] + +[[package]] +name = "egui" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc938cc27cd911415e1e4d151bc56ff9df063d8db081374e87aeb805ea74b2ba" +dependencies = [ + "accesskit", + "ahash", + "bitflags 2.13.2", + "emath", + "epaint", + "itertools", + "log", + "nohash-hasher", + "profiling", + "smallvec", + "unicode-segmentation", + "web-sys", +] + +[[package]] +name = "egui-wgpu" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cef1f6d1bba40b09a4d0d62cb822b81030922cfbf65c5373398c3a3caafc5bd" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "profiling", + "thiserror 2.0.21", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98466000559d66ba4db786a30448b01483946add4188e7f10f522a27b4fb89cc" +dependencies = [ + "accesskit_winit", + "arboard", + "bytemuck", + "egui", + "log", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", + "profiling", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "winit", +] + +[[package]] +name = "egui_extras" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05cbdc785020861553f230f23d330ba1fb72613b9d8e0e457f3bdc8ba6db6dcf" +dependencies = [ + "ahash", + "egui", + "enum-map", + "itertools", + "log", + "profiling", + "resvg", +] + +[[package]] +name = "egui_glow" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "606ed4167daf341e5753d703285c4c1d562324b2c07aa8f405cd7d066fe58f3b" +dependencies = [ + "bytemuck", + "egui", + "glow", + "log", + "profiling", + "winit", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "emath" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c8d4141cf0f60fb2f7aefaf352d3ffa5171ef0ab09731c64391801e95e48f1" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "epaint" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0ec0308dc23130fc1a7d4623a8534c2ed069c6dc84e258ee4f3de99615aa1a3" +dependencies = [ + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "font-types 0.12.5", + "harfrust", + "log", + "nohash-hasher", + "parking_lot", + "profiling", + "self_cell", + "skrifa 0.44.0", + "smallvec", + "unicode-general-category", + "unicode-segmentation", + "vello_cpu 0.1.0", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773fa9c96dd0dbef887e39d0ed6177f141cce4d68e6041df77570aa3702dfa13" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fearless_simd" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "font-types" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8eb065f3251655b3c90e22e5e363f310fc5332fb3402e37bbc94752283248f6" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.5", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glifo" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d99fc21d493812643aae86d53b7bbd02f376434a90317e8a790bc209fdd6605e" +dependencies = [ + "bytemuck", + "foldhash", + "hashbrown 0.17.1", + "log", + "peniko", + "png 0.18.1", + "skrifa 0.42.1", + "smallvec", + "vello_common 0.0.9", +] + +[[package]] +name = "glifo" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed4a1bb24121291d27230c1b1b44e07d6a9b28cefdb32fe1581dfb84e14f940a" +dependencies = [ + "bytemuck", + "foldhash", + "hashbrown 0.17.1", + "log", + "peniko", + "skrifa 0.44.0", + "smallvec", + "vello_common 0.1.0", +] + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.13.2", + "cfg_aliases", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.21", + "windows", +] + +[[package]] +name = "guillotiere" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" +dependencies = [ + "euclid", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "read-fonts 0.41.0", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hayro" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4caa128ab87fd48ffb7490617cf93f77f606820dffbc9fd9ef6ab0ed077f56d" +dependencies = [ + "bytemuck", + "hayro-interpret", + "image", + "kurbo 0.13.1", + "pic-scale", + "vello_cpu 0.0.8", +] + +[[package]] +name = "hayro-ccitt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a" + +[[package]] +name = "hayro-cmap" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d285dc30731c8485de5fa732fbdf2b3affdf01e4da7c2135022ca6fa664bf6" +dependencies = [ + "brotli", + "hayro-postscript", +] + +[[package]] +name = "hayro-interpret" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2613d0406898995042d0794c4245b2f2fba1246490c8ac593769bba6551129d" +dependencies = [ + "bitflags 2.13.2", + "hayro-cmap", + "hayro-syntax", + "kurbo 0.13.1", + "moxcms", + "phf", + "rustc-hash 2.1.3", + "siphasher", + "skrifa 0.42.1", + "smallvec", + "yoke", +] + +[[package]] +name = "hayro-jbig2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69374b3668dd45aeb3d3145cda68f2c7b4f223aaa2511e67d076f1c7d741388d" +dependencies = [ + "fearless_simd", + "hayro-ccitt", +] + +[[package]] +name = "hayro-jpeg2000" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ab947623ef4ccaa7acf0579edf7cbb5a73838e3839a7be73335e522f433a1" +dependencies = [ + "fearless_simd", +] + +[[package]] +name = "hayro-postscript" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885c5ef0654933139a9b9546fc2c69e18d37f38aa2520f079092b1be18f1fcaa" + +[[package]] +name = "hayro-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edeafd70aa2db743de8ede8637d07ec87db05efe69cde371d03f1b185fcef27" +dependencies = [ + "flate2", + "hayro-ccitt", + "hayro-jbig2", + "hayro-jpeg2000", + "memchr", + "rustc-hash 2.1.3", + "smallvec", + "zune-jpeg", +] + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", + "tiff", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding", + "hybrid-array", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "bitflags 2.13.2", + "libc", + "plain", + "redox_syscall 0.9.4", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lopdf" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfffda0fe1ab0157e1a13c14bebd3f28671f2fccb7922f0722ec53926e6922d3" +dependencies = [ + "aes", + "bitflags 2.13.2", + "brotli-decompressor", + "cbc", + "chrono", + "ecb", + "encoding_rs", + "flate2", + "getrandom 0.4.3", + "indexmap", + "itoa", + "log", + "md-5", + "nom", + "rand", + "rangemap", + "rayon", + "sha2", + "stringprep", + "thiserror 2.0.21", + "weezl 0.2.1", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "naga" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.17.1", + "indexmap", + "libm", + "log", + "naga-types", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.21", + "unicode-ident", +] + +[[package]] +name = "naga-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49" +dependencies = [ + "hashbrown 0.17.1", + "indexmap", + "rustc-hash 1.1.0", + "thiserror 2.0.21", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "peniko" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6" +dependencies = [ + "bytemuck", + "color", + "kurbo 0.13.1", + "linebender_resource_handle", + "smallvec", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pic-scale" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb0ff645d35accaec252de081d55984a0d6c952cb1e5dc2b9afa7c78f07288" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.5", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "pollster" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6355899e1c9462875b6757c79f3caa011a1fdae12bbb1a2e72dd1f234f8336" + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "printcraft" +version = "0.1.0" +dependencies = [ + "eframe", + "egui", + "printcraft-ui-egui", +] + +[[package]] +name = "printcraft-engine" +version = "0.1.0" +dependencies = [ + "printcraft-render", +] + +[[package]] +name = "printcraft-geom" +version = "0.1.0" + +[[package]] +name = "printcraft-render" +version = "0.1.0" +dependencies = [ + "hayro", + "hayro-syntax", + "log", + "lopdf", + "thiserror 2.0.21", +] + +[[package]] +name = "printcraft-ui-egui" +version = "0.1.0" +dependencies = [ + "eframe", + "egui", + "egui_extras", + "printcraft-engine", + "printcraft-render", + "rfd", + "serde", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rangemap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" +dependencies = [ + "bytemuck", + "font-types 0.11.3", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types 0.12.5", + "once_cell", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_syscall" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", +] + +[[package]] +name = "rfd" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" +dependencies = [ + "block2 0.6.2", + "dispatch2", + "js-sys", + "libc", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "percent-encoding", + "pollster 0.4.0", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "skrifa" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" +dependencies = [ + "bytemuck", + "read-fonts 0.39.2", +] + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts 0.41.0", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.13.2", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.13.2", + "calloop 0.14.4", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.5", + "thiserror 2.0.21", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.3", + "siphasher", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.5", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl 0.1.12", + "zune-jpeg", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.3", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64", + "data-url", + "flate2", + "imagesize", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "xmlwriter", +] + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vello_common" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3361bff7f7d82c0c496b92048db83846691f0e844cc28dee92b1c824291b55ee" +dependencies = [ + "bytemuck", + "fearless_simd", + "guillotiere", + "hashbrown 0.17.1", + "log", + "peniko", + "png 0.18.1", + "smallvec", + "thiserror 2.0.21", +] + +[[package]] +name = "vello_common" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d672facaa2d697285a786cd9d44d614cd2ce54cdc022504bf339f8fff3b750" +dependencies = [ + "bytemuck", + "fearless_simd", + "guillotiere", + "hashbrown 0.17.1", + "log", + "peniko", + "png 0.18.1", + "smallvec", + "thiserror 2.0.21", +] + +[[package]] +name = "vello_common" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e9aed918117e8152c9eddfd8362d73c465c23f26c44786aa331707b8a64fa2" +dependencies = [ + "bytemuck", + "fearless_simd", + "guillotiere", + "log", + "peniko", + "smallvec", + "thiserror 2.0.21", +] + +[[package]] +name = "vello_cpu" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d8ded630e8316bb94a55881256506d1f3b9947b5f66db8a7d32ca7ba02decd0" +dependencies = [ + "bytemuck", + "glifo 0.1.1", + "hashbrown 0.17.1", + "png 0.18.1", + "vello_common 0.0.8", +] + +[[package]] +name = "vello_cpu" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac7349e1f55f6b801c7c277958df4ea53e7f20f21e8014910ad888b2ecda93ea" +dependencies = [ + "bytemuck", + "glifo 0.2.0", + "hashbrown 0.17.1", + "vello_common 0.1.0", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.5", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix 1.1.5", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.13.2", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.5", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "weezl" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ca08e5ef825b65b056d9efbd95c8750683f0a6d0466d02e96dc2e4e360f3d2" + +[[package]] +name = "wgpu" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" +dependencies = [ + "arrayvec", + "bitflags 2.13.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags 2.13.2", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "indexmap", + "log", + "naga", + "naga-types", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.21", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-wasm" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d260ca2ca8adc654f513a869bf3cac0b47f5b8d3f87803d6942015f8de597aae" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.13.2", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "hashbrown 0.17.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "naga", + "naga-types", + "ndk-sys", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "static_assertions", + "thiserror 2.0.21", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge", + "wgpu-types", + "windows", + "windows-core", + "windows-result", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "js-sys", + "log", + "naga-types", + "raw-window-handle", + "static_assertions", + "web-sys", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.13.2", + "block2 0.5.1", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "smithay-client-toolkit 0.19.2", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.5", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.2", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "anyhow", + "lopdf", + "png 0.18.1", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.5", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.6", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" +dependencies = [ + "serde", + "winnow", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + +[[package]] +name = "zerocopy" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.6", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.6", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..9d150313 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,54 @@ +[workspace] +resolver = "3" +members = ["crates/*", "apps/*", "xtask"] +default-members = ["crates/*", "apps/printcraft", "xtask"] + +[workspace.package] +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" +rust-version = "1.90" +repository = "https://github.com/storytold/printcraft" + +[workspace.dependencies] +# internal +printcraft-geom = { path = "crates/geom" } +printcraft-render = { path = "crates/render" } +printcraft-engine = { path = "crates/engine" } +printcraft-ui-egui = { path = "crates/ui-egui" } + +# pdf +hayro = "0.7.1" +hayro-syntax = "0.7.2" +lopdf = "0.45" + +# foundation +thiserror = "2" +serde = { version = "1", features = ["derive", "rc"] } +serde_json = "1" +log = "0.4" +kurbo = "0.12" + +# ui +egui = "0.36" +egui_extras = { version = "0.36", default-features = false, features = ["svg"] } +eframe = { version = "0.36", default-features = false } +egui_kittest = "0.36" +rfd = "0.17" + +[workspace.lints.rust] +unsafe_code = "deny" + +[workspace.lints.clippy] +dbg_macro = "warn" +todo = "warn" + +[profile.dev.package."*"] +opt-level = 2 + +[profile.dev] +opt-level = 1 + +[profile.release] +lto = "thin" +codegen-units = 1 diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..1a98ff85 --- /dev/null +++ b/NOTICE @@ -0,0 +1,15 @@ +PrintCraft +Copyright the PrintCraft contributors. Licensed under MIT OR Apache-2.0. + +This product bundles or depends on third-party material under the following licences: + +- Lucide icons (assets/icons/*.svg) — ISC License. See assets/icons/LICENSE-lucide.txt. +- Inter typeface (assets/fonts/Inter-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-Inter.txt. +- JetBrains Mono typeface (assets/fonts/JetBrainsMono-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-JetBrainsMono.txt. +- hayro (rendering, via crates.io) — Apache-2.0 OR MIT. Its `embed-fonts` feature bundles the Foxit + standard-14 Type 1 fonts under BSD-3-Clause ("Copyright 2014 PDFium Authors"), and hayro-cmap bundles + Adobe CMap resources under BSD-3-Clause. +- lopdf (bootstrap document inspection, via crates.io) — MIT. + +Asset licences are not visible to cargo-deny; keep this file current whenever assets or asset-bundling +dependencies change (plan/adr/0002, 0006). diff --git a/README.md b/README.md new file mode 100644 index 00000000..b13535fc --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +PrintCraft +========== + +From the artcraft team + +A clean-room, open-source PDF application written in Rust, aiming at Adobe Acrobat Pro parity. It runs natively on macOS, Windows and Linux, and in the browser via WebAssembly. It is a sibling project of PhotoCraft. + +**Status:** early. The viewer slice works; the core PDF object layer (M1) is next. The plan lives in `plan/` (local) and the rules for contributors and agents are in `CLAUDE.md`. + +```sh +cargo run -p printcraft -- some.pdf # desktop app +cargo xtask demo-pdf # build dist/demo/printcraft-showcase.pdf (needs Chrome) +cargo run -p printcraft -- dist/demo/printcraft-showcase.pdf --panel bookmarks +``` + +Licence: MIT OR Apache-2.0 (proposed). Third-party material is listed in `NOTICE`. diff --git a/apps/printcraft/Cargo.toml b/apps/printcraft/Cargo.toml new file mode 100644 index 00000000..c959ad46 --- /dev/null +++ b/apps/printcraft/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "printcraft" +description = "PrintCraft desktop app" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +printcraft-ui-egui = { workspace = true } +eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11"] } +egui = { workspace = true } diff --git a/apps/printcraft/src/main.rs b/apps/printcraft/src/main.rs new file mode 100644 index 00000000..a868b9aa --- /dev/null +++ b/apps/printcraft/src/main.rs @@ -0,0 +1,56 @@ +//! PrintCraft desktop app. +//! +//! Usage: `printcraft [options] [files…]` +//! +//! View options (applied after the files open; also the seed of the UI control channel): +//! `--page N --zoom 150 --layout continuous|two-up|single --panel comments|bookmarks|pages|fields|layers|attachments|none +//! --theme light|dark --mode all|read|edit|convert|sign --tool --left open|closed +//! --organize on --fields on --dialog properties|shortcuts|about --palette --home on` + +use printcraft_ui_egui::PrintCraftApp; + +fn main() -> eframe::Result { + let mut files = Vec::new(); + let mut options: Vec<(String, String)> = Vec::new(); + let mut args = std::env::args().skip(1); + while let Some(a) = args.next() { + match a.as_str() { + "--version" => { + println!("printcraft {}", env!("CARGO_PKG_VERSION")); + return Ok(()); + } + flag if flag.starts_with("--") => { + let value = args.next().unwrap_or_default(); + options.push((flag.trim_start_matches("--").to_string(), value)); + } + _ => files.push(a), + } + } + let integrated = cfg!(target_os = "macos"); + let mut viewport = egui::ViewportBuilder::default() + .with_title("PrintCraft") + .with_inner_size([1440.0, 920.0]) + .with_min_inner_size([820.0, 520.0]) + .with_drag_and_drop(true); + if integrated { + viewport = viewport.with_fullsize_content_view(true).with_titlebar_shown(false).with_title_shown(false); + } + let native = eframe::NativeOptions { viewport, ..Default::default() }; + eframe::run_native( + "PrintCraft", + native, + Box::new(move |_cc| { + let mut app = PrintCraftApp::new(); + app.integrated_titlebar = integrated; + for f in files { + app.open_path(&f); + } + for (k, v) in options { + if let Err(e) = app.set_option(&k, &v) { + eprintln!("printcraft: --{k} {v}: {e}"); + } + } + Ok(Box::new(app)) + }), + ) +} diff --git a/assets/demo/showcase-data.csv b/assets/demo/showcase-data.csv new file mode 100644 index 00000000..2ae4f4ad --- /dev/null +++ b/assets/demo/showcase-data.csv @@ -0,0 +1,23 @@ +# Illustrative sample data behind the charts on page 7 of the PrintCraft showcase. +# These are made-up numbers for demonstration, not measurements. +chart,series,label,value +pages_per_second,throughput,Text,189 +pages_per_second,throughput,Forms,139 +pages_per_second,throughput,Vector,94 +pages_per_second,throughput,Scans,70 +pages_per_second,throughput,Maps,43 +conformance_percent,Text,0.1,74 +conformance_percent,Text,0.2,82 +conformance_percent,Text,0.3,89 +conformance_percent,Text,0.4,94 +conformance_percent,Text,0.5,97 +conformance_percent,Vector,0.1,66 +conformance_percent,Vector,0.2,73 +conformance_percent,Vector,0.3,80 +conformance_percent,Vector,0.4,87 +conformance_percent,Vector,0.5,92 +conformance_percent,Forms,0.1,62 +conformance_percent,Forms,0.2,65 +conformance_percent,Forms,0.3,71 +conformance_percent,Forms,0.4,79 +conformance_percent,Forms,0.5,85 diff --git a/assets/demo/showcase.html b/assets/demo/showcase.html new file mode 100644 index 00000000..e5b10786 --- /dev/null +++ b/assets/demo/showcase.html @@ -0,0 +1,779 @@ + + + + +PrintCraft Showcase + + + + + + +
+ +
+
Specimen · Volume One
+

PrintCraft
Showcase

+
A tour of the page, glyph by glyph.
+

Typography, world scripts, vector graphics, mathematics, forms and annotations — gathered in one document to put a PDF engine through its paces.

+
+
PrintCraft
Open-source PDF toolkit in Rust
+
Generated by cargo xtask demo-pdf
US Letter · 8.5 × 11 in
+
+
+
+ + +
+
PrintCraft ShowcaseContents
+
Inside this specimen
+

Contents

+
    +
  1. 1Setting TextColumns, justification, hyphenation, drop caps and notes1
  2. +
  3. 2OpenType FeaturesLigatures, small capitals, figures, fractions, swashes, weights2
  4. +
  5. 3Expressive TypeShadows, gradients, rotation, skew and text on a path3
  6. +
  7. 4Scripts of the WorldRight-to-left, complex shaping, vertical CJK and emoji4
  8. +
  9. 5MathematicsMathML Core set in STIX Two Math5
  10. +
  11. 6Vector GraphicsShadings, blend modes, clipping, masks and patterns6
  12. +
  13. 7Data & TablesCharts and a striped feature matrix7
  14. +
  15. 8Code & ImagesA syntax-coloured listing and a generated raster8
  16. +
  17. 9Review & MarkupEvery common annotation type, with appearance streams9
  18. +
  19. 10Interactive FormAcroForm fields written directly by the xtask10
  20. +
+
+

Colophon

Body text is set in Hoefler Text with Didot for display, Avenir Next and Inter for labels, and JetBrains Mono for code. Layout is authored in HTML and CSS, printed by headless Chrome, then post-processed with lopdf.
+

What to look for

Open the bookmarks, layers, attachments and comments panels. Page labels run Cover, i–ii, then 1–10. One layer adds a draft watermark; another holds print-only notes and starts hidden.
+
+
PrintCraft Showcasei
+
+ + +
+
PrintCraft ShowcaseForeword
+
Foreword
+

Why a specimen?

+

A PDF reader is judged in the first second. The page appears, and either the letters sit on it the way the author intended or they do not. Every kerning pair, every ligature and every soft gradient is a small promise the file makes and the renderer must keep.

+

This document collects those promises in one place. It was written for the people who build PrintCraft, as a single file to open after every change: if all of it still looks right, the change is probably safe. It is also, we hope, pleasant to read on its own.

+

Nothing here is exotic. Each feature is something ordinary documents use every day — an invoice with a form field, a thesis full of equations, a contract with comments in the margin, a brochure with a translucent photograph laid over a tinted panel. What is unusual is only that they are all together, and that each page says plainly what it is testing.

+

The layout is authored in HTML and printed by a browser; the interactive parts are then written into the file by hand, object by object. That split mirrors the real world, where most PDFs are produced by one program and amended by several others.

+
The PrintCraft contributors
+ +

Tracking, from tight to airy

+
+
Measure twice, set once −40
+
Measure twice, set once 0
+
Measure twice, set once +120
+
Measure twice +300
+
+
Forewordii
+
+ + +
+
PrintCraft Showcase1 · Setting Text
+
Chapter one
+

Setting Text

+

Two justified columns with automatic hyphenation, old-style figures, a drop cap and a pull quote — the everyday machinery of a well-made page.

+
+

Long before there were screens, a page of type was a negotiation between the width of a column and the length of the words that had to fill it. The compositor adjusted spaces, divided a word at a sensible syllable, and occasionally rewrote a line so the paragraph would lie flat. Justified text asks for exactly that care, and a renderer must honour the spacing it was given.1

+

A PDF does not reflow. Every glyph has already been placed by the program that wrote the file, and the reader’s job is to reproduce those positions faithfully at any zoom. That sounds simple until one remembers that a single line may mix three fonts, a ligature standing in for two letters, and a hyphen that exists only because the line happened to break there in 1994 or 2026.

+

Old-style figures, like the ones in this paragraph’s 1,280 characters and 36 lines, rise and fall with the lowercase so that numbers do not shout. Ligatures such as fi, fl and ffi in words like officially, affluent and afflicted join letters that would otherwise collide.2

+
“The best typography is the kind you only notice when it is missing.”A working maxim of the trade
+

Hyphenation is where browsers and word processors differ most. Chrome consults the system’s hyphenation dictionaries when a paragraph declares its language and asks for automatic hyphens, which is why the words in these columns occasionally divide across lines. The result is a more even texture, with fewer rivers of white running down the column.

+

Columns introduce their own questions: where to break, whether a heading may sit at the foot of one, and how a quotation spanning both should interrupt the flow. Here the pull quote spans the full measure and the text resumes beneath it, balanced between the two columns.

+

Small text matters too. Notes, captions and legal lines are often set at seven or eight points, where hinting, stem darkening and subpixel positioning decide whether a reader squints. The footnotes below are set at seven and a half points in the same face as the body.3

+

Finally, notice the drop cap that opens this chapter. It is an ordinary letter, floated and enlarged, with the first line set in small capitals. A renderer need do nothing special for it, which is precisely the point: most typographic richness in a PDF is just careful placement of ordinary glyphs.

+
+
+
+

1 Word spacing in PDF is usually baked into glyph positions rather than expressed with the word-spacing operator, so readers rarely need to stretch anything themselves.

+

2 In a PDF a ligature is a single glyph; text extraction relies on the font’s ToUnicode map to recover the letters it represents.

+

3 Try zooming to 400 percent here: well-rendered small text stays crisp and evenly weighted.

+
+
Setting Text1
+
+ + +
+
PrintCraft Showcase2 · OpenType Features
+
Chapter two
+

OpenType Features

+

Each row shows the same text with a feature off, then on. The glyphs differ; the underlying characters do not.

+ + + + + + + + + + + +
TagFeatureOffOn
ligaStandard ligaturesoffice fluffy fjordoffice fluffy fjord
dligDiscretionary ligaturesAsterisk & strictAsterisk & strict
smcpSmall capitalsPrinted in BostonPrinted in Boston
onumOld-style figuresSince 1885: 3,467,920Since 1885: 3,467,920
fracFractions1/2 cup, 3/4 inch, 7/81/2 cup, 3/4 inch, 7/8
swshSwash capitalsThe Quiet RiverThe Quiet River
ss01Stylistic set 1a 1 4 6 9 G ?a 1 4 6 9 G ?
kernKerningAVATAR Type WaveAVATAR Type Wave
tnumTabular figures1111 / 88881111 / 8888
+ +

One variable font, nine weights

+
+
Ag100
+
Ag200
+
Ag300
+
Ag400
+
Ag500
+
Ag600
+
Ag700
+
Ag800
+
Ag900
+
+

System UI · a single variable font file supplies every weight; the PDF embeds each instance as its own subset.

+ +

Script faces

+
Handsome letters, joined in flight
+
Quietly Ornamental — 1234567890
+
OpenType Features2
+
+ + +
+
PrintCraft Showcase3 · Expressive Type
+
Chapter three
+

Expressive Type

+

Effects that become transparency groups, soft masks, clipping paths and transformed text objects once they reach PDF.

+
+
Layered
text-shadow · two translucent offset copies composited under the glyphs
+
Spectrum
background-clip: text · a linear shading clipped to glyph outlines
+
Skewed
Rotated
transform · skewX(−18°) on a filled box and rotate(−12°) on a label
+
Outline
Tracked to 600
-webkit-text-stroke · glyphs stroked, not filled (render mode 1)
+
+ +

Text on a curve

+ + + + + + + + + + + Letters can follow any path the designer draws — up, over, and away + + +

SVG textPath · each glyph is positioned and rotated individually along a cubic Bézier, filled with a gradient.

+ +

Hierarchy through contrast

+
+
Contrast is the
engine of design.
+
Pair a hairline Didot italic with a heavy grotesque and the eye knows where to go. The PDF sees only two fonts and a handful of positioning operators; the hierarchy is entirely in the choices.
+
+
Expressive Type3
+
+ + +
+
PrintCraft Showcase4 · Scripts of the World
+
Chapter four
+

Scripts of the World

+

A short greeting — roughly, “every letter deserves a good home” — in writing systems that need bidirectional layout, contextual shaping, reordering and vertical setting.

+
+
+
Arabic RTL · joining
لكل حرف بيت جميل
Geeza Pro
+
Hebrew RTL
לכל אות יש בית טוב
SF Hebrew
+
Devanagari conjuncts
हर अक्षर का सुंदर घर
Kohinoor Devanagari
+
Thai no spaces
ทุกตัวอักษรมีบ้านที่ดี
Thonburi
+
Greek polytonic
Κάθε γράμμα ἔχει σπίτι
Hoefler Text
+
Cyrillic
У каждой буквы есть дом
Baskerville
+
Chinese simplified
每个字都有一个好家
PingFang SC
+
Korean Hangul
모든 글자에게 좋은 집을
Apple SD Gothic Neo
+
IPA phonetics
/ˈtaɪpˌsɛtɪŋ ɪz ˈkræft/
International Phonetic Alphabet
+
Armenian
Ամեն տառ ունի տուն
SF Armenian
+
Georgian
ყველა ასოს აქვს სახლი
SF Georgian
+
Tamil
ஒவ்வொரு எழுத்துக்கும் வீடு
Tamil Sangam MN
+
+
+
Japanese vertical
+ +
Hiragino Mincho ProN, writing-mode: vertical-rl
+
+
+
+ +

Mixed direction in one line

+

The word كتاب means “book”, and ספר does too — the bidi algorithm keeps each run in its own direction while the sentence stays left-to-right, even across numbers like ٣٤٥ and 345.

+ +

Colour emoji

+
📄🖋️🎨📐🔍🌍🧮✅🦀📎
+

Apple Color Emoji · bitmap (sbix) glyphs that Chrome places into the PDF as images.

+
Scripts of the World4
+
+ + +
+
PrintCraft Showcase5 · Mathematics
+
Chapter five
+

Mathematics

+

Equations written in MathML Core and laid out by the browser: stretchy delimiters, radicals, large operators, scripts and matrices.

+
+
+ x=−b±b2−4ac2a +
Quadratic formula · fraction, radical, superscript
+
+
+ ∫−∞∞e−x2dx=π +
Gaussian integral · large operator with limits
+
+
+ eiπ+1=0 +
Euler’s identity · five constants, one line
+
+
+ ∑n=1∞1n2=π26 +
Basel problem · summation with under- and over-scripts
+
+
+ R(θ)=[cosθ−sinθsinθcosθ] +
Rotation matrix · the transform behind every rotated page
+
+
+ B(t)=∑i=03(3i)(1−t)3−itiPi +
Cubic Bézier · the curve PDF paths are made of
+
+
+ +

Inline mathematics

+

A PDF page maps user space to device space with a matrix [abcdef], so a point (x,y) lands at (ax+cy+e,bx+dy+f). With one point equal to 172 inch, a Letter page measures 612×792 points, and a 300 dpi raster of it holds roughly 8.4×106 pixels.

+ +

Symbols and arrows

+
∀ ∃ ∅ ∇ ∂ ∈ ∉ ⊂ ⊆ ∪ ∩ ≤ ≥ ≠ ≈ ≡ ∞ ℵ ℝ ℂ ℤ ℕ → ⇒ ⇔ ↦ ⟨ ⟩ ⌈ ⌉
+
Mathematics5
+
+ + +
+
PrintCraft Showcase6 · Vector Graphics
+
Chapter six
+

Vector Graphics

+

Nine small plates, each exercising one piece of the PDF imaging model.

+
+
+ +
Axial shading · three-stop linear gradient
+
+
+ +
Radial shading · off-centre focal point
+
+
+
+
Multiply · subtractive overlaps, /BM /Multiply
+
+
+
+
Screen · additive light, /BM /Screen
+
+
+ Clip + +
Clipping path · glyph outlines as a clip
+
+
+ +
Soft mask · luminosity gradient fades artwork
+
+
+ +
Dash patterns · caps, joins and phase
+
+
+ +
Tiling patterns · checks and rotated hatch
+
+
+ + + +
Fill rules · even-odd pentagram, 45% opacity
+
+
+
+ + + + + + + + +

Layered waves · five-stop shading, 50% overlay in multiply mode, dashed stroke over the top.

+
Vector Graphics6
+
+ + +
+
PrintCraft Showcase7 · Data & Tables
+
Chapter seven
+

Data & Tables

+

Charts drawn as SVG and a striped table. All figures are illustrative sample data, not measurements.

+
+
+

Pages per second, by document type

+

Illustrative sample data · higher is better

+ + + + 050100150200 + + + + + + + + + + 189139947043 + + + TextFormsVectorScansMaps + + +
+
+

Renderer conformance over releases

+

Illustrative sample data · % of test pages matching reference

+ + + + 60708090100 + 0.10.20.30.40.5 + + + + + + + + + + + Text 97Vector 92Forms 85 + + +
+ Text + Vector + Forms +
+
+
+ +

Where each PDF feature lives in this file

+ + + + + + + + + + + + + + + +
FeaturePDF objectProduced byPageStatus
Embedded font subsets/FontFile2, /FontFile3ChromeallPresent
Tagged structure/StructTreeRootChromeallPresent
Axial & radial shadings/ShadingType 2, 3Chrome6Present
Blend modes/ExtGState /BMChrome6Present
Bookmarks/OutlinesChrome, xtask—Present
Page labels/PageLabelsxtask—Present
Optional content/OCPropertiesxtask9, 10Two layers
Annotations/Annots with /APxtask1, 9, 10Present
Form fields/AcroFormxtask10Present
File attachments/EmbeddedFilesxtask—Present
Digital signature/FT /Sigxtask10Unsigned
+
Data & Tables7
+
+ + +
+
PrintCraft Showcase8 · Code & Images
+
Chapter eight
+

Code & Images

+

A monospaced listing with syntax colouring, and the raster that it describes.

+
1// Escape-time rendering of the Mandelbrot set into an RGB buffer.
+2fn mandelbrot(width: u32, height: u32) -> Vec<u8> {
+3    let mut rgb = Vec::with_capacity((width * height * 3) as usize);
+4    for py in 0..height {
+5        for px in 0..width {
+6            let c = (-2.2 + 3.0 * px as f64 / width as f64,
+7                     -1.5 + 3.0 * py as f64 / height as f64);
+8            let (mut z, mut n) = ((0.0, 0.0), 0);
+9            while n < MAX_ITER && z.0 * z.0 + z.1 * z.1 <= 4.0 {
+10               z = (z.0 * z.0 - z.1 * z.1 + c.0, 2.0 * z.0 * z.1 + c.1);
+11               n += 1;
+12           }
+13           rgb.extend_from_slice(&palette(n)); // smooth colour ramp
+14       }
+15   }
+16   rgb
+17}
+
+
Procedurally generated Mandelbrot set
+
+

A generated raster

+

The image at left is computed by the xtask each time the showcase is built, encoded as PNG and handed to Chrome, which embeds it as an image XObject. Nothing is downloaded and no third-party artwork is involved, so the file stays reproducible and free to redistribute.

+

At 60 dpi the fractal edge looks smooth; zoom in and the individual pixels appear, a useful check that the viewer switches from filtered to nearest-neighbour sampling at the right moment.

+

Links

+ +
+
+
Code & Images8
+
+ + +
+
PrintCraft Showcase9 · Review & Markup
+
Chapter nine
+

Review & Markup

+

Every annotation on this page was added after printing, each with its own appearance stream, author, date and unique name.

+
+
+

Text markup

+

Reviewers rarely rewrite a document; they highlight the phrase that matters, underline a claim that needs a source, and strike out a redundant word before moving on.

+

A wavy line says check the spelling here, while a caret marks where text should  be inserted.

+

Comments

+

Sticky notes carry threaded replies, and a free-text box writes directly on the page. The paperclip holds an attached file that can be saved from the comment itself.

+

Legend

+
    +
  1. Highlight
  2. Underline
  3. StrikeOut
  4. Squiggly
  5. Caret
  6. Text + Popup
  7. FreeText
  8. Line (arrow)
  9. Square
  10. Circle
  11. Polygon
  12. PolyLine
  13. Ink
  14. Stamp
  15. FileAttachment
  16. Link
  17. +
+
+
+ Review canvas + . + . + . + . + . + . + . + . + . + . +
+
+
Review & Markup9
+
+ + + diff --git a/assets/fonts/Inter-Medium.ttf b/assets/fonts/Inter-Medium.ttf new file mode 100644 index 00000000..458cd060 Binary files /dev/null and b/assets/fonts/Inter-Medium.ttf differ diff --git a/assets/fonts/Inter-Regular.ttf b/assets/fonts/Inter-Regular.ttf new file mode 100644 index 00000000..b7aaca8d Binary files /dev/null and b/assets/fonts/Inter-Regular.ttf differ diff --git a/assets/fonts/Inter-SemiBold.ttf b/assets/fonts/Inter-SemiBold.ttf new file mode 100644 index 00000000..47f8ab1d Binary files /dev/null and b/assets/fonts/Inter-SemiBold.ttf differ diff --git a/assets/fonts/JetBrainsMono-Regular.ttf b/assets/fonts/JetBrainsMono-Regular.ttf new file mode 100644 index 00000000..711830ed Binary files /dev/null and b/assets/fonts/JetBrainsMono-Regular.ttf differ diff --git a/assets/fonts/OFL-Inter.txt b/assets/fonts/OFL-Inter.txt new file mode 100644 index 00000000..9b2ca37b --- /dev/null +++ b/assets/fonts/OFL-Inter.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/OFL-JetBrainsMono.txt b/assets/fonts/OFL-JetBrainsMono.txt new file mode 100644 index 00000000..5ceee002 --- /dev/null +++ b/assets/fonts/OFL-JetBrainsMono.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/icons/LICENSE-lucide.txt b/assets/icons/LICENSE-lucide.txt new file mode 100644 index 00000000..718bb3f0 --- /dev/null +++ b/assets/icons/LICENSE-lucide.txt @@ -0,0 +1,43 @@ +ISC License + +Copyright (c) 2026 Lucide Icons and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +--- + +The following Lucide icons are derived from the Feather project: + +airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out + +The MIT License (MIT) (for the icons listed above) + +Copyright (c) 2013-present Cole Bemis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/assets/icons/accessibility.svg b/assets/icons/accessibility.svg new file mode 100644 index 00000000..9a87cb59 --- /dev/null +++ b/assets/icons/accessibility.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/assets/icons/align-left.svg b/assets/icons/align-left.svg new file mode 100644 index 00000000..b5b70eeb --- /dev/null +++ b/assets/icons/align-left.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/app-window.svg b/assets/icons/app-window.svg new file mode 100644 index 00000000..815548bc --- /dev/null +++ b/assets/icons/app-window.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/assets/icons/arrow-left-right.svg b/assets/icons/arrow-left-right.svg new file mode 100644 index 00000000..acf2cb39 --- /dev/null +++ b/assets/icons/arrow-left-right.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/assets/icons/arrow-up-down.svg b/assets/icons/arrow-up-down.svg new file mode 100644 index 00000000..f58abde3 --- /dev/null +++ b/assets/icons/arrow-up-down.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/arrow-up-right.svg b/assets/icons/arrow-up-right.svg new file mode 100644 index 00000000..8a669e29 --- /dev/null +++ b/assets/icons/arrow-up-right.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/badge-check.svg b/assets/icons/badge-check.svg new file mode 100644 index 00000000..24153bb4 --- /dev/null +++ b/assets/icons/badge-check.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/bandage.svg b/assets/icons/bandage.svg new file mode 100644 index 00000000..8241041a --- /dev/null +++ b/assets/icons/bandage.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/assets/icons/bell.svg b/assets/icons/bell.svg new file mode 100644 index 00000000..17d78790 --- /dev/null +++ b/assets/icons/bell.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/blend.svg b/assets/icons/blend.svg new file mode 100644 index 00000000..3ee5832c --- /dev/null +++ b/assets/icons/blend.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/bold.svg b/assets/icons/bold.svg new file mode 100644 index 00000000..8bc826be --- /dev/null +++ b/assets/icons/bold.svg @@ -0,0 +1,15 @@ + + + + diff --git a/assets/icons/book-open.svg b/assets/icons/book-open.svg new file mode 100644 index 00000000..36b690da --- /dev/null +++ b/assets/icons/book-open.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/bookmark.svg b/assets/icons/bookmark.svg new file mode 100644 index 00000000..81d52c2e --- /dev/null +++ b/assets/icons/bookmark.svg @@ -0,0 +1,15 @@ + + + + diff --git a/assets/icons/braces.svg b/assets/icons/braces.svg new file mode 100644 index 00000000..f195bd8a --- /dev/null +++ b/assets/icons/braces.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/brush-cleaning.svg b/assets/icons/brush-cleaning.svg new file mode 100644 index 00000000..68885262 --- /dev/null +++ b/assets/icons/brush-cleaning.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/assets/icons/brush.svg b/assets/icons/brush.svg new file mode 100644 index 00000000..772cc289 --- /dev/null +++ b/assets/icons/brush.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/assets/icons/camera.svg b/assets/icons/camera.svg new file mode 100644 index 00000000..442ce616 --- /dev/null +++ b/assets/icons/camera.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/check-circle-2.svg b/assets/icons/check-circle-2.svg new file mode 100644 index 00000000..c5e433ef --- /dev/null +++ b/assets/icons/check-circle-2.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/check.svg b/assets/icons/check.svg new file mode 100644 index 00000000..5abe1527 --- /dev/null +++ b/assets/icons/check.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/chevron-down.svg b/assets/icons/chevron-down.svg new file mode 100644 index 00000000..e70d30b8 --- /dev/null +++ b/assets/icons/chevron-down.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/chevron-left.svg b/assets/icons/chevron-left.svg new file mode 100644 index 00000000..161890f2 --- /dev/null +++ b/assets/icons/chevron-left.svg @@ -0,0 +1,15 @@ + + + + diff --git a/assets/icons/chevron-right.svg b/assets/icons/chevron-right.svg new file mode 100644 index 00000000..db62a0c2 --- /dev/null +++ b/assets/icons/chevron-right.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/chevron-up.svg b/assets/icons/chevron-up.svg new file mode 100644 index 00000000..33f722d3 --- /dev/null +++ b/assets/icons/chevron-up.svg @@ -0,0 +1,15 @@ + + + + diff --git a/assets/icons/chevrons-left.svg b/assets/icons/chevrons-left.svg new file mode 100644 index 00000000..94b37487 --- /dev/null +++ b/assets/icons/chevrons-left.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/chevrons-right.svg b/assets/icons/chevrons-right.svg new file mode 100644 index 00000000..0d2d8ec9 --- /dev/null +++ b/assets/icons/chevrons-right.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/circle-check.svg b/assets/icons/circle-check.svg new file mode 100644 index 00000000..f4e16b0a --- /dev/null +++ b/assets/icons/circle-check.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/circle-dashed.svg b/assets/icons/circle-dashed.svg new file mode 100644 index 00000000..4ae53eed --- /dev/null +++ b/assets/icons/circle-dashed.svg @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/assets/icons/circle-dot.svg b/assets/icons/circle-dot.svg new file mode 100644 index 00000000..880029db --- /dev/null +++ b/assets/icons/circle-dot.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/circle-help.svg b/assets/icons/circle-help.svg new file mode 100644 index 00000000..308057b4 --- /dev/null +++ b/assets/icons/circle-help.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/circle.svg b/assets/icons/circle.svg new file mode 100644 index 00000000..4e464d6f --- /dev/null +++ b/assets/icons/circle.svg @@ -0,0 +1,15 @@ + + + + diff --git a/assets/icons/clock-3.svg b/assets/icons/clock-3.svg new file mode 100644 index 00000000..66e227fe --- /dev/null +++ b/assets/icons/clock-3.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/clock.svg b/assets/icons/clock.svg new file mode 100644 index 00000000..b6ae2834 --- /dev/null +++ b/assets/icons/clock.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/cloud-upload.svg b/assets/icons/cloud-upload.svg new file mode 100644 index 00000000..0bc36875 --- /dev/null +++ b/assets/icons/cloud-upload.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/cloud.svg b/assets/icons/cloud.svg new file mode 100644 index 00000000..14b4e191 --- /dev/null +++ b/assets/icons/cloud.svg @@ -0,0 +1,15 @@ + + + + diff --git a/assets/icons/columns-2.svg b/assets/icons/columns-2.svg new file mode 100644 index 00000000..4f0a43f6 --- /dev/null +++ b/assets/icons/columns-2.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/compass.svg b/assets/icons/compass.svg new file mode 100644 index 00000000..1a4e8c7d --- /dev/null +++ b/assets/icons/compass.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/contrast.svg b/assets/icons/contrast.svg new file mode 100644 index 00000000..334f5281 --- /dev/null +++ b/assets/icons/contrast.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/copy-plus.svg b/assets/icons/copy-plus.svg new file mode 100644 index 00000000..ae5aafb5 --- /dev/null +++ b/assets/icons/copy-plus.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg new file mode 100644 index 00000000..f62ce99c --- /dev/null +++ b/assets/icons/copy.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/crop.svg b/assets/icons/crop.svg new file mode 100644 index 00000000..4ca1e6fa --- /dev/null +++ b/assets/icons/crop.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/droplet.svg b/assets/icons/droplet.svg new file mode 100644 index 00000000..36aa334f --- /dev/null +++ b/assets/icons/droplet.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/ellipsis-vertical.svg b/assets/icons/ellipsis-vertical.svg new file mode 100644 index 00000000..785b6c63 --- /dev/null +++ b/assets/icons/ellipsis-vertical.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/ellipsis.svg b/assets/icons/ellipsis.svg new file mode 100644 index 00000000..68456c39 --- /dev/null +++ b/assets/icons/ellipsis.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/assets/icons/eraser.svg b/assets/icons/eraser.svg new file mode 100644 index 00000000..6abb6fdd --- /dev/null +++ b/assets/icons/eraser.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/eye-off.svg b/assets/icons/eye-off.svg new file mode 100644 index 00000000..0083aaa8 --- /dev/null +++ b/assets/icons/eye-off.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg new file mode 100644 index 00000000..e5e78706 --- /dev/null +++ b/assets/icons/eye.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/file-check.svg b/assets/icons/file-check.svg new file mode 100644 index 00000000..d597cf9e --- /dev/null +++ b/assets/icons/file-check.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/file-down.svg b/assets/icons/file-down.svg new file mode 100644 index 00000000..21f36af8 --- /dev/null +++ b/assets/icons/file-down.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/file-input.svg b/assets/icons/file-input.svg new file mode 100644 index 00000000..b644fd3e --- /dev/null +++ b/assets/icons/file-input.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/file-lock-2.svg b/assets/icons/file-lock-2.svg new file mode 100644 index 00000000..8b093550 --- /dev/null +++ b/assets/icons/file-lock-2.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/file-minus.svg b/assets/icons/file-minus.svg new file mode 100644 index 00000000..b2b554c0 --- /dev/null +++ b/assets/icons/file-minus.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/file-output.svg b/assets/icons/file-output.svg new file mode 100644 index 00000000..4232cd41 --- /dev/null +++ b/assets/icons/file-output.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/file-pen-line.svg b/assets/icons/file-pen-line.svg new file mode 100644 index 00000000..f8c789d5 --- /dev/null +++ b/assets/icons/file-pen-line.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/file-plus-2.svg b/assets/icons/file-plus-2.svg new file mode 100644 index 00000000..14ed0fea --- /dev/null +++ b/assets/icons/file-plus-2.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/file-plus.svg b/assets/icons/file-plus.svg new file mode 100644 index 00000000..fe4715be --- /dev/null +++ b/assets/icons/file-plus.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/assets/icons/file-scan.svg b/assets/icons/file-scan.svg new file mode 100644 index 00000000..aedddafd --- /dev/null +++ b/assets/icons/file-scan.svg @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/assets/icons/file-search.svg b/assets/icons/file-search.svg new file mode 100644 index 00000000..e03262e3 --- /dev/null +++ b/assets/icons/file-search.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/file-symlink.svg b/assets/icons/file-symlink.svg new file mode 100644 index 00000000..fe49d39a --- /dev/null +++ b/assets/icons/file-symlink.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/file-text.svg b/assets/icons/file-text.svg new file mode 100644 index 00000000..d9abeb7b --- /dev/null +++ b/assets/icons/file-text.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/assets/icons/file-x.svg b/assets/icons/file-x.svg new file mode 100644 index 00000000..9dcd134e --- /dev/null +++ b/assets/icons/file-x.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/files.svg b/assets/icons/files.svg new file mode 100644 index 00000000..d211e156 --- /dev/null +++ b/assets/icons/files.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/film.svg b/assets/icons/film.svg new file mode 100644 index 00000000..b54b6f70 --- /dev/null +++ b/assets/icons/film.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/assets/icons/filter.svg b/assets/icons/filter.svg new file mode 100644 index 00000000..a8decfc1 --- /dev/null +++ b/assets/icons/filter.svg @@ -0,0 +1,15 @@ + + + + diff --git a/assets/icons/folder-open.svg b/assets/icons/folder-open.svg new file mode 100644 index 00000000..b01f60fc --- /dev/null +++ b/assets/icons/folder-open.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/folder-plus.svg b/assets/icons/folder-plus.svg new file mode 100644 index 00000000..661ebf4d --- /dev/null +++ b/assets/icons/folder-plus.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/assets/icons/folder.svg b/assets/icons/folder.svg new file mode 100644 index 00000000..ef69b345 --- /dev/null +++ b/assets/icons/folder.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/gauge.svg b/assets/icons/gauge.svg new file mode 100644 index 00000000..340b9327 --- /dev/null +++ b/assets/icons/gauge.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/git-compare.svg b/assets/icons/git-compare.svg new file mode 100644 index 00000000..136a1e11 --- /dev/null +++ b/assets/icons/git-compare.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/grid-3x3.svg b/assets/icons/grid-3x3.svg new file mode 100644 index 00000000..6c714352 --- /dev/null +++ b/assets/icons/grid-3x3.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/assets/icons/hand.svg b/assets/icons/hand.svg new file mode 100644 index 00000000..30af9046 --- /dev/null +++ b/assets/icons/hand.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/assets/icons/hash.svg b/assets/icons/hash.svg new file mode 100644 index 00000000..6a859ad2 --- /dev/null +++ b/assets/icons/hash.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/heading.svg b/assets/icons/heading.svg new file mode 100644 index 00000000..7f17662c --- /dev/null +++ b/assets/icons/heading.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/highlighter.svg b/assets/icons/highlighter.svg new file mode 100644 index 00000000..a9976190 --- /dev/null +++ b/assets/icons/highlighter.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/house.svg b/assets/icons/house.svg new file mode 100644 index 00000000..bbe4a2c4 --- /dev/null +++ b/assets/icons/house.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/image-plus.svg b/assets/icons/image-plus.svg new file mode 100644 index 00000000..67cf6ee7 --- /dev/null +++ b/assets/icons/image-plus.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/assets/icons/image.svg b/assets/icons/image.svg new file mode 100644 index 00000000..3241134c --- /dev/null +++ b/assets/icons/image.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/assets/icons/info.svg b/assets/icons/info.svg new file mode 100644 index 00000000..2a46eac2 --- /dev/null +++ b/assets/icons/info.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/assets/icons/italic.svg b/assets/icons/italic.svg new file mode 100644 index 00000000..023e1656 --- /dev/null +++ b/assets/icons/italic.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/languages.svg b/assets/icons/languages.svg new file mode 100644 index 00000000..33c4953d --- /dev/null +++ b/assets/icons/languages.svg @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/assets/icons/lasso.svg b/assets/icons/lasso.svg new file mode 100644 index 00000000..f2c6d7f8 --- /dev/null +++ b/assets/icons/lasso.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/assets/icons/layers.svg b/assets/icons/layers.svg new file mode 100644 index 00000000..a3b69ae3 --- /dev/null +++ b/assets/icons/layers.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/assets/icons/layout-grid.svg b/assets/icons/layout-grid.svg new file mode 100644 index 00000000..950226b6 --- /dev/null +++ b/assets/icons/layout-grid.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/link-2.svg b/assets/icons/link-2.svg new file mode 100644 index 00000000..a8833113 --- /dev/null +++ b/assets/icons/link-2.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/link.svg b/assets/icons/link.svg new file mode 100644 index 00000000..645e746a --- /dev/null +++ b/assets/icons/link.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/list-checks.svg b/assets/icons/list-checks.svg new file mode 100644 index 00000000..fb7de274 --- /dev/null +++ b/assets/icons/list-checks.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/assets/icons/list-ordered.svg b/assets/icons/list-ordered.svg new file mode 100644 index 00000000..a25d5c33 --- /dev/null +++ b/assets/icons/list-ordered.svg @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/assets/icons/list.svg b/assets/icons/list.svg new file mode 100644 index 00000000..a9885faf --- /dev/null +++ b/assets/icons/list.svg @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/assets/icons/lock-open.svg b/assets/icons/lock-open.svg new file mode 100644 index 00000000..b2367128 --- /dev/null +++ b/assets/icons/lock-open.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/lock.svg b/assets/icons/lock.svg new file mode 100644 index 00000000..b2b32d12 --- /dev/null +++ b/assets/icons/lock.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/maximize-2.svg b/assets/icons/maximize-2.svg new file mode 100644 index 00000000..790d2538 --- /dev/null +++ b/assets/icons/maximize-2.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg new file mode 100644 index 00000000..eb5beff0 --- /dev/null +++ b/assets/icons/maximize.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/message-circle-reply.svg b/assets/icons/message-circle-reply.svg new file mode 100644 index 00000000..f5aa0f43 --- /dev/null +++ b/assets/icons/message-circle-reply.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/message-square-text.svg b/assets/icons/message-square-text.svg new file mode 100644 index 00000000..02676efa --- /dev/null +++ b/assets/icons/message-square-text.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/message-square.svg b/assets/icons/message-square.svg new file mode 100644 index 00000000..5044bc7e --- /dev/null +++ b/assets/icons/message-square.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/mic.svg b/assets/icons/mic.svg new file mode 100644 index 00000000..6b85c529 --- /dev/null +++ b/assets/icons/mic.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg new file mode 100644 index 00000000..4e32ebfe --- /dev/null +++ b/assets/icons/minimize.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/minus.svg b/assets/icons/minus.svg new file mode 100644 index 00000000..75093e27 --- /dev/null +++ b/assets/icons/minus.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/moon.svg b/assets/icons/moon.svg new file mode 100644 index 00000000..3c4bc311 --- /dev/null +++ b/assets/icons/moon.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/mouse-pointer-2.svg b/assets/icons/mouse-pointer-2.svg new file mode 100644 index 00000000..b881f795 --- /dev/null +++ b/assets/icons/mouse-pointer-2.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/move.svg b/assets/icons/move.svg new file mode 100644 index 00000000..d78876f7 --- /dev/null +++ b/assets/icons/move.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/assets/icons/navigation.svg b/assets/icons/navigation.svg new file mode 100644 index 00000000..b2be022d --- /dev/null +++ b/assets/icons/navigation.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/paint-bucket.svg b/assets/icons/paint-bucket.svg new file mode 100644 index 00000000..f7997334 --- /dev/null +++ b/assets/icons/paint-bucket.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/assets/icons/palette.svg b/assets/icons/palette.svg new file mode 100644 index 00000000..25fb773a --- /dev/null +++ b/assets/icons/palette.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/assets/icons/panel-left.svg b/assets/icons/panel-left.svg new file mode 100644 index 00000000..5c4fc652 --- /dev/null +++ b/assets/icons/panel-left.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/panel-right.svg b/assets/icons/panel-right.svg new file mode 100644 index 00000000..53ba1e61 --- /dev/null +++ b/assets/icons/panel-right.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/panels-top-left.svg b/assets/icons/panels-top-left.svg new file mode 100644 index 00000000..574e4dea --- /dev/null +++ b/assets/icons/panels-top-left.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/assets/icons/paperclip.svg b/assets/icons/paperclip.svg new file mode 100644 index 00000000..aeb2bc1b --- /dev/null +++ b/assets/icons/paperclip.svg @@ -0,0 +1,15 @@ + + + + diff --git a/assets/icons/pen-line.svg b/assets/icons/pen-line.svg new file mode 100644 index 00000000..4d635533 --- /dev/null +++ b/assets/icons/pen-line.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/pen-tool.svg b/assets/icons/pen-tool.svg new file mode 100644 index 00000000..738c6fec --- /dev/null +++ b/assets/icons/pen-tool.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/assets/icons/pencil.svg b/assets/icons/pencil.svg new file mode 100644 index 00000000..10445cac --- /dev/null +++ b/assets/icons/pencil.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/pipette.svg b/assets/icons/pipette.svg new file mode 100644 index 00000000..2a5e1363 --- /dev/null +++ b/assets/icons/pipette.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg new file mode 100644 index 00000000..41f92bb9 --- /dev/null +++ b/assets/icons/plus.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/presentation.svg b/assets/icons/presentation.svg new file mode 100644 index 00000000..d00bbe36 --- /dev/null +++ b/assets/icons/presentation.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/printer.svg b/assets/icons/printer.svg new file mode 100644 index 00000000..25a37798 --- /dev/null +++ b/assets/icons/printer.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/rectangle-horizontal.svg b/assets/icons/rectangle-horizontal.svg new file mode 100644 index 00000000..dbc0f87a --- /dev/null +++ b/assets/icons/rectangle-horizontal.svg @@ -0,0 +1,15 @@ + + + + diff --git a/assets/icons/redo-2.svg b/assets/icons/redo-2.svg new file mode 100644 index 00000000..72371c8d --- /dev/null +++ b/assets/icons/redo-2.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/replace.svg b/assets/icons/replace.svg new file mode 100644 index 00000000..e9e8447b --- /dev/null +++ b/assets/icons/replace.svg @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/assets/icons/rotate-ccw.svg b/assets/icons/rotate-ccw.svg new file mode 100644 index 00000000..99a2e273 --- /dev/null +++ b/assets/icons/rotate-ccw.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/rotate-cw.svg b/assets/icons/rotate-cw.svg new file mode 100644 index 00000000..f5f750d4 --- /dev/null +++ b/assets/icons/rotate-cw.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/ruler.svg b/assets/icons/ruler.svg new file mode 100644 index 00000000..573ee24f --- /dev/null +++ b/assets/icons/ruler.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/assets/icons/save.svg b/assets/icons/save.svg new file mode 100644 index 00000000..15bdf54c --- /dev/null +++ b/assets/icons/save.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/scan-text.svg b/assets/icons/scan-text.svg new file mode 100644 index 00000000..7db9543e --- /dev/null +++ b/assets/icons/scan-text.svg @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/assets/icons/scan.svg b/assets/icons/scan.svg new file mode 100644 index 00000000..33d067b2 --- /dev/null +++ b/assets/icons/scan.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/assets/icons/scissors.svg b/assets/icons/scissors.svg new file mode 100644 index 00000000..f87bf20b --- /dev/null +++ b/assets/icons/scissors.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/assets/icons/search.svg b/assets/icons/search.svg new file mode 100644 index 00000000..03e442a5 --- /dev/null +++ b/assets/icons/search.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/send.svg b/assets/icons/send.svg new file mode 100644 index 00000000..65451e5c --- /dev/null +++ b/assets/icons/send.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/settings-2.svg b/assets/icons/settings-2.svg new file mode 100644 index 00000000..513d5da1 --- /dev/null +++ b/assets/icons/settings-2.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/assets/icons/settings.svg b/assets/icons/settings.svg new file mode 100644 index 00000000..4c6263af --- /dev/null +++ b/assets/icons/settings.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/share-2.svg b/assets/icons/share-2.svg new file mode 100644 index 00000000..c57325fa --- /dev/null +++ b/assets/icons/share-2.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/assets/icons/shield-check.svg b/assets/icons/shield-check.svg new file mode 100644 index 00000000..01623f00 --- /dev/null +++ b/assets/icons/shield-check.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/signature.svg b/assets/icons/signature.svg new file mode 100644 index 00000000..8f4b3b23 --- /dev/null +++ b/assets/icons/signature.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/sliders-horizontal.svg b/assets/icons/sliders-horizontal.svg new file mode 100644 index 00000000..819f80c1 --- /dev/null +++ b/assets/icons/sliders-horizontal.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/assets/icons/sparkles.svg b/assets/icons/sparkles.svg new file mode 100644 index 00000000..2d749398 --- /dev/null +++ b/assets/icons/sparkles.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/assets/icons/spline.svg b/assets/icons/spline.svg new file mode 100644 index 00000000..ca24df5e --- /dev/null +++ b/assets/icons/spline.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/square-dashed-mouse-pointer.svg b/assets/icons/square-dashed-mouse-pointer.svg new file mode 100644 index 00000000..10c1dc2f --- /dev/null +++ b/assets/icons/square-dashed-mouse-pointer.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/assets/icons/square-dashed.svg b/assets/icons/square-dashed.svg new file mode 100644 index 00000000..f0822a34 --- /dev/null +++ b/assets/icons/square-dashed.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/square.svg b/assets/icons/square.svg new file mode 100644 index 00000000..78627335 --- /dev/null +++ b/assets/icons/square.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/squares-subtract.svg b/assets/icons/squares-subtract.svg new file mode 100644 index 00000000..8dbd9db1 --- /dev/null +++ b/assets/icons/squares-subtract.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/assets/icons/stamp.svg b/assets/icons/stamp.svg new file mode 100644 index 00000000..5eb5c081 --- /dev/null +++ b/assets/icons/stamp.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/assets/icons/star.svg b/assets/icons/star.svg new file mode 100644 index 00000000..3502ccca --- /dev/null +++ b/assets/icons/star.svg @@ -0,0 +1,15 @@ + + + + diff --git a/assets/icons/sticky-note.svg b/assets/icons/sticky-note.svg new file mode 100644 index 00000000..0fb87bdc --- /dev/null +++ b/assets/icons/sticky-note.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/strikethrough.svg b/assets/icons/strikethrough.svg new file mode 100644 index 00000000..ece40840 --- /dev/null +++ b/assets/icons/strikethrough.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/sun.svg b/assets/icons/sun.svg new file mode 100644 index 00000000..3ecadd68 --- /dev/null +++ b/assets/icons/sun.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/assets/icons/swatch-book.svg b/assets/icons/swatch-book.svg new file mode 100644 index 00000000..5daba1ed --- /dev/null +++ b/assets/icons/swatch-book.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/assets/icons/tag.svg b/assets/icons/tag.svg new file mode 100644 index 00000000..97494d7e --- /dev/null +++ b/assets/icons/tag.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/text-cursor-input.svg b/assets/icons/text-cursor-input.svg new file mode 100644 index 00000000..9e77e6d5 --- /dev/null +++ b/assets/icons/text-cursor-input.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/assets/icons/text-select.svg b/assets/icons/text-select.svg new file mode 100644 index 00000000..2c869ef3 --- /dev/null +++ b/assets/icons/text-select.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/trash-2.svg b/assets/icons/trash-2.svg new file mode 100644 index 00000000..9685b29e --- /dev/null +++ b/assets/icons/trash-2.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/assets/icons/trash.svg b/assets/icons/trash.svg new file mode 100644 index 00000000..e8cb0edf --- /dev/null +++ b/assets/icons/trash.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/assets/icons/triangle-alert.svg b/assets/icons/triangle-alert.svg new file mode 100644 index 00000000..dd1636ce --- /dev/null +++ b/assets/icons/triangle-alert.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/assets/icons/type.svg b/assets/icons/type.svg new file mode 100644 index 00000000..8f269e72 --- /dev/null +++ b/assets/icons/type.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/assets/icons/underline.svg b/assets/icons/underline.svg new file mode 100644 index 00000000..56511f08 --- /dev/null +++ b/assets/icons/underline.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/undo-2.svg b/assets/icons/undo-2.svg new file mode 100644 index 00000000..3e93594c --- /dev/null +++ b/assets/icons/undo-2.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/user-round.svg b/assets/icons/user-round.svg new file mode 100644 index 00000000..bfa190f1 --- /dev/null +++ b/assets/icons/user-round.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/assets/icons/wand-sparkles.svg b/assets/icons/wand-sparkles.svg new file mode 100644 index 00000000..fd60bf0e --- /dev/null +++ b/assets/icons/wand-sparkles.svg @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/assets/icons/wrench.svg b/assets/icons/wrench.svg new file mode 100644 index 00000000..43fb0bfd --- /dev/null +++ b/assets/icons/wrench.svg @@ -0,0 +1,15 @@ + + + + diff --git a/assets/icons/x.svg b/assets/icons/x.svg new file mode 100644 index 00000000..e3179434 --- /dev/null +++ b/assets/icons/x.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/zoom-in.svg b/assets/icons/zoom-in.svg new file mode 100644 index 00000000..11173c0e --- /dev/null +++ b/assets/icons/zoom-in.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/assets/icons/zoom-out.svg b/assets/icons/zoom-out.svg new file mode 100644 index 00000000..c9b8a58d --- /dev/null +++ b/assets/icons/zoom-out.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml new file mode 100644 index 00000000..ebeff37d --- /dev/null +++ b/crates/engine/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "printcraft-engine" +description = "PrintCraft engine façade: session, documents, command/tool catalogue" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +printcraft-render = { workspace = true } diff --git a/crates/engine/src/catalog.rs b/crates/engine/src/catalog.rs new file mode 100644 index 00000000..da58ad2a --- /dev/null +++ b/crates/engine/src/catalog.rs @@ -0,0 +1,512 @@ +//! The tool catalogue: Acrobat's "All tools" information architecture, as data. +//! +//! Every UI (the egui shell today, anything tomorrow) builds its tool panel, palette and menus +//! from this table; nothing is hard-coded in the toolkit. Each item names the command it will +//! dispatch and the milestone (plan/execution-plan.md) in which it ships, so the UI can be honest +//! about what works today. +//! +//! Source of the inventory: plan/acrobat/02-ui-ux.md §2 (observed in Acrobat Pro 26.002). + +/// Where a tool or item stands. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Availability { + /// Works in this build. + Ready, + /// Planned; ships in the named milestone. + Planned(&'static str), + /// Cloud-only in Acrobat; PrintCraft offers an optional pluggable provider instead. + Provider, +} + +#[derive(Clone, Copy, Debug)] +pub struct ToolItem { + pub label: &'static str, + pub icon: &'static str, + pub command: &'static str, + pub availability: Availability, +} + +#[derive(Clone, Copy, Debug)] +pub struct ToolSection { + pub title: &'static str, + pub items: &'static [ToolItem], +} + +#[derive(Clone, Copy, Debug)] +pub struct ToolGroup { + pub id: &'static str, + pub label: &'static str, + pub icon: &'static str, + /// Icon tint, as Acrobat colour-codes its tool families. + pub hue: [u8; 3], + pub badge: Option<&'static str>, + pub availability: Availability, + pub sections: &'static [ToolSection], +} + +const fn item(label: &'static str, icon: &'static str, command: &'static str, availability: Availability) -> ToolItem { + ToolItem { label, icon, command, availability } +} + +use Availability::{Planned, Provider, Ready}; + +const RED: [u8; 3] = [0xE0, 0x3E, 0x3E]; +const PURPLE: [u8; 3] = [0x8E, 0x4E, 0xE6]; +const BLUE: [u8; 3] = [0x3A, 0x6F, 0xE8]; +const GREEN: [u8; 3] = [0x2D, 0x9D, 0x5B]; +const ORANGE: [u8; 3] = [0xE8, 0x8A, 0x1A]; +const TEAL: [u8; 3] = [0x14, 0x9C, 0xA8]; +const PINK: [u8; 3] = [0xD6, 0x3B, 0x8F]; + +pub static TOOL_GROUPS: &[ToolGroup] = &[ + ToolGroup { + id: "export", + label: "Export a PDF", + icon: "file-output", + hue: BLUE, + badge: None, + availability: Planned("M10"), + sections: &[ToolSection { + title: "Export to", + items: &[ + item("Microsoft Word (.docx)", "file-text", "export.docx", Planned("M10")), + item("Spreadsheet (.xlsx)", "grid-3x3", "export.xlsx", Planned("M10")), + item("Presentation (.pptx)", "presentation", "export.pptx", Planned("M10")), + item("Image (PNG, JPEG, TIFF)", "image", "export.image", Planned("M10")), + item("HTML web page", "file-symlink", "export.html", Planned("M10")), + item("Text (plain / accessible)", "type", "export.text", Planned("M10")), + item("PostScript / EPS", "file-down", "export.ps", Planned("M10")), + ], + }], + }, + ToolGroup { + id: "fill_sign", + label: "Fill & Sign", + icon: "pen-line", + hue: PURPLE, + badge: None, + availability: Planned("M5"), + sections: &[ToolSection { + title: "Fill", + items: &[ + item("Add text", "type", "sign.fill.text", Planned("M5")), + item("Checkmark", "check", "sign.fill.check", Planned("M5")), + item("Cross", "x", "sign.fill.cross", Planned("M5")), + item("Date", "clock-3", "sign.fill.date", Planned("M5")), + item("Signature", "signature", "sign.fill.signature", Planned("M5")), + ], + }], + }, + ToolGroup { + id: "edit", + label: "Edit a PDF", + icon: "file-pen-line", + hue: PINK, + badge: None, + availability: Planned("M7"), + sections: &[ + ToolSection { + title: "Modify page", + items: &[ + item("Rotate pages", "rotate-cw", "page.rotate", Planned("M4")), + item("Insert pages", "file-plus-2", "page.insert", Planned("M4")), + item("Delete pages", "trash-2", "page.delete", Planned("M4")), + item("Extract pages", "file-output", "page.extract", Planned("M4")), + item("Organize pages", "layout-grid", "page.organize", Ready), + ], + }, + ToolSection { + title: "Add content", + items: &[ + item("Text", "type", "edit.text", Planned("M7")), + item("Image", "image-plus", "edit.image", Planned("M7")), + item("Header and footer", "heading", "edit.header_footer", Planned("M7")), + item("Watermark", "stamp", "edit.watermark", Planned("M7")), + item("Link", "link-2", "edit.link", Planned("M7")), + item("Bates numbering", "hash", "edit.bates", Planned("M7")), + item("Background", "palette", "edit.background", Planned("M7")), + item("Attach file", "paperclip", "edit.attach", Planned("M12")), + ], + }, + ], + }, + ToolGroup { + id: "create", + label: "Create a PDF", + icon: "file-plus-2", + hue: RED, + badge: None, + availability: Planned("M10"), + sections: &[ToolSection { + title: "Create from", + items: &[ + item("Single file", "file-input", "create.file", Planned("M10")), + item("Multiple files", "files", "create.multiple", Planned("M10")), + item("Images", "image", "create.images", Planned("M10")), + item("Clipboard", "copy-plus", "create.clipboard", Planned("M10")), + item("Blank page", "file-plus-2", "create.blank", Planned("M10")), + ], + }], + }, + ToolGroup { + id: "combine", + label: "Combine files", + icon: "files", + hue: BLUE, + badge: None, + availability: Planned("M4"), + sections: &[ToolSection { title: "Combine", items: &[item("Add files to combine", "files", "page.combine", Planned("M4"))] }], + }, + ToolGroup { + id: "organize", + label: "Organize pages", + icon: "layout-grid", + hue: GREEN, + badge: None, + availability: Ready, + sections: &[ + ToolSection { + title: "Page options", + items: &[ + item("Page grid", "layout-grid", "page.organize", Ready), + item("Rotate", "rotate-cw", "page.rotate", Planned("M4")), + item("Delete", "trash-2", "page.delete", Planned("M4")), + item("Extract", "file-output", "page.extract", Planned("M4")), + item("Insert", "file-plus-2", "page.insert", Planned("M4")), + item("Replace", "replace", "page.replace", Planned("M4")), + item("Split", "scissors", "page.split", Planned("M4")), + ], + }, + ToolSection { + title: "More", + items: &[ + item("Set page boxes", "square-dashed-mouse-pointer", "page.boxes", Planned("M4")), + item("Page labels", "tag", "page.labels", Planned("M4")), + item("Page transitions", "presentation", "page.transitions", Planned("M4")), + ], + }, + ], + }, + ToolGroup { + id: "comment", + label: "Add comments", + icon: "message-square-text", + hue: ORANGE, + badge: None, + availability: Planned("M5"), + sections: &[ + ToolSection { + title: "Markup", + items: &[ + item("Sticky note", "sticky-note", "comment.note", Planned("M5")), + item("Highlight text", "highlighter", "comment.highlight", Planned("M5")), + item("Underline", "underline", "comment.underline", Planned("M5")), + item("Strikethrough", "strikethrough", "comment.strikeout", Planned("M5")), + item("Text box", "type", "comment.freetext", Planned("M5")), + ], + }, + ToolSection { + title: "Drawing", + items: &[ + item("Draw freehand", "pencil", "comment.ink", Planned("M5")), + item("Rectangle", "square", "comment.square", Planned("M5")), + item("Oval", "circle", "comment.circle", Planned("M5")), + item("Line / arrow", "arrow-up-right", "comment.line", Planned("M5")), + item("Cloud", "cloud", "comment.cloud", Planned("M5")), + item("Stamp", "stamp", "comment.stamp", Planned("M5")), + ], + }, + ToolSection { title: "Review", items: &[item("Comment list", "message-square-text", "comment.list", Ready)] }, + ], + }, + ToolGroup { + id: "scan", + label: "Scan & OCR", + icon: "scan-text", + hue: GREEN, + badge: None, + availability: Planned("M10"), + sections: &[ToolSection { + title: "Recognize text", + items: &[ + item("In this file", "scan-text", "ocr.recognize", Planned("M10")), + item("In multiple files", "files", "ocr.recognize_batch", Planned("M10")), + item("Enhance scanned file", "sparkles", "ocr.enhance", Planned("M10")), + item("Correct recognized text", "text-select", "ocr.correct", Planned("M10")), + ], + }], + }, + ToolGroup { + id: "protect", + label: "Protect a PDF", + icon: "shield-check", + hue: BLUE, + badge: None, + availability: Planned("M8"), + sections: &[ + ToolSection { + title: "Protect", + items: &[ + item("Protect with password", "lock", "protect.password", Planned("M8")), + item("Remove hidden information", "eye-off", "protect.sanitize", Planned("M8")), + ], + }, + ToolSection { + title: "Advanced options", + items: &[ + item("Encrypt with certificate", "file-lock-2", "protect.certificate", Planned("M8")), + item("Security properties", "shield-check", "protect.properties", Ready), + item("Remove security", "lock", "protect.remove", Planned("M8")), + ], + }, + ], + }, + ToolGroup { + id: "redact", + label: "Redact a PDF", + icon: "rectangle-horizontal", + hue: RED, + badge: None, + availability: Planned("M8"), + sections: &[ToolSection { + title: "Redact", + items: &[ + item("Redact text and images", "rectangle-horizontal", "redact.mark", Planned("M8")), + item("Redact pages", "file-x", "redact.pages", Planned("M8")), + item("Find text and redact", "file-search", "redact.search", Planned("M8")), + item("Sanitize document", "sparkles", "redact.sanitize", Planned("M8")), + ], + }], + }, + ToolGroup { + id: "compress", + label: "Compress a PDF", + icon: "file-down", + hue: RED, + badge: None, + availability: Planned("M11"), + sections: &[ToolSection { + title: "Optimize", + items: &[ + item("Reduce file size", "file-down", "optimize.reduce", Planned("M11")), + item("Advanced optimization", "settings-2", "optimize.advanced", Planned("M11")), + item("Audit space usage", "gauge", "optimize.audit", Planned("M11")), + ], + }], + }, + ToolGroup { + id: "form", + label: "Prepare a form", + icon: "text-cursor-input", + hue: PURPLE, + badge: None, + availability: Planned("M6"), + sections: &[ + ToolSection { title: "Fields", items: &[item("Field list", "list", "form.fields", Ready)] }, + ToolSection { + title: "Add form components", + items: &[ + item("Text field", "text-cursor-input", "form.add.text", Planned("M6")), + item("Checkbox", "check-circle-2", "form.add.checkbox", Planned("M6")), + item("Radio button", "circle", "form.add.radio", Planned("M6")), + item("Drop-down list", "chevron-down", "form.add.combo", Planned("M6")), + item("List box", "list", "form.add.list", Planned("M6")), + item("Button", "square", "form.add.button", Planned("M6")), + item("Date field", "clock-3", "form.add.date", Planned("M6")), + item("Digital signature", "signature", "form.add.signature", Planned("M6")), + ], + }, + ], + }, + ToolGroup { + id: "stamp", + label: "Add a stamp", + icon: "stamp", + hue: PURPLE, + badge: None, + availability: Planned("M5"), + sections: &[ToolSection { title: "Stamps", items: &[item("Stamp palette", "stamp", "comment.stamp", Planned("M5"))] }], + }, + ToolGroup { + id: "certificate", + label: "Use a certificate", + icon: "badge-check", + hue: TEAL, + badge: None, + availability: Planned("M9"), + sections: &[ToolSection { + title: "Certificates", + items: &[ + item("Digitally sign", "signature", "sign.digital", Planned("M9")), + item("Timestamp", "clock-3", "sign.timestamp", Planned("M9")), + item("Validate all signatures", "badge-check", "sign.validate", Planned("M9")), + item("Certify (visible signature)", "badge-check", "sign.certify", Planned("M9")), + ], + }], + }, + ToolGroup { + id: "print_production", + label: "Use print production", + icon: "printer", + hue: PURPLE, + badge: None, + availability: Planned("M11"), + sections: &[ToolSection { + title: "Print production", + items: &[ + item("Output preview", "layers", "prepress.output_preview", Planned("M11")), + item("Preflight", "file-check", "preflight.run", Planned("M11")), + item("Convert colors", "palette", "prepress.convert_colors", Planned("M11")), + item("Set page boxes", "square-dashed-mouse-pointer", "page.boxes", Planned("M11")), + item("Add printer marks", "ruler", "prepress.marks", Planned("M11")), + item("Fix hairlines", "minus", "prepress.hairlines", Planned("M11")), + ], + }], + }, + ToolGroup { + id: "measure", + label: "Measure objects", + icon: "ruler", + hue: PINK, + badge: None, + availability: Planned("M12"), + sections: &[ToolSection { + title: "Measure", + items: &[ + item("Measuring tool", "ruler", "measure.distance", Planned("M12")), + item("Geospatial location", "compass", "measure.geo", Planned("M12")), + ], + }], + }, + ToolGroup { + id: "compare", + label: "Compare files", + icon: "git-compare", + hue: PINK, + badge: None, + availability: Planned("M12"), + sections: &[ToolSection { title: "Compare", items: &[item("Select files to compare", "git-compare", "compare.files", Planned("M12"))] }], + }, + ToolGroup { + id: "actions", + label: "Use guided actions", + icon: "list-checks", + hue: PURPLE, + badge: None, + availability: Planned("M13"), + sections: &[ToolSection { + title: "Actions list", + items: &[ + item("Make accessible", "accessibility", "actions.make_accessible", Planned("M13")), + item("Prepare for distribution", "send", "actions.distribution", Planned("M13")), + item("Optimize scanned documents", "scan-text", "actions.optimize_scans", Planned("M13")), + item("Archive documents", "file-check", "actions.archive", Planned("M13")), + ], + }], + }, + ToolGroup { + id: "accessibility", + label: "Prepare for accessibility", + icon: "accessibility", + hue: PURPLE, + badge: None, + availability: Planned("M12"), + sections: &[ToolSection { + title: "Accessibility", + items: &[ + item("Automatically tag PDF", "tag", "a11y.autotag", Planned("M12")), + item("Check for accessibility", "accessibility", "a11y.check", Planned("M12")), + item("Fix reading order", "list-ordered", "a11y.reading_order", Planned("M12")), + item("Add alternate text", "image", "a11y.alt_text", Planned("M12")), + ], + }], + }, + ToolGroup { + id: "standards", + label: "Apply PDF standards", + icon: "file-check", + hue: RED, + badge: None, + availability: Planned("M11"), + sections: &[ToolSection { + title: "Standards", + items: &[ + item("Save as PDF/A", "file-check", "standards.pdfa", Planned("M11")), + item("Save as PDF/X", "file-check", "standards.pdfx", Planned("M11")), + item("Save as PDF/UA", "accessibility", "standards.pdfua", Planned("M11")), + item("Preflight", "file-check", "preflight.run", Planned("M11")), + ], + }], + }, + ToolGroup { + id: "search_index", + label: "Add search index", + icon: "file-search", + hue: GREEN, + badge: None, + availability: Planned("M12"), + sections: &[ToolSection { title: "Index", items: &[item("Manage embedded index", "file-search", "search.index", Planned("M12"))] }], + }, + ToolGroup { + id: "javascript", + label: "Use JavaScript", + icon: "braces", + hue: BLUE, + badge: None, + availability: Planned("M6"), + sections: &[ToolSection { + title: "JavaScript", + items: &[ + item("Console", "braces", "js.console", Planned("M6")), + item("Document JavaScripts", "file-text", "js.document", Planned("M6")), + item("Document actions", "list-checks", "js.actions", Planned("M6")), + ], + }], + }, + ToolGroup { + id: "ai", + label: "AI assistant", + icon: "sparkles", + hue: PINK, + badge: Some("Optional"), + availability: Provider, + sections: &[ToolSection { + title: "Bring your own model (off by default)", + items: &[ + item("Summarize", "sparkles", "ai.summary", Provider), + item("Ask about this document", "message-circle-reply", "ai.ask", Provider), + item("Translate", "languages", "ai.translate", Provider), + ], + }], + }, +]; + +pub fn group(id: &str) -> Option<&'static ToolGroup> { + TOOL_GROUPS.iter().find(|g| g.id == id) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + + #[test] + fn ids_are_unique_and_every_group_has_items() { + let mut seen = HashSet::new(); + for g in TOOL_GROUPS { + assert!(seen.insert(g.id), "duplicate group id {}", g.id); + assert!(g.sections.iter().any(|s| !s.items.is_empty()), "{} has no items", g.id); + } + } + + #[test] + fn commands_are_namespaced() { + for g in TOOL_GROUPS { + for s in g.sections { + for i in s.items { + assert!(i.command.contains('.'), "{} lacks a namespace", i.command); + } + } + } + } +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs new file mode 100644 index 00000000..2db3584f --- /dev/null +++ b/crates/engine/src/lib.rs @@ -0,0 +1,60 @@ +//! printcraft-engine — the façade every frontend talks to (architecture §4). +//! +//! This first slice holds open documents and the tool catalogue. The command registry, history +//! and jobs arrive in M4; the types here are shaped so that the UI never touches rendering or +//! parsing crates directly. + +pub mod catalog; + +use std::sync::Arc; + +use printcraft_render::{DocInfo, OpenError, RenderPool, inspect}; + +/// Stable identifier of an open document within a session. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct DocId(pub u64); + +pub struct Document { + pub id: DocId, + pub name: String, + pub path: Option, + pub bytes: Arc>, + pub info: DocInfo, + pub renderer: RenderPool, +} + +#[derive(Default)] +pub struct Session { + docs: Vec, + next_id: u64, +} + +impl Session { + pub fn new() -> Self { + Self::default() + } + + /// Open a document from bytes. Rendering starts lazily when pages are requested. + pub fn open(&mut self, name: impl Into, path: Option, bytes: Vec) -> Result { + let bytes = Arc::new(bytes); + let info = inspect(bytes.clone())?; + let threads = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4).clamp(2, 8) - 1; + let renderer = RenderPool::new(bytes.clone(), threads); + self.next_id += 1; + let id = DocId(self.next_id); + self.docs.push(Document { id, name: name.into(), path, bytes, info, renderer }); + Ok(id) + } + + pub fn close(&mut self, id: DocId) { + self.docs.retain(|d| d.id != id); + } + + pub fn get(&self, id: DocId) -> Option<&Document> { + self.docs.iter().find(|d| d.id == id) + } + + pub fn docs(&self) -> &[Document] { + &self.docs + } +} diff --git a/crates/geom/Cargo.toml b/crates/geom/Cargo.toml new file mode 100644 index 00000000..25ca71dd --- /dev/null +++ b/crates/geom/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "printcraft-geom" +description = "Geometry primitives for PrintCraft (points, rects, page transforms)" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] diff --git a/crates/geom/src/lib.rs b/crates/geom/src/lib.rs new file mode 100644 index 00000000..75ff3a42 --- /dev/null +++ b/crates/geom/src/lib.rs @@ -0,0 +1,67 @@ +//! printcraft-geom — L0 geometry primitives shared by every PrintCraft crate. +//! +//! PDF user space is y-up with the origin at the bottom-left of the page; view space is y-down. +//! `PageRect` is always in PDF user space (points, 1/72 inch). + +/// An axis-aligned rectangle in PDF user space (points). +#[derive(Clone, Copy, Debug, PartialEq, Default)] +pub struct PageRect { + pub x0: f32, + pub y0: f32, + pub x1: f32, + pub y1: f32, +} + +impl PageRect { + pub fn new(x0: f32, y0: f32, x1: f32, y1: f32) -> Self { + Self { x0: x0.min(x1), y0: y0.min(y1), x1: x0.max(x1), y1: y0.max(y1) } + } + pub fn width(&self) -> f32 { + self.x1 - self.x0 + } + pub fn height(&self) -> f32 { + self.y1 - self.y0 + } +} + +/// Page rotation in quarter turns, as in the PDF `/Rotate` key (normalised to 0/90/180/270). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum Rotation { + #[default] + R0, + R90, + R180, + R270, +} + +impl Rotation { + pub fn from_degrees(deg: i64) -> Self { + match deg.rem_euclid(360) { + 90 => Self::R90, + 180 => Self::R180, + 270 => Self::R270, + _ => Self::R0, + } + } + pub fn swaps_axes(self) -> bool { + matches!(self, Self::R90 | Self::R270) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rect_normalises() { + let r = PageRect::new(10.0, 20.0, 0.0, 5.0); + assert_eq!((r.x0, r.y0, r.width(), r.height()), (0.0, 5.0, 10.0, 15.0)); + } + + #[test] + fn rotation_normalises_negative_and_large_values() { + assert_eq!(Rotation::from_degrees(-90), Rotation::R270); + assert_eq!(Rotation::from_degrees(450), Rotation::R90); + assert!(Rotation::R90.swaps_axes()); + } +} diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml new file mode 100644 index 00000000..afadd24a --- /dev/null +++ b/crates/render/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "printcraft-render" +description = "Page rendering (bootstrap: hayro) and document inspection for PrintCraft" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +hayro = { workspace = true } +hayro-syntax = { workspace = true } +lopdf = { workspace = true } +thiserror = { workspace = true } +log = { workspace = true } diff --git a/crates/render/src/inspect.rs b/crates/render/src/inspect.rs new file mode 100644 index 00000000..9a6eb01c --- /dev/null +++ b/crates/render/src/inspect.rs @@ -0,0 +1,641 @@ +//! Document inspection: everything panels need that is not pixels. +//! +//! Page geometry comes from hayro (which resolves inheritance and rotation); the rest comes from +//! lopdf (bootstrap, replaced by `printcraft-model` in M2). Inspection is *tolerant*: when lopdf +//! cannot load a file that hayro can render, panels are simply empty and `warnings` says why. + +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::sync::Arc; + +use hayro::hayro_syntax::Pdf; +use lopdf::{Dictionary, Document, Object, ObjectId}; + +use crate::OpenError; + +#[derive(Clone, Debug, Default)] +pub struct DocInfo { + pub title: Option, + pub author: Option, + pub subject: Option, + pub keywords: Option, + pub creator: Option, + pub producer: Option, + pub pdf_version: String, + pub file_size: usize, + pub encrypted: bool, + pub tagged: bool, + pub has_javascript: bool, + pub pages: Vec, + pub outline: Vec, + pub annotations: Vec, + pub fields: Vec, + pub links: Vec, + pub layers: Vec, + pub attachments: Vec, + pub warnings: Vec, +} + +#[derive(Clone, Debug)] +pub struct PageInfo { + /// Displayed size in points, after `/Rotate` and `/UserUnit`. + pub width: f32, + pub height: f32, + /// Page label (`/PageLabels`), falling back to the 1-based page number. + pub label: String, + /// Effective crop box in user space [x0, y0, x1, y1] (the visible region). + pub crop: [f32; 4], + /// Clockwise page rotation in degrees (0, 90, 180, 270). + pub rotation: u16, +} + +/// A clickable link annotation. +#[derive(Clone, Debug)] +pub struct Link { + pub page: usize, + pub rect: [f32; 4], + pub target: LinkTarget, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum LinkTarget { + Page(usize), + Uri(String), + Other(String), +} + +#[derive(Clone, Debug)] +pub struct OutlineItem { + pub title: String, + pub page: Option, + pub children: Vec, + pub open: bool, +} + +#[derive(Clone, Debug)] +pub struct Annotation { + pub page: usize, + pub subtype: String, + pub author: Option, + pub contents: Option, + pub modified: Option, + /// `/NM` of this annotation, and of the one it replies to (`/IRT`), for threading. + pub name: Option, + pub in_reply_to: Option, + /// Rect in PDF user space: [x0, y0, x1, y1]. + pub rect: [f32; 4], + pub color: Option<[f32; 3]>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FieldKind { + Text, + CheckBox, + Radio, + PushButton, + Combo, + List, + Signature, + Unknown, +} + +#[derive(Clone, Debug)] +pub struct Field { + pub name: String, + pub kind: FieldKind, + pub value: Option, + pub page: Option, + pub tooltip: Option, + pub has_actions: bool, + /// Rect of the first widget, in user space. + pub rect: Option<[f32; 4]>, +} + +#[derive(Clone, Debug)] +pub struct Layer { + pub name: String, + pub visible: bool, +} + +#[derive(Clone, Debug)] +pub struct Attachment { + pub name: String, + pub description: Option, + pub size: Option, +} + +/// Inspect a document. Fails only if the renderer itself cannot open the file. +pub fn inspect(bytes: Arc>) -> Result { + let pdf = Pdf::new(bytes.clone()).map_err(|e| match format!("{e:?}") { + s if s.to_lowercase().contains("encrypt") || s.to_lowercase().contains("password") => OpenError::NeedsPassword, + s => OpenError::Invalid(s), + })?; + let mut info = DocInfo { file_size: bytes.len(), pdf_version: format!("{:?}", pdf.version()), ..Default::default() }; + for (i, page) in pdf.pages().iter().enumerate() { + let (w, h) = page.render_dimensions(); + let c = page.intersected_crop_box(); + let rotation = match page.rotation() { + hayro::hayro_syntax::page::Rotation::None => 0, + hayro::hayro_syntax::page::Rotation::Horizontal => 90, + hayro::hayro_syntax::page::Rotation::Flipped => 180, + hayro::hayro_syntax::page::Rotation::FlippedHorizontal => 270, + }; + let crop = [c.x0 as f32, c.y0 as f32, c.x1 as f32, c.y1 as f32]; + info.pages.push(PageInfo { width: w, height: h, label: (i + 1).to_string(), crop, rotation }); + } + match Document::load_mem(&bytes) { + Ok(doc) => Inspector::new(&doc).fill(&mut info), + Err(e) => info.warnings.push(format!("structure inspection unavailable: {e}")), + } + Ok(info) +} + +struct Inspector<'a> { + doc: &'a Document, + page_index: HashMap, +} + +impl<'a> Inspector<'a> { + fn new(doc: &'a Document) -> Self { + let page_index = doc.get_pages().into_iter().map(|(n, id)| (id, n as usize - 1)).collect(); + Self { doc, page_index } + } + + fn fill(&self, info: &mut DocInfo) { + info.encrypted = self.doc.is_encrypted() || self.doc.trailer.get(b"Encrypt").is_ok(); + if let Some(d) = self.doc.trailer.get(b"Info").ok().and_then(|o| self.dict(o)) { + info.title = self.text(d, b"Title"); + info.author = self.text(d, b"Author"); + info.subject = self.text(d, b"Subject"); + info.keywords = self.text(d, b"Keywords"); + info.creator = self.text(d, b"Creator"); + info.producer = self.text(d, b"Producer"); + } + let Ok(catalog) = self.doc.catalog() else { return }; + info.tagged = catalog + .get(b"MarkInfo") + .ok() + .and_then(|o| self.dict(o)) + .and_then(|m| m.get(b"Marked").ok()) + .and_then(|o| o.as_bool().ok()) + .unwrap_or(false); + self.page_labels(catalog, &mut info.pages); + if let Some(first) = catalog.get(b"Outlines").ok().and_then(|o| self.dict(o)).and_then(|d| d.get(b"First").ok()) { + let mut seen = HashSet::new(); + info.outline = self.outline_siblings(first, &mut seen, 0); + } + self.annotations(info); + if let Some(form) = catalog.get(b"AcroForm").ok().and_then(|o| self.dict(o)) + && let Ok(fields) = form.get(b"Fields").and_then(|o| self.resolve(o).as_array()) + { + let mut seen = HashSet::new(); + for f in fields { + self.field(f, None, &mut info.fields, &mut seen, 0); + } + } + info.has_javascript |= info.fields.iter().any(|f| f.has_actions); + self.layers(catalog, &mut info.layers); + if let Some(names) = catalog.get(b"Names").ok().and_then(|o| self.dict(o)) { + if let Some(ef) = names.get(b"EmbeddedFiles").ok().and_then(|o| self.dict(o)) { + let mut seen = HashSet::new(); + for (name, spec) in self.name_tree(ef, &mut seen, 0) { + info.attachments.push(self.attachment(name, spec)); + } + } + info.has_javascript |= names.get(b"JavaScript").is_ok(); + } + } + + // ── object helpers ────────────────────────────────────────────────────────────────────── + + fn resolve(&self, o: &'a Object) -> &'a Object { + self.doc.dereference(o).map(|(_, o)| o).unwrap_or(o) + } + + fn dict(&self, o: &'a Object) -> Option<&'a Dictionary> { + match self.resolve(o) { + Object::Dictionary(d) => Some(d), + Object::Stream(s) => Some(&s.dict), + _ => None, + } + } + + fn text(&self, d: &Dictionary, key: &[u8]) -> Option { + let o = self.resolve(d.get(key).ok()?); + let s = match o { + Object::String(..) => lopdf::decode_text_string(o).ok()?, + Object::Name(n) => String::from_utf8_lossy(n).into_owned(), + _ => return None, + }; + let s = s.trim_matches('\0').trim().to_string(); + (!s.is_empty()).then_some(s) + } + + fn name(&self, d: &Dictionary, key: &[u8]) -> Option { + self.resolve(d.get(key).ok()?).as_name().ok().map(|n| String::from_utf8_lossy(n).into_owned()) + } + + fn page_of(&self, o: &Object) -> Option { + match o { + Object::Reference(id) => self.page_index.get(id).copied(), + // Some producers write page *numbers* in remote-style destinations. + Object::Integer(n) => usize::try_from(*n).ok(), + _ => None, + } + } + + // ── destinations ──────────────────────────────────────────────────────────────────────── + + fn dest_page(&self, dest: &Object, depth: u32) -> Option { + if depth > 8 { + return None; + } + match self.resolve(dest) { + Object::Array(a) => a.first().and_then(|p| self.page_of(p)), + Object::Dictionary(d) => d.get(b"D").ok().and_then(|d| self.dest_page(d, depth + 1)), + named @ (Object::String(..) | Object::Name(_)) => { + let key = match named { + Object::String(s, _) => s.clone(), + Object::Name(n) => n.clone(), + _ => unreachable!(), + }; + self.named_dest(&key).and_then(|d| self.dest_page(d, depth + 1)) + } + _ => None, + } + } + + fn named_dest(&self, key: &[u8]) -> Option<&'a Object> { + let catalog = self.doc.catalog().ok()?; + if let Some(tree) = catalog.get(b"Names").ok().and_then(|o| self.dict(o)).and_then(|n| n.get(b"Dests").ok()).and_then(|o| self.dict(o)) { + let mut seen = HashSet::new(); + if let Some((_, v)) = self.name_tree(tree, &mut seen, 0).into_iter().find(|(k, _)| k.as_bytes() == key) { + return Some(v); + } + } + // PDF 1.1 style /Dests dictionary. + catalog.get(b"Dests").ok().and_then(|o| self.dict(o)).and_then(|d| d.get(key).ok()) + } + + fn name_tree(&self, node: &'a Dictionary, seen: &mut HashSet<*const Dictionary>, depth: u32) -> Vec<(String, &'a Object)> { + let mut out = Vec::new(); + if depth > 32 || !seen.insert(node as *const _) { + return out; + } + if let Ok(Object::Array(pairs)) = node.get(b"Names").map(|o| self.resolve(o)) { + for pair in pairs.chunks(2) { + if let [k, v] = pair { + let key = lopdf::decode_text_string(self.resolve(k)).unwrap_or_default(); + out.push((key, v)); + } + } + } + if let Ok(Object::Array(kids)) = node.get(b"Kids").map(|o| self.resolve(o)) { + for k in kids { + if let Some(d) = self.dict(k) { + out.extend(self.name_tree(d, seen, depth + 1)); + } + } + } + out + } + + // ── outline ───────────────────────────────────────────────────────────────────────────── + + fn outline_siblings(&self, first: &'a Object, seen: &mut HashSet, depth: u32) -> Vec { + let mut items = Vec::new(); + let mut cur = Some(first); + while let Some(o) = cur { + if let Object::Reference(id) = o + && !seen.insert(*id) + { + break; // cycle + } + let Some(d) = self.dict(o) else { break }; + let page = d + .get(b"Dest") + .ok() + .and_then(|dest| self.dest_page(dest, 0)) + .or_else(|| d.get(b"A").ok().and_then(|a| self.dict(a)).and_then(|a| a.get(b"D").ok()).and_then(|dest| self.dest_page(dest, 0))); + let children = match (d.get(b"First").ok(), depth < 32) { + (Some(f), true) => self.outline_siblings(f, seen, depth + 1), + _ => Vec::new(), + }; + let open = d.get(b"Count").ok().and_then(|c| c.as_i64().ok()).is_some_and(|c| c > 0); + items.push(OutlineItem { title: self.text(d, b"Title").unwrap_or_default(), page, children, open }); + cur = d.get(b"Next").ok(); + if items.len() > 100_000 { + break; + } + } + items + } + + // ── page labels (ISO 32000-2 §12.4.2) ─────────────────────────────────────────────────── + + fn page_labels(&self, catalog: &Dictionary, pages: &mut [PageInfo]) { + let Some(tree) = catalog.get(b"PageLabels").ok().and_then(|o| self.dict(o)) else { return }; + let mut ranges: BTreeMap = BTreeMap::new(); + let mut stack = vec![(tree, 0u32)]; + while let Some((node, depth)) = stack.pop() { + if let Ok(Object::Array(nums)) = node.get(b"Nums").map(|o| self.resolve(o)) { + for pair in nums.chunks(2) { + if let [k, v] = pair + && let (Ok(start), Some(d)) = (self.resolve(k).as_i64(), self.dict(v)) + && start >= 0 + { + ranges.insert(start as usize, d); + } + } + } + if depth < 32 + && let Ok(Object::Array(kids)) = node.get(b"Kids").map(|o| self.resolve(o)) + { + stack.extend(kids.iter().filter_map(|k| self.dict(k)).map(|d| (d, depth + 1))); + } + } + for (i, page) in pages.iter_mut().enumerate() { + let Some((&start, d)) = ranges.range(..=i).next_back() else { continue }; + let first = d.get(b"St").ok().and_then(|o| o.as_i64().ok()).unwrap_or(1).max(1) as usize; + let n = first + (i - start); + let prefix = self.text(d, b"P").unwrap_or_default(); + let number = match self.name(d, b"S").as_deref() { + Some("D") => n.to_string(), + Some("R") => roman(n).to_uppercase(), + Some("r") => roman(n), + Some("A") => alpha(n).to_uppercase(), + Some("a") => alpha(n), + _ => String::new(), + }; + page.label = format!("{prefix}{number}"); + if page.label.is_empty() { + page.label = (i + 1).to_string(); + } + } + } + + // ── annotations ───────────────────────────────────────────────────────────────────────── + + fn annotations(&self, info: &mut DocInfo) { + for (&id, &page) in &self.page_index { + let Ok(page_dict) = self.doc.get_dictionary(id) else { continue }; + let Ok(Object::Array(annots)) = page_dict.get(b"Annots").map(|o| self.resolve(o)) else { continue }; + for a in annots { + let Some(d) = self.dict(a) else { continue }; + let Some(subtype) = self.name(d, b"Subtype") else { continue }; + if subtype == "Link" { + if let Some(link) = self.link(page, d) { + if matches!(&link.target, LinkTarget::Other(s) if s == "JavaScript") { + info.has_javascript = true; + } + info.links.push(link); + } + continue; + } + if matches!(subtype.as_str(), "Widget" | "Popup") { + continue; + } + let rect = rect4(self.resolve(d.get(b"Rect").unwrap_or(&Object::Null))); + let color = match d.get(b"C").map(|o| self.resolve(o)) { + Ok(Object::Array(c)) if c.len() == 3 => { + let f = |i: usize| c[i].as_float().unwrap_or(0.0); + Some([f(0), f(1), f(2)]) + } + _ => None, + }; + let in_reply_to = d.get(b"IRT").ok().and_then(|o| self.dict(o)).and_then(|p| self.text(p, b"NM")); + info.annotations.push(Annotation { + page, + subtype, + author: self.text(d, b"T"), + contents: self.text(d, b"Contents"), + modified: self.text(d, b"M").map(|m| pretty_date(&m)), + name: self.text(d, b"NM"), + in_reply_to, + rect, + color, + }); + } + } + info.annotations.sort_by(|a, b| a.page.cmp(&b.page).then(b.rect[3].total_cmp(&a.rect[3]))); + } + + fn link(&self, page: usize, d: &Dictionary) -> Option { + let rect = rect4(self.resolve(d.get(b"Rect").ok()?)); + let target = if let Ok(dest) = d.get(b"Dest") { + LinkTarget::Page(self.dest_page(dest, 0)?) + } else { + let a = d.get(b"A").ok().and_then(|a| self.dict(a))?; + match self.name(a, b"S").as_deref() { + Some("GoTo") => LinkTarget::Page(self.dest_page(a.get(b"D").ok()?, 0)?), + Some("URI") => { + LinkTarget::Uri(a.get(b"URI").ok().and_then(|u| self.resolve(u).as_str().ok()).map(|b| String::from_utf8_lossy(b).into_owned())?) + } + Some(other) => LinkTarget::Other(other.to_string()), + None => return None, + } + }; + Some(Link { page, rect, target }) + } + + // ── form fields ───────────────────────────────────────────────────────────────────────── + + fn field( + &self, + o: &'a Object, + parent: Option<(&str, Option, Option)>, + out: &mut Vec, + seen: &mut HashSet, + depth: u32, + ) { + if depth > 32 { + return; + } + if let Object::Reference(id) = o + && !seen.insert(*id) + { + return; + } + let Some(d) = self.dict(o) else { return }; + let (parent_name, parent_ft, parent_ff) = parent.unwrap_or(("", None, None)); + let partial = self.text(d, b"T"); + let full = match (&partial, parent_name.is_empty()) { + (Some(t), true) => t.clone(), + (Some(t), false) => format!("{parent_name}.{t}"), + (None, _) => parent_name.to_string(), + }; + let ft = self.name(d, b"FT").or(parent_ft); + let ff = d.get(b"Ff").ok().and_then(|x| x.as_i64().ok()).or(parent_ff); + // Non-terminal: has kids that are fields (they carry /T). Widgets-only kids are terminal. + let kids = match d.get(b"Kids").map(|k| self.resolve(k)) { + Ok(Object::Array(k)) => k.as_slice(), + _ => &[], + }; + let field_kids: Vec<_> = kids.iter().filter(|k| self.dict(k).is_some_and(|kd| kd.get(b"T").is_ok())).collect(); + if !field_kids.is_empty() { + for k in field_kids { + self.field(k, Some((&full, ft.clone(), ff)), out, seen, depth + 1); + } + return; + } + let flags = ff.unwrap_or(0); + let kind = match ft.as_deref() { + Some("Tx") => FieldKind::Text, + Some("Btn") if flags & (1 << 16) != 0 => FieldKind::PushButton, + Some("Btn") if flags & (1 << 15) != 0 => FieldKind::Radio, + Some("Btn") => FieldKind::CheckBox, + Some("Ch") if flags & (1 << 17) != 0 => FieldKind::Combo, + Some("Ch") => FieldKind::List, + Some("Sig") => FieldKind::Signature, + _ => FieldKind::Unknown, + }; + let value = match d.get(b"V").map(|v| self.resolve(v)) { + Ok(Object::Name(n)) => Some(String::from_utf8_lossy(n).into_owned()), + Ok(v @ Object::String(..)) => lopdf::decode_text_string(v).ok(), + Ok(Object::Array(a)) => Some(a.iter().filter_map(|x| lopdf::decode_text_string(self.resolve(x)).ok()).collect::>().join(", ")), + Ok(Object::Dictionary(_)) if kind == FieldKind::Signature => Some("signed".into()), + _ => None, + }; + // The widget is either this dict (merged field/widget) or its first kid. + let widget = if d.get(b"Rect").is_ok() { Some(d) } else { kids.first().and_then(|k| self.dict(k)) }; + let page = widget.and_then(|w| w.get(b"P").ok()).and_then(|p| self.page_of(p)).or_else(|| self.find_widget_page(o, kids)); + let has_actions = d.get(b"AA").is_ok() || widget.is_some_and(|w| w.get(b"AA").is_ok() || w.get(b"A").is_ok()); + let rect = widget.and_then(|w| w.get(b"Rect").ok()).map(|r| rect4(self.resolve(r))); + out.push(Field { name: full, kind, value, page, tooltip: self.text(d, b"TU"), has_actions, rect }); + } + + fn find_widget_page(&self, field: &Object, kids: &[Object]) -> Option { + let targets: HashSet = + std::iter::once(field).chain(kids.iter()).filter_map(|o| if let Object::Reference(id) = o { Some(*id) } else { None }).collect(); + if targets.is_empty() { + return None; + } + self.page_index.iter().find_map(|(&pid, &idx)| { + let annots = self.doc.get_dictionary(pid).ok()?.get(b"Annots").ok()?; + let arr = self.resolve(annots).as_array().ok()?; + arr.iter().any(|a| matches!(a, Object::Reference(id) if targets.contains(id))).then_some(idx) + }) + } + + // ── optional content ──────────────────────────────────────────────────────────────────── + + fn layers(&self, catalog: &Dictionary, out: &mut Vec) { + let Some(props) = catalog.get(b"OCProperties").ok().and_then(|o| self.dict(o)) else { return }; + let config = props.get(b"D").ok().and_then(|o| self.dict(o)); + let off: HashSet = config + .and_then(|c| c.get(b"OFF").ok()) + .and_then(|o| self.resolve(o).as_array().ok()) + .map(|a| a.iter().filter_map(|x| x.as_reference().ok()).collect()) + .unwrap_or_default(); + let base_off = config.and_then(|c| self.name(c, b"BaseState")).as_deref() == Some("OFF"); + let on: HashSet = config + .and_then(|c| c.get(b"ON").ok()) + .and_then(|o| self.resolve(o).as_array().ok()) + .map(|a| a.iter().filter_map(|x| x.as_reference().ok()).collect()) + .unwrap_or_default(); + let Ok(Object::Array(ocgs)) = props.get(b"OCGs").map(|o| self.resolve(o)) else { return }; + for g in ocgs { + let Some(d) = self.dict(g) else { continue }; + let id = g.as_reference().ok(); + let visible = match id { + Some(id) if off.contains(&id) => false, + Some(id) if on.contains(&id) => true, + _ => !base_off, + }; + out.push(Layer { name: self.text(d, b"Name").unwrap_or_else(|| "Layer".into()), visible }); + } + } + + fn attachment(&self, name: String, spec: &Object) -> Attachment { + let d = self.dict(spec); + let size = + d.and_then(|d| d.get(b"EF").ok()).and_then(|o| self.dict(o)).and_then(|ef| ef.get(b"F").ok().or_else(|| ef.get(b"UF").ok())).and_then( + |f| match self.resolve(f) { + Object::Stream(s) => s + .dict + .get(b"Params") + .ok() + .and_then(|p| self.dict(p)) + .and_then(|p| p.get(b"Size").ok()) + .and_then(|s| s.as_i64().ok()) + .map(|s| s as usize) + .or(Some(s.content.len())), + _ => None, + }, + ); + let display = d.and_then(|d| self.text(d, b"UF").or_else(|| self.text(d, b"F"))).unwrap_or(name); + Attachment { name: display, description: d.and_then(|d| self.text(d, b"Desc")), size } + } +} + +fn rect4(o: &Object) -> [f32; 4] { + match o { + Object::Array(a) if a.len() == 4 => { + let v: Vec = a.iter().map(|x| x.as_float().unwrap_or(0.0)).collect(); + [v[0].min(v[2]), v[1].min(v[3]), v[0].max(v[2]), v[1].max(v[3])] + } + _ => [0.0; 4], + } +} + +fn roman(mut n: usize) -> String { + const T: [(usize, &str); 13] = [ + (1000, "m"), + (900, "cm"), + (500, "d"), + (400, "cd"), + (100, "c"), + (90, "xc"), + (50, "l"), + (40, "xl"), + (10, "x"), + (9, "ix"), + (5, "v"), + (4, "iv"), + (1, "i"), + ]; + let mut s = String::new(); + for (v, r) in T { + while n >= v { + s.push_str(r); + n -= v; + } + } + s +} + +/// a..z, aa..zz, aaa.. (ISO 32000-2: "a to z for the first 26 pages, aa to zz for the next 26"). +fn alpha(n: usize) -> String { + let n = n.max(1) - 1; + let letter = (b'a' + (n % 26) as u8) as char; + std::iter::repeat_n(letter, n / 26 + 1).collect() +} + +/// `D:20260930104512-04'00'` → `2026-09-30 10:45`. +fn pretty_date(s: &str) -> String { + let d = s.trim_start_matches("D:"); + if d.len() >= 12 && d[..12].bytes().all(|b| b.is_ascii_digit()) { + format!("{}-{}-{} {}:{}", &d[0..4], &d[4..6], &d[6..8], &d[8..10], &d[10..12]) + } else { + s.to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn roman_and_alpha_labels() { + assert_eq!(roman(4), "iv"); + assert_eq!(roman(1994), "mcmxciv"); + assert_eq!(alpha(1), "a"); + assert_eq!(alpha(27), "aa"); + assert_eq!(alpha(53), "aaa"); + } + + #[test] + fn dates_are_prettified() { + assert_eq!(pretty_date("D:20260930104512-04'00'"), "2026-09-30 10:45"); + assert_eq!(pretty_date("yesterday"), "yesterday"); + } +} diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs new file mode 100644 index 00000000..283d41ba --- /dev/null +++ b/crates/render/src/lib.rs @@ -0,0 +1,22 @@ +//! printcraft-render — page rasterization and document inspection. +//! +//! **Bootstrap status (see ADR-0004):** rasterization goes straight through the `hayro` crate +//! (hayro-interpret + vello_cpu), and inspection (outline, annotations, fields, layers, +//! attachments, metadata) uses `lopdf`. Both are replaced by our own `cos`/`model` crates and the +//! DisplayList device design in M1–M2. The public API here is what the engine and UI rely on, so +//! the swap stays internal to this crate. + +mod inspect; +mod raster; + +pub use inspect::{Annotation, Attachment, DocInfo, Field, FieldKind, Layer, Link, LinkTarget, OutlineItem, PageInfo, inspect}; +pub use raster::{RenderPool, RenderRequest, RenderedPage}; + +/// Errors surfaced to the user when a document cannot be opened. +#[derive(Debug, thiserror::Error)] +pub enum OpenError { + #[error("the file is not a readable PDF: {0}")] + Invalid(String), + #[error("the document is encrypted and needs a password")] + NeedsPassword, +} diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs new file mode 100644 index 00000000..2672e591 --- /dev/null +++ b/crates/render/src/raster.rs @@ -0,0 +1,138 @@ +//! Background page rasterization. +//! +//! A `RenderPool` owns N worker threads. Each worker parses the shared bytes once (hayro's parser +//! is lazy, so this is cheap) and keeps its own render cache. Requests carry a generation number; +//! the UI drops results for stale generations (e.g. after a zoom change). + +use std::sync::mpsc::{Receiver, Sender, channel}; +use std::sync::{Arc, Mutex}; +use std::thread::JoinHandle; + +use hayro::hayro_interpret::InterpreterSettings; +use hayro::hayro_syntax::Pdf; +use hayro::vello_cpu::color::palette::css::WHITE; +use hayro::{RenderCache, RenderSettings, render}; + +/// Hard cap on a rendered side, to bound memory at extreme zoom levels (tiling arrives in M3.3). +const MAX_SIDE: f32 = 8192.0; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct RenderRequest { + pub page: usize, + /// Device pixels per PDF point. + pub scale: f32, + /// Caller-defined tag, echoed back (used for zoom generations / thumbnail vs page). + pub tag: u64, +} + +pub struct RenderedPage { + pub request: RenderRequest, + pub width: u32, + pub height: u32, + /// Premultiplied RGBA8, row-major. + pub rgba: Vec, +} + +type Queue = Arc>>; + +pub struct RenderPool { + queue: Queue, + wake: Vec>, + results: Receiver, + _workers: Vec>, +} + +impl RenderPool { + pub fn new(bytes: Arc>, threads: usize) -> Self { + let queue: Queue = Arc::new(Mutex::new(Vec::new())); + let (res_tx, results) = channel(); + let mut wake = Vec::new(); + let mut workers = Vec::new(); + for i in 0..threads.max(1) { + let (wtx, wrx) = channel::<()>(); + wake.push(wtx); + let queue = queue.clone(); + let res_tx = res_tx.clone(); + let bytes = bytes.clone(); + let handle = std::thread::Builder::new() + .name(format!("printcraft-render-{i}")) + .spawn(move || worker(bytes, queue, wrx, res_tx)) + .expect("spawn render worker"); + workers.push(handle); + } + Self { queue, wake, results, _workers: workers } + } + + /// Replace the pending queue (most urgent first). In-flight renders are not interrupted. + pub fn set_queue(&self, mut requests: Vec) { + requests.reverse(); // workers pop from the end + if let Ok(mut q) = self.queue.lock() { + *q = requests; + } + for w in &self.wake { + let _ = w.send(()); + } + } + + pub fn try_recv(&self) -> Option { + self.results.try_recv().ok() + } +} + +fn worker(bytes: Arc>, queue: Queue, wake: Receiver<()>, out: Sender) { + let Ok(pdf) = Pdf::new(bytes) else { return }; + let settings = InterpreterSettings::default(); + let cache = RenderCache::new(); + let pages = pdf.pages(); + loop { + let next = queue.lock().ok().and_then(|mut q| q.pop()); + let Some(req) = next else { + if wake.recv().is_err() { + return; // pool dropped + } + continue; + }; + let Some(page) = pages.get(req.page) else { continue }; + let (w, h) = page.render_dimensions(); + let scale = req.scale.min(MAX_SIDE / w.max(h).max(1.0)); + let rs = RenderSettings { x_scale: scale, y_scale: scale, bg_color: WHITE, ..Default::default() }; + let pixmap = render(page, &cache, &settings, &rs); + let (width, height) = (pixmap.width() as u32, pixmap.height() as u32); + let rgba = pixmap.data_as_u8_slice().to_vec(); + if out.send(RenderedPage { request: req, width, height, rgba }).is_err() { + return; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const ONE_PAGE: &[u8] = b"%PDF-1.4 +1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj +2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj +3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 100 50] /Contents 4 0 R >> endobj +4 0 obj << /Length 35 >> stream +0 0 1 rg 10 10 30 20 re f +endstream endobj +trailer << /Root 1 0 R >> +%%EOF"; + + #[test] + fn renders_a_blue_rectangle() { + let pool = RenderPool::new(Arc::new(ONE_PAGE.to_vec()), 1); + pool.set_queue(vec![RenderRequest { page: 0, scale: 1.0, tag: 7 }]); + let page = loop { + if let Some(p) = pool.try_recv() { + break p; + } + std::thread::sleep(std::time::Duration::from_millis(5)); + }; + assert_eq!((page.width, page.height, page.request.tag), (100, 50, 7)); + // Pixel (20, 30) in y-down device space is inside the rect drawn at y 10..30 (y-up). + let px = |x: u32, y: u32| &page.rgba[((y * page.width + x) * 4) as usize..][..4]; + assert_eq!(px(20, 30), &[0, 0, 255, 255]); + assert_eq!(px(80, 5), &[255, 255, 255, 255]); + } +} diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml new file mode 100644 index 00000000..4d29ff6e --- /dev/null +++ b/crates/ui-egui/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "printcraft-ui-egui" +description = "The egui shell of PrintCraft" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +printcraft-engine = { workspace = true } +printcraft-render = { workspace = true } +egui = { workspace = true } +egui_extras = { workspace = true } +eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] } +serde = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +rfd = { workspace = true } diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs new file mode 100644 index 00000000..71b951d5 --- /dev/null +++ b/crates/ui-egui/src/canvas.rs @@ -0,0 +1,573 @@ +//! The document area: page layout, zoom, render scheduling, overlays (links, annotation hovers, +//! form-field highlights), and the organize-pages grid. +//! +//! Page images come from the engine's render pool as whole-page rasters at the current zoom; +//! stale rasters are shown stretched until the sharp one arrives (tiling comes in M3.3). + +use std::collections::HashMap; + +use egui::{Align2, Color32, CornerRadius, Pos2, Rect, Sense, Stroke, TextureHandle, TextureOptions, Vec2, pos2, vec2}; +use printcraft_engine::DocId; +use printcraft_render::{DocInfo, LinkTarget, RenderPool, RenderRequest}; + +use crate::theme::{self, Tokens}; +use crate::{PrintCraftApp, QuickTool, RightPanel, icons}; + +/// Logical pixels per PDF point at 100% (96 dpi, like browsers). +pub const PT: f32 = 96.0 / 72.0; +const GAP: f32 = 14.0; +const MARGIN: f32 = 28.0; +/// Horizontal gutter that keeps pages clear of the floating quick-action bar. +const SIDE: f32 = 70.0; +const THUMB_TAG: u64 = 1 << 63; +const THUMB_W: f32 = 132.0; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Fit { + Width, + Page, + None, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PageLayout { + Continuous, + TwoUp, + Single, +} + +struct PageTex { + tag: u64, + tex: TextureHandle, +} + +pub struct DocView { + pub id: DocId, + pub zoom: f32, + pub fit: Fit, + pub layout: PageLayout, + pub current: usize, + pub organize: bool, + pub highlight_fields: bool, + pub page_input: String, + pub notice_dismissed: bool, + /// Pending navigation: page and fraction down the page to align with the viewport top. + pub goto: Option<(usize, f32)>, + /// Briefly outline an annotation after navigating to it from a panel. + pub flash: Option<(usize, [f32; 4], f64)>, + pages: HashMap, + thumbs: HashMap, + last_queue: Vec, + viewport_w: f32, + viewport_h: f32, + page_count: usize, +} + +impl DocView { + pub fn new(id: DocId, info: &DocInfo) -> Self { + Self { + id, + zoom: 1.0, + fit: Fit::Width, + layout: PageLayout::Continuous, + current: 0, + organize: false, + highlight_fields: false, + page_input: "1".into(), + notice_dismissed: false, + goto: None, + flash: None, + pages: HashMap::new(), + thumbs: HashMap::new(), + last_queue: Vec::new(), + viewport_w: 800.0, + viewport_h: 600.0, + page_count: info.pages.len(), + } + } + + pub fn thumb(&self, page: usize) -> Option<&TextureHandle> { + self.thumbs.get(&page) + } + + pub fn go_to_page(&mut self, page: usize) { + let page = page.min(self.page_count.saturating_sub(1)); + self.goto = Some((page, 0.0)); + self.current = page; + self.page_input = (page + 1).to_string(); + } + + pub fn set_zoom(&mut self, zoom: f32) { + let frac = 0.0; + self.zoom = zoom.clamp(0.08, 64.0); + self.fit = Fit::None; + self.goto = Some((self.current, frac)); + } + + /// Standard zoom steps (as in Acrobat's zoom menu). + pub fn zoom_step(&mut self, up: bool) { + const STEPS: [f32; 17] = [0.1, 0.25, 0.33, 0.5, 0.66, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 12.0, 16.0, 32.0]; + let z = self.zoom; + let next = if up { STEPS.iter().copied().find(|s| *s > z * 1.01) } else { STEPS.iter().rev().copied().find(|s| *s < z * 0.99) }; + if let Some(n) = next { + self.set_zoom(n); + } + } + + /// Pull finished renders into textures. + pub fn receive(&mut self, ctx: &egui::Context, pool: &RenderPool) { + let mut got = false; + while let Some(r) = pool.try_recv() { + got = true; + let img = egui::ColorImage::from_rgba_premultiplied([r.width as usize, r.height as usize], &r.rgba); + let page = r.request.page; + if r.request.tag & THUMB_TAG != 0 { + let tex = ctx.load_texture(format!("thumb-{:?}-{page}", self.id), img, TextureOptions::LINEAR); + self.thumbs.insert(page, tex); + } else { + let tex = ctx.load_texture(format!("page-{:?}-{page}", self.id), img, TextureOptions::LINEAR); + self.pages.insert(page, PageTex { tag: r.request.tag, tex }); + } + } + if got { + ctx.request_repaint(); + } + } + + fn fit_zoom(&mut self, info: &DocInfo) { + let max_w = info.pages.iter().map(|p| p.width).fold(1.0, f32::max); + let avail_w = (self.viewport_w - 2.0 * SIDE).max(100.0); + let per_row = if self.layout == PageLayout::TwoUp { 2.0 } else { 1.0 }; + match self.fit { + Fit::Width => self.zoom = (avail_w - GAP * (per_row - 1.0)) / (max_w * PT * per_row), + Fit::Page => { + let p = &info.pages[self.current.min(info.pages.len() - 1)]; + let zw = (avail_w - GAP * (per_row - 1.0)) / (p.width * PT * per_row); + let zh = (self.viewport_h - 2.0 * MARGIN) / (p.height * PT); + self.zoom = zw.min(zh); + } + Fit::None => {} + } + self.zoom = self.zoom.clamp(0.08, 64.0); + } + + /// Page rects in content coordinates (origin at the scroll content's top-left). + fn layout(&self, info: &DocInfo, content_w: f32) -> Vec { + let s = self.zoom * PT; + let mut rects = Vec::with_capacity(info.pages.len()); + let mut y = MARGIN; + match self.layout { + PageLayout::Continuous | PageLayout::Single => { + for p in &info.pages { + let size = vec2(p.width * s, p.height * s); + rects.push(Rect::from_min_size(pos2(((content_w - size.x) / 2.0).max(SIDE), y), size)); + y += size.y + GAP; + } + } + PageLayout::TwoUp => { + for pair in info.pages.chunks(2) { + let sizes: Vec = pair.iter().map(|p| vec2(p.width * s, p.height * s)).collect(); + let row_w: f32 = sizes.iter().map(|v| v.x).sum::() + GAP * (sizes.len() as f32 - 1.0); + let row_h = sizes.iter().map(|v| v.y).fold(0.0, f32::max); + let mut x = ((content_w - row_w) / 2.0).max(SIDE); + for size in sizes { + rects.push(Rect::from_min_size(pos2(x, y + (row_h - size.y) / 2.0), size)); + x += size.x + GAP; + } + y += row_h + GAP; + } + } + } + rects + } + + fn render_scale(&self, ppp: f32) -> f32 { + // Quantize so tiny fit-width changes don't trigger re-renders. + ((self.zoom * PT * ppp) * 64.0).round() / 64.0 + } +} + +/// Map a user-space rect on a page to screen space, honouring crop box and rotation. +pub fn page_to_screen(info: &DocInfo, page: usize, page_rect: Rect, r: [f32; 4]) -> Rect { + let p = &info.pages[page]; + let [cx0, cy0, cx1, cy1] = p.crop; + let (cw, ch) = ((cx1 - cx0).max(1.0), (cy1 - cy0).max(1.0)); + let map = |x: f32, y: f32| -> Pos2 { + // Normalised coordinates in the unrotated crop box, y down. + let (u, v) = ((x - cx0) / cw, (cy1 - y) / ch); + let (u, v) = match p.rotation { + 90 => (1.0 - v, u), + 180 => (1.0 - u, 1.0 - v), + 270 => (v, 1.0 - u), + _ => (u, v), + }; + pos2(page_rect.left() + u * page_rect.width(), page_rect.top() + v * page_rect.height()) + }; + Rect::from_two_pos(map(r[0], r[1]), map(r[2], r[3])) +} + +pub fn shortcuts(view: &mut DocView, ctx: &egui::Context) { + use egui::{Key, KeyboardShortcut, Modifiers}; + if ctx.egui_wants_keyboard_input() { + return; + } + let cmd = |k| KeyboardShortcut::new(Modifiers::COMMAND, k); + let pressed = |s: KeyboardShortcut| ctx.input_mut(|i| i.consume_shortcut(&s)); + if pressed(cmd(Key::Plus)) || pressed(cmd(Key::Equals)) { + view.zoom_step(true); + } + if pressed(cmd(Key::Minus)) { + view.zoom_step(false); + } + if pressed(cmd(Key::Num0)) { + view.fit = Fit::Page; + view.goto = Some((view.current, 0.0)); + } + if pressed(cmd(Key::Num1)) { + view.set_zoom(1.0); + } + if pressed(cmd(Key::Num2)) { + view.fit = Fit::Width; + view.goto = Some((view.current, 0.0)); + } + let key = |k| ctx.input(|i| i.key_pressed(k)); + if key(Key::Home) { + view.go_to_page(0); + } + if key(Key::End) { + view.go_to_page(usize::MAX); + } + if view.layout == PageLayout::Single || ctx.input(|i| i.modifiers.command) { + if key(Key::ArrowRight) || key(Key::PageDown) { + view.go_to_page(view.current + 1); + } + if key(Key::ArrowLeft) || key(Key::PageUp) { + view.go_to_page(view.current.saturating_sub(1)); + } + } +} + +pub fn document_area(app: &mut PrintCraftApp, index: usize, ui: &mut egui::Ui) { + let t = Tokens::get(ui.ctx()); + let Some(doc) = app.session.get(app.views[index].id) else { return }; + let info = &doc.info; + if info.pages.is_empty() { + ui.centered_and_justified(|ui| ui.label("This document has no pages.")); + return; + } + let want_thumbs = app.right == Some(RightPanel::Pages) || app.views[index].organize; + let view = &mut app.views[index]; + notices(view, info, ui, &t); + if view.organize { + organize_grid(view, info, &doc.renderer, ui, &t); + return; + } + + let avail = ui.available_rect_before_wrap(); + view.viewport_w = avail.width(); + view.viewport_h = avail.height(); + view.fit_zoom(info); + + // Pinch / ctrl+scroll zoom, anchored on the current page. + let zoom_delta = ui.input(|i| i.zoom_delta()); + if (zoom_delta - 1.0).abs() > 0.001 && ui.rect_contains_pointer(avail) { + let z = view.zoom * zoom_delta; + view.set_zoom(z); + } + + let max_w = info.pages.iter().map(|p| p.width).fold(0.0, f32::max) * view.zoom * PT * if view.layout == PageLayout::TwoUp { 2.0 } else { 1.0 }; + let content_w = (max_w + 2.0 * SIDE).max(avail.width()); + let rects = view.layout(info, content_w); + let visible_pages: Vec = match view.layout { + PageLayout::Single => vec![view.current.min(rects.len() - 1)], + _ => (0..rects.len()).collect(), + }; + let (y_shift, content_h) = match view.layout { + PageLayout::Single => { + let r = rects[visible_pages[0]]; + (r.top() - MARGIN, r.height() + 2.0 * MARGIN) + } + _ => (0.0, rects.last().map(|r| r.bottom() + MARGIN).unwrap_or(0.0)), + }; + + let mut scroll = egui::ScrollArea::both().auto_shrink([false, false]).scroll_source(egui::scroll_area::ScrollSource { + drag: if app.quick_tool == QuickTool::Hand { egui::scroll_area::DragScroll::Always } else { egui::scroll_area::DragScroll::OnTouch }, + ..Default::default() + }); + if let Some((page, frac)) = view.goto.take() { + let page = page.min(rects.len() - 1); + let r = rects[page]; + scroll = scroll.vertical_scroll_offset((r.top() - y_shift - GAP + frac * r.height()).max(0.0)); + } + let ppp = ui.ctx().pixels_per_point(); + let scale = view.render_scale(ppp); + let tag = (scale * 1000.0) as u64; + let hand = app.quick_tool == QuickTool::Hand; + let mut hover_text: Option<(Pos2, String)> = None; + let mut clicked_link: Option = None; + + let out = scroll.show_viewport(ui, |ui, viewport| { + let (resp_rect, resp) = ui.allocate_exact_size(vec2(content_w, content_h), Sense::click()); + let origin = resp_rect.min - vec2(0.0, y_shift); + let painter = ui.painter(); + let visible = viewport.translate(resp_rect.min.to_vec2()); + let mut wanted = Vec::new(); + let mut current = view.current; + let mut best_overlap = -1.0f32; + let pointer = ui.input(|i| i.pointer.hover_pos()); + for &i in &visible_pages { + let r = rects[i].translate(origin.to_vec2()); + if !r.intersects(visible.expand(400.0)) { + continue; + } + let overlap = r.intersect(visible).height(); + if overlap > best_overlap { + best_overlap = overlap; + current = i; + } + // Shadow + paper. + painter.rect_filled(r.translate(vec2(0.0, 2.0)).expand(1.5), CornerRadius::same(2), t.page_shadow); + painter.rect_filled(r, CornerRadius::ZERO, Color32::WHITE); + match view.pages.get(&i) { + Some(p) => { + painter.image(p.tex.id(), r, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); + if p.tag != tag { + wanted.push(i); + } + } + None => { + wanted.push(i); + painter.text(r.center(), Align2::CENTER_CENTER, "Rendering…", theme::regular(12.0), t.text_faint); + } + } + painter.rect_stroke(r, CornerRadius::ZERO, Stroke::new(1.0, t.border), egui::StrokeKind::Outside); + + // Form-field highlight (Acrobat's "Highlight existing fields"). + if view.highlight_fields { + for f in info.fields.iter().filter(|f| f.page == Some(i)) { + if let Some(fr) = f.rect { + let sr = page_to_screen(info, i, r, fr); + painter.rect_filled(sr, CornerRadius::same(1), Color32::from_rgba_unmultiplied(0x6E, 0x8E, 0xF5, 48)); + painter.rect_stroke( + sr, + CornerRadius::same(1), + Stroke::new(1.0, Color32::from_rgb(0x6E, 0x8E, 0xF5)), + egui::StrokeKind::Inside, + ); + } + } + } + // Link hover + click. + if let Some(p) = pointer { + for l in info.links.iter().filter(|l| l.page == i) { + let sr = page_to_screen(info, i, r, l.rect); + if sr.contains(p) { + ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); + let label = match &l.target { + LinkTarget::Page(n) => format!("Go to page {}", info.pages.get(*n).map(|p| p.label.as_str()).unwrap_or("?")), + LinkTarget::Uri(u) => u.clone(), + LinkTarget::Other(s) => format!("{s} action"), + }; + hover_text = Some((p, label)); + if resp.clicked() && !hand { + clicked_link = Some(l.target.clone()); + } + } + } + // Annotation hover shows the comment, as Acrobat's popups do. + for a in info.annotations.iter().filter(|a| a.page == i) { + let sr = page_to_screen(info, i, r, a.rect); + if sr.contains(p) && hover_text.is_none() { + let who = a.author.clone().unwrap_or_else(|| a.subtype.clone()); + let body = a.contents.clone().unwrap_or_default(); + hover_text = Some((p, if body.is_empty() { who } else { format!("{who}\n{body}") })); + } + } + } + if let Some((fp, fr, t0)) = view.flash + && fp == i + { + let now = ui.input(|inp| inp.time); + let t0 = if t0 == 0.0 { now } else { t0 }; + view.flash = Some((fp, fr, t0)); + let age = (now - t0) as f32; + if age < 1.6 { + let a = ((1.6 - age) / 1.6 * 255.0) as u8; + let sr = page_to_screen(info, i, r, fr).expand(4.0); + painter.rect_stroke( + sr, + CornerRadius::same(3), + Stroke::new(2.5, Color32::from_rgba_unmultiplied(0x1B, 0x63, 0xE0, a)), + egui::StrokeKind::Outside, + ); + ui.ctx().request_repaint(); + } else { + view.flash = None; + } + } + } + if view.layout != PageLayout::Single { + view.current = current; + if !ui.memory(|m| m.has_focus(egui::Id::new("page-input"))) { + view.page_input = (current + 1).to_string(); + } + } + wanted + }); + + // Schedule renders: visible pages first (nearest the current page), then thumbnails. + let mut wanted = out.inner; + let cur = view.current; + wanted.sort_by_key(|&p| (p as isize - cur as isize).unsigned_abs()); + let mut queue: Vec = wanted.into_iter().map(|page| RenderRequest { page, scale, tag }).collect(); + if want_thumbs { + let s = THUMB_W * ppp / info.pages.iter().map(|p| p.width).fold(1.0, f32::max); + for page in 0..info.pages.len() { + if !view.thumbs.contains_key(&page) { + queue.push(RenderRequest { page, scale: s, tag: THUMB_TAG }); + } + } + } + if queue != view.last_queue { + doc.renderer.set_queue(queue.clone()); + view.last_queue = queue; + } + if !view.last_queue.is_empty() { + ui.ctx().request_repaint_after(std::time::Duration::from_millis(30)); + } + + if let Some((pos, text)) = hover_text { + egui::Area::new(egui::Id::new("canvas-hover")).order(egui::Order::Tooltip).fixed_pos(pos + vec2(14.0, 16.0)).show(ui.ctx(), |ui| { + egui::Frame::popup(ui.style()).show(ui, |ui| { + ui.set_max_width(320.0); + ui.label(text); + }); + }); + } + match clicked_link { + Some(LinkTarget::Page(p)) => view.go_to_page(p), + Some(LinkTarget::Uri(u)) => ui.ctx().open_url(egui::OpenUrl::new_tab(u)), + Some(LinkTarget::Other(s)) => app.notify(format!("{s} actions run in the JavaScript engine (M6)")), + None => {} + } + quick_bar(app, avail, ui); +} + +fn notices(view: &mut DocView, info: &DocInfo, ui: &mut egui::Ui, t: &Tokens) { + if view.notice_dismissed { + return; + } + let msg = if !info.fields.is_empty() { + Some(("text-cursor-input", format!("This document contains {} interactive form fields.", info.fields.len()), true)) + } else if !info.warnings.is_empty() { + Some(("triangle-alert", info.warnings[0].clone(), false)) + } else { + None + }; + let Some((icon, text, fields)) = msg else { return }; + egui::Frame::NONE.fill(t.accent_soft).inner_margin(egui::Margin::symmetric(14, 7)).show(ui, |ui| { + ui.horizontal(|ui| { + ui.add(icons::image(icon, 16.0, t.accent_text)); + ui.label(egui::RichText::new(text).color(t.text)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if icons::button(ui, "x", 22.0, false, "Dismiss").clicked() { + view.notice_dismissed = true; + } + if fields { + let label = if view.highlight_fields { "Hide field highlights" } else { "Highlight fields" }; + if crate::widgets::pill_button(ui, label, view.highlight_fields).clicked() { + view.highlight_fields = !view.highlight_fields; + } + } + }); + }); + }); +} + +/// The floating quick-action bar at the left edge of the document area. +fn quick_bar(app: &mut PrintCraftApp, area: Rect, ui: &mut egui::Ui) { + let t = Tokens::get(ui.ctx()); + let pos = area.left_top() + vec2(14.0, 14.0); + egui::Area::new(egui::Id::new("quick-bar")).order(egui::Order::Middle).fixed_pos(pos).show(ui.ctx(), |ui| { + egui::Frame::NONE + .fill(t.card) + .stroke(Stroke::new(1.0, t.border)) + .corner_radius(CornerRadius::same(10)) + .shadow(egui::Shadow { offset: [0, 2], blur: 10, spread: 0, color: Color32::from_black_alpha(if t.dark() { 80 } else { 22 }) }) + .inner_margin(egui::Margin::same(4)) + .show(ui, |ui| { + ui.spacing_mut().item_spacing.y = 2.0; + ui.vertical(|ui| { + if icons::button(ui, "mouse-pointer-2", 32.0, app.quick_tool == QuickTool::Select, "Select (V)").clicked() { + app.quick_tool = QuickTool::Select; + } + if icons::button(ui, "hand", 32.0, app.quick_tool == QuickTool::Hand, "Hand (H)").clicked() { + app.quick_tool = QuickTool::Hand; + } + for (icon, tip, cmd) in [ + ("message-square-text", "Add a comment (M5)", "comment.note"), + ("highlighter", "Highlight text (M5)", "comment.highlight"), + ("pencil", "Draw freehand (M5)", "comment.ink"), + ("pen-line", "Fill & Sign (M5)", "sign.fill.text"), + ] { + if icons::button(ui, icon, 32.0, false, tip).clicked() { + app.run_command(cmd); + } + } + }); + }); + }); +} + +/// Organize pages: a thumbnail grid (Acrobat's Organize Pages view). +fn organize_grid(view: &mut DocView, info: &DocInfo, pool: &RenderPool, ui: &mut egui::Ui, t: &Tokens) { + let ppp = ui.ctx().pixels_per_point(); + let cell = vec2(190.0, 250.0); + let mut open_page = None; + egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { + ui.add_space(20.0); + let cols = ((ui.available_width() - 40.0) / cell.x).floor().max(1.0) as usize; + let rows = info.pages.len().div_ceil(cols); + let left = (ui.available_width() - cols as f32 * cell.x) / 2.0; + for row in 0..rows { + let (row_rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), cell.y), Sense::hover()); + for col in 0..cols { + let i = row * cols + col; + let Some(p) = info.pages.get(i) else { break }; + let c = Rect::from_min_size(pos2(row_rect.left() + left + col as f32 * cell.x, row_rect.top()), cell); + let resp = ui.interact(c, ui.id().with(("org", i)), Sense::click()); + let s = (cell.x - 44.0) / p.width.max(1.0); + let size = vec2(p.width * s, p.height * s).min(vec2(cell.x - 44.0, cell.y - 56.0)); + let pr = Rect::from_center_size(pos2(c.center().x, c.top() + 16.0 + size.y / 2.0), size); + let selected = i == view.current; + if selected || resp.hovered() { + ui.painter().rect_filled(c.shrink(6.0), CornerRadius::same(8), if selected { t.accent_soft } else { t.hover }); + } + ui.painter().rect_filled(pr.translate(vec2(0.0, 1.5)), CornerRadius::same(1), t.page_shadow); + ui.painter().rect_filled(pr, CornerRadius::ZERO, Color32::WHITE); + if let Some(tex) = view.thumbs.get(&i) { + ui.painter().image(tex.id(), pr, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE); + } + ui.painter().rect_stroke(pr, CornerRadius::ZERO, Stroke::new(1.0, t.border), egui::StrokeKind::Outside); + ui.painter().text(pos2(c.center().x, pr.bottom() + 16.0), Align2::CENTER_CENTER, &p.label, theme::medium(12.0), t.text_muted); + if resp.clicked() { + view.current = i; + } + if resp.double_clicked() { + open_page = Some(i); + } + } + } + }); + let s = THUMB_W * ppp / info.pages.iter().map(|p| p.width).fold(1.0, f32::max); + let queue: Vec = + (0..info.pages.len()).filter(|p| !view.thumbs.contains_key(p)).map(|page| RenderRequest { page, scale: s, tag: THUMB_TAG }).collect(); + if queue != view.last_queue { + pool.set_queue(queue.clone()); + view.last_queue = queue; + } + if let Some(p) = open_page { + view.organize = false; + view.go_to_page(p); + } +} diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs new file mode 100644 index 00000000..c58aab9f --- /dev/null +++ b/crates/ui-egui/src/chrome.rs @@ -0,0 +1,316 @@ +//! Window chrome: tab strip (with the integrated macOS title bar), mode bar, right rail. + +use egui::{Align, Align2, Color32, CornerRadius, Layout, Rect, Sense, Stroke, vec2}; + +use crate::canvas::{Fit, PageLayout}; +use crate::theme::{self, ThemeKind, Tokens}; +use crate::{Dialog, Mode, PrintCraftApp, PropsTab, RightPanel, icons, widgets}; + +pub fn tab_strip(app: &mut PrintCraftApp, ui: &mut egui::Ui) { + let t = Tokens::get(ui.ctx()); + let left = if cfg!(target_os = "macos") && app.integrated_titlebar { 80 } else { 8 }; + egui::Panel::top("tab_strip") + .exact_size(38.0) + .frame(egui::Frame::NONE.fill(t.titlebar).inner_margin(egui::Margin { left, right: 10, top: 0, bottom: 0 })) + .show(ui, |ui| { + let full = ui.max_rect(); + let drag = ui.interact(full, ui.id().with("titledrag"), Sense::click_and_drag()); + if drag.drag_started() { + ui.ctx().send_viewport_cmd(egui::ViewportCommand::StartDrag); + } + if drag.double_clicked() { + let max = ui.ctx().input(|i| i.viewport().maximized.unwrap_or(false)); + ui.ctx().send_viewport_cmd(egui::ViewportCommand::Maximized(!max)); + } + ui.horizontal_centered(|ui| { + ui.spacing_mut().item_spacing.x = 4.0; + if icons::button(ui, "house", 28.0, app.active.is_none(), "Home").clicked() { + app.active = None; + } + let mut close = None; + for i in 0..app.views.len() { + let Some(doc) = app.session.get(app.views[i].id) else { continue }; + let name = doc.name.clone(); + if tab(ui, &t, &name, app.active == Some(i), &mut close, i).clicked() { + app.active = Some(i); + } + } + if let Some(i) = close { + app.close_tab(i); + } + ui.add_space(4.0); + if widgets::ghost_button(ui, "plus", "Open").on_hover_text("Open a PDF (⌘O)").clicked() { + app.open_dialog(); + } + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + let (icon, next, tip) = match app.theme { + ThemeKind::Light => ("moon", ThemeKind::Dark, "Dark gray theme"), + ThemeKind::Dark => ("sun", ThemeKind::Light, "Light theme"), + }; + if icons::button(ui, icon, 28.0, false, tip).clicked() { + let ctx = ui.ctx().clone(); + app.set_theme(&ctx, next); + } + if icons::button(ui, "circle-help", 28.0, false, "Keyboard shortcuts").clicked() { + app.dialog = Some(Dialog::Shortcuts); + } + }); + }); + }); +} + +fn tab(ui: &mut egui::Ui, t: &Tokens, name: &str, active: bool, close: &mut Option, index: usize) -> egui::Response { + let font = theme::regular(13.0); + let label: String = if name.chars().count() > 28 { format!("{}…", name.chars().take(27).collect::()) } else { name.to_string() }; + let text_w = ui.fonts_mut(|f| f.layout_no_wrap(label.clone(), font.clone(), t.text).size().x); + let (rect, resp) = ui.allocate_exact_size(vec2(text_w + 64.0, 30.0), Sense::click()); + let bg = if active { + t.chrome + } else if resp.hovered() { + t.hover + } else { + Color32::TRANSPARENT + }; + ui.painter().rect_filled(rect, CornerRadius { nw: 7, ne: 7, sw: 0, se: 0 }, bg); + icons::paint( + ui, + Rect::from_min_size(rect.min + vec2(6.0, 7.0), vec2(16.0, 16.0)), + "file-text", + 15.0, + if active { t.accent } else { t.text_muted }, + ); + ui.painter().text(rect.min + vec2(28.0, rect.height() / 2.0), Align2::LEFT_CENTER, label, font, if active { t.text } else { t.text_muted }); + let x_rect = Rect::from_center_size(rect.right_center() - vec2(16.0, 0.0), vec2(20.0, 20.0)); + let x = ui.interact(x_rect, ui.id().with(("tabclose", index)), Sense::click()); + if x.hovered() { + ui.painter().rect_filled(x_rect, CornerRadius::same(4), t.pressed); + } + if active || resp.hovered() || x.hovered() { + icons::paint(ui, x_rect, "x", 13.0, t.text_muted); + } + if x.clicked() { + *close = Some(index); + } + resp.on_hover_text(name) +} + +pub fn mode_bar(app: &mut PrintCraftApp, ui: &mut egui::Ui) { + let t = Tokens::get(ui.ctx()); + egui::Panel::top("mode_bar") + .exact_size(48.0) + .frame(egui::Frame::NONE.fill(t.chrome).inner_margin(egui::Margin::symmetric(10, 0)).stroke(Stroke::new(1.0, t.divider))) + .show(ui, |ui| { + ui.horizontal_centered(|ui| { + ui.spacing_mut().item_spacing.x = 2.0; + main_menu(app, ui); + ui.add_space(6.0); + ui.painter().vline(ui.cursor().left(), ui.max_rect().y_range().shrink(12.0), Stroke::new(1.0, t.divider)); + ui.add_space(10.0); + for (mode, label) in + [(Mode::AllTools, "All tools"), (Mode::Read, "Read"), (Mode::Edit, "Edit"), (Mode::Convert, "Convert"), (Mode::Sign, "E-Sign")] + { + if widgets::mode_tab(ui, label, app.mode == mode).clicked() { + app.mode = mode; + app.left_open = true; + app.left = match mode { + Mode::Edit => crate::LeftPanel::Tool("edit"), + Mode::Convert => crate::LeftPanel::Tool("export"), + Mode::Sign => crate::LeftPanel::Tool("fill_sign"), + _ => crate::LeftPanel::AllTools, + }; + } + } + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.spacing_mut().item_spacing.x = 4.0; + let has_doc = app.active.is_some(); + ui.add_enabled_ui(has_doc, |ui| { + if icons::button(ui, "printer", 32.0, false, "Print (M10)").clicked() { + app.run_command("print.dialog"); + } + if icons::button(ui, "save", 32.0, false, "Save (M4)").clicked() { + app.run_command("file.save"); + } + if icons::button(ui, "info", 32.0, false, "Document properties (⌘D)").clicked() { + app.dialog = Some(Dialog::Properties(PropsTab::Description)); + } + }); + ui.add_space(8.0); + if widgets::search_box(ui, "Find tools and commands", 260.0).clicked() { + app.palette_open = true; + } + }); + }); + }); +} + +fn main_menu(app: &mut PrintCraftApp, ui: &mut egui::Ui) { + let t = Tokens::get(ui.ctx()); + let resp = widgets::ghost_button(ui, "panel-left", "Menu"); + egui::Popup::menu(&resp).show(|ui| { + ui.set_min_width(230.0); + ui.menu_button("File", |ui| { + if widgets::menu_item(ui, "Open…", "⌘O").clicked() { + app.open_dialog(); + } + let has = app.active.is_some(); + if ui.add_enabled(has, egui::Button::new("Close file").shortcut_text("⌘W")).clicked() + && let Some(i) = app.active + { + app.close_tab(i); + } + ui.separator(); + if ui.add_enabled(has, egui::Button::new("Document properties…").shortcut_text("⌘D")).clicked() { + app.dialog = Some(Dialog::Properties(PropsTab::Description)); + } + }); + ui.menu_button("View", |ui| { + if let Some(i) = app.active { + let v = &mut app.views[i]; + ui.label(egui::RichText::new("Zoom").color(t.text_faint).small()); + if widgets::menu_item(ui, "Actual size", "⌘1").clicked() { + v.set_zoom(1.0); + } + if widgets::menu_item(ui, "Zoom to page level", "⌘0").clicked() { + v.fit = Fit::Page; + } + if widgets::menu_item(ui, "Fit to width", "⌘2").clicked() { + v.fit = Fit::Width; + } + if widgets::menu_item(ui, "Zoom in", "⌘+").clicked() { + v.zoom_step(true); + } + if widgets::menu_item(ui, "Zoom out", "⌘−").clicked() { + v.zoom_step(false); + } + ui.separator(); + ui.label(egui::RichText::new("Page display").color(t.text_faint).small()); + for (l, label) in + [(PageLayout::Continuous, "Continuous scrolling"), (PageLayout::TwoUp, "Two-page view"), (PageLayout::Single, "Single page")] + { + if ui.radio(v.layout == l, label).clicked() { + v.layout = l; + v.goto = Some((v.current, 0.0)); + } + } + ui.separator(); + } + if widgets::menu_item(ui, "Read mode", "⌃⌘H").clicked() { + app.mode = if app.mode == Mode::Read { Mode::AllTools } else { Mode::Read }; + } + ui.menu_button("Display theme", |ui| { + let ctx = ui.ctx().clone(); + if ui.radio(app.theme == ThemeKind::Light, "Light gray").clicked() { + app.set_theme(&ctx, ThemeKind::Light); + } + if ui.radio(app.theme == ThemeKind::Dark, "Dark gray").clicked() { + app.set_theme(&ctx, ThemeKind::Dark); + } + }); + ui.menu_button("Side panels", |ui| { + for (p, label) in [ + (RightPanel::Comments, "Comments"), + (RightPanel::Bookmarks, "Bookmarks"), + (RightPanel::Pages, "Pages"), + (RightPanel::Fields, "Fields"), + (RightPanel::Layers, "Layers"), + (RightPanel::Attachments, "Attachments"), + ] { + if ui.radio(app.right == Some(p), label).clicked() { + app.right = Some(p); + } + } + }); + }); + ui.menu_button("Help", |ui| { + if ui.button("Keyboard shortcuts").clicked() { + app.dialog = Some(Dialog::Shortcuts); + } + if ui.button("About PrintCraft").clicked() { + app.dialog = Some(Dialog::About); + } + }); + }); +} + +pub fn right_rail(app: &mut PrintCraftApp, ui: &mut egui::Ui) { + let t = Tokens::get(ui.ctx()); + let Some((index, id)) = app.active_ids() else { return }; + let Some(doc) = app.session.get(id) else { return }; + let (has_comments, has_outline, has_fields, has_layers, has_files) = ( + !doc.info.annotations.is_empty(), + !doc.info.outline.is_empty(), + !doc.info.fields.is_empty(), + !doc.info.layers.is_empty(), + !doc.info.attachments.is_empty(), + ); + let page_count = doc.info.pages.len(); + egui::Panel::right("rail") + .resizable(false) + .exact_size(48.0) + .frame(egui::Frame::NONE.fill(t.chrome).inner_margin(egui::Margin::symmetric(6, 8)).stroke(Stroke::new(1.0, t.divider))) + .show(ui, |ui| { + ui.spacing_mut().item_spacing.y = 4.0; + let mut rail_button = |ui: &mut egui::Ui, panel: RightPanel, icon: &str, tip: &str, has: bool| { + let selected = app.right == Some(panel); + let r = icons::button(ui, icon, 34.0, selected, tip); + if has && !selected { + let c = r.rect.right_top() + vec2(-8.0, 8.0); + ui.painter().circle_filled(c, 3.0, t.accent); + } + if r.clicked() { + app.right = if selected { None } else { Some(panel) }; + } + }; + rail_button(ui, RightPanel::Comments, "message-square-text", "Comments", has_comments); + rail_button(ui, RightPanel::Bookmarks, "bookmark", "Bookmarks", has_outline); + rail_button(ui, RightPanel::Pages, "files", "Page thumbnails", false); + rail_button(ui, RightPanel::Fields, "text-cursor-input", "Form fields", has_fields); + rail_button(ui, RightPanel::Layers, "layers", "Layers", has_layers); + rail_button(ui, RightPanel::Attachments, "paperclip", "Attachments", has_files); + + // Page navigation cluster at the bottom (as in Acrobat's rail). + let view = &mut app.views[index]; + ui.with_layout(Layout::bottom_up(Align::Center), |ui| { + ui.spacing_mut().item_spacing.y = 2.0; + if icons::button(ui, "zoom-out", 32.0, false, "Zoom out (⌘−)").clicked() { + view.zoom_step(false); + } + if icons::button(ui, "zoom-in", 32.0, false, "Zoom in (⌘+)").clicked() { + view.zoom_step(true); + } + let fit_icon = if view.fit == Fit::Width { "maximize" } else { "columns-2" }; + if icons::button(ui, fit_icon, 32.0, false, "Toggle fit page / fit width").clicked() { + view.fit = if view.fit == Fit::Width { Fit::Page } else { Fit::Width }; + view.goto = Some((view.current, 0.0)); + } + ui.label(egui::RichText::new(format!("{:.0}%", view.zoom * 100.0)).font(theme::regular(10.5)).color(t.text_faint)); + ui.add_space(6.0); + if icons::button(ui, "chevron-down", 30.0, false, "Next page").clicked() { + view.go_to_page(view.current + 1); + } + if icons::button(ui, "chevron-up", 30.0, false, "Previous page").clicked() { + view.go_to_page(view.current.saturating_sub(1)); + } + ui.label(egui::RichText::new(page_count.to_string()).font(theme::regular(11.0)).color(t.text_muted)); + let edit = egui::TextEdit::singleline(&mut view.page_input) + .id(egui::Id::new("page-input")) + .desired_width(34.0) + .horizontal_align(Align::Center) + .font(theme::medium(12.0)) + .margin(vec2(2.0, 4.0)); + let r = egui::Frame::NONE + .fill(t.field) + .stroke(Stroke::new(1.0, t.border)) + .corner_radius(CornerRadius::same(5)) + .show(ui, |ui| ui.add(edit)) + .inner + .on_hover_text("Current page — type a number and press Enter"); + if r.lost_focus() + && ui.input(|i| i.key_pressed(egui::Key::Enter)) + && let Ok(n) = view.page_input.trim().parse::() + { + view.go_to_page(n.saturating_sub(1)); + } + }); + }); +} diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs new file mode 100644 index 00000000..90c3c7ba --- /dev/null +++ b/crates/ui-egui/src/dialogs.rs @@ -0,0 +1,127 @@ +//! Modal dialogs: Document Properties, Keyboard Shortcuts, About. + +use egui::{Align, Layout, vec2}; + +use crate::theme::{self, Tokens}; +use crate::{Dialog, PrintCraftApp, PropsTab, panels::human_size, widgets}; + +pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { + let Some(dialog) = app.dialog else { return }; + let t = Tokens::get(ctx); + let mut close = false; + let mut next = dialog; + let modal = egui::Modal::new(egui::Id::new("dialog")).show(ctx, |ui| { + ui.set_width(560.0); + match dialog { + Dialog::Properties(tab) => { + ui.label(egui::RichText::new("Document Properties").font(theme::semibold(18.0))); + ui.add_space(8.0); + ui.horizontal(|ui| { + for (tb, label) in [(PropsTab::Description, "Description"), (PropsTab::Security, "Security"), (PropsTab::Advanced, "Advanced")] { + if widgets::mode_tab(ui, label, tab == tb).clicked() { + next = Dialog::Properties(tb); + } + } + }); + ui.separator(); + let Some((_, id)) = app.active_ids() else { return }; + let Some(doc) = app.session.get(id) else { return }; + let i = &doc.info; + let row = |ui: &mut egui::Ui, k: &str, v: String| { + ui.label(egui::RichText::new(k).color(t.text_muted)); + ui.label(if v.is_empty() { egui::RichText::new("—").color(t.text_faint) } else { egui::RichText::new(v) }); + ui.end_row(); + }; + egui::Grid::new("props").num_columns(2).spacing([18.0, 8.0]).min_col_width(140.0).show(ui, |ui| match tab { + PropsTab::Description => { + row(ui, "File", doc.name.clone()); + row(ui, "Title", i.title.clone().unwrap_or_default()); + row(ui, "Author", i.author.clone().unwrap_or_default()); + row(ui, "Subject", i.subject.clone().unwrap_or_default()); + row(ui, "Keywords", i.keywords.clone().unwrap_or_default()); + row(ui, "Application", i.creator.clone().unwrap_or_default()); + row(ui, "PDF producer", i.producer.clone().unwrap_or_default()); + } + PropsTab::Security => { + row(ui, "Security method", if i.encrypted { "Password security".into() } else { "No security".into() }); + for k in [ + "Printing", + "Changing the document", + "Document assembly", + "Content copying", + "Commenting", + "Filling of form fields", + "Signing", + ] { + row(ui, k, if i.encrypted { "See permissions (M8)".into() } else { "Allowed".into() }); + } + } + PropsTab::Advanced => { + row(ui, "PDF version", i.pdf_version.replace("Pdf", "").replace('_', ".")); + row(ui, "Location", doc.path.clone().unwrap_or_default()); + row(ui, "File size", format!("{} ({} bytes)", human_size(i.file_size), i.file_size)); + let p = &i.pages[0]; + row(ui, "Page size", format!("{:.2} × {:.2} in", p.width / 72.0, p.height / 72.0)); + row(ui, "Number of pages", i.pages.len().to_string()); + row(ui, "Tagged PDF", yes(i.tagged)); + row(ui, "Form fields", i.fields.len().to_string()); + row(ui, "Comments", i.annotations.len().to_string()); + row(ui, "Layers", i.layers.len().to_string()); + row(ui, "Attachments", i.attachments.len().to_string()); + row(ui, "JavaScript", yes(i.has_javascript)); + } + }); + } + Dialog::Shortcuts => { + ui.label(egui::RichText::new("Keyboard shortcuts").font(theme::semibold(18.0))); + ui.add_space(8.0); + egui::Grid::new("keys").num_columns(2).spacing([24.0, 6.0]).show(ui, |ui| { + for (k, v) in [ + ("⌘O", "Open"), + ("⌘W", "Close file"), + ("⌘D", "Document properties"), + ("⌘K", "Find tools and commands"), + ("⌘1", "Actual size"), + ("⌘0", "Zoom to page level"), + ("⌘2", "Fit to width"), + ("⌘+ / ⌘−", "Zoom in / out (also pinch or ⌘-scroll)"), + ("Home / End", "First / last page"), + ("⌘← / ⌘→", "Previous / next page"), + ("⌃⌘H", "Read mode"), + ] { + ui.label(egui::RichText::new(k).font(egui::FontId::monospace(12.5))); + ui.label(v); + ui.end_row(); + } + }); + } + Dialog::About => { + ui.label(egui::RichText::new("PrintCraft").font(theme::semibold(20.0))); + ui.label(format!("Version {}", env!("CARGO_PKG_VERSION"))); + ui.add_space(6.0); + ui.label("A clean-room, open-source PDF application written in Rust. MIT OR Apache-2.0."); + ui.label( + egui::RichText::new("Rendering: hayro (bootstrap) · UI: egui · Icons: Lucide (ISC) · Fonts: Inter, JetBrains Mono (OFL)") + .color(t.text_muted) + .small(), + ); + } + } + ui.add_space(12.0); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if widgets::pill_button(ui, "Close", true).clicked() { + close = true; + } + }); + let _ = vec2(0.0, 0.0); + }); + if modal.should_close() || close { + app.dialog = None; + } else { + app.dialog = Some(next); + } +} + +fn yes(b: bool) -> String { + if b { "Yes".into() } else { "No".into() } +} diff --git a/crates/ui-egui/src/home.rs b/crates/ui-egui/src/home.rs new file mode 100644 index 00000000..7a5582a6 --- /dev/null +++ b/crates/ui-egui/src/home.rs @@ -0,0 +1,123 @@ +//! Home tab: recommended tools, open card, recent files (local only, never another app's list). + +use egui::{Align2, CornerRadius, Rect, Sense, Stroke, vec2}; +use printcraft_engine::catalog; + +use crate::theme::{self, Tokens}; +use crate::{LeftPanel, PrintCraftApp, icons, panels::human_size, widgets}; + +const RECOMMENDED: [&str; 5] = ["organize", "comment", "form", "edit", "protect"]; + +pub fn show(app: &mut PrintCraftApp, ui: &mut egui::Ui) { + let t = Tokens::get(ui.ctx()); + egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { + egui::Frame::NONE.inner_margin(egui::Margin { left: 36, right: 36, top: 28, bottom: 28 }).show(ui, |ui| { + ui.label(egui::RichText::new("Welcome to PrintCraft").font(theme::semibold(24.0))); + ui.label( + egui::RichText::new("An open-source PDF workbench — local, private, and scriptable.").color(t.text_muted).font(theme::regular(14.0)), + ); + ui.add_space(22.0); + + egui::Frame::NONE + .fill(t.card) + .stroke(Stroke::new(1.0, t.border)) + .corner_radius(CornerRadius::same(12)) + .inner_margin(egui::Margin::same(18)) + .show(ui, |ui| { + ui.set_width(ui.available_width()); + ui.label(egui::RichText::new("Recommended tools").font(theme::semibold(15.0))); + ui.add_space(10.0); + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing = vec2(14.0, 14.0); + for id in RECOMMENDED { + let Some(g) = catalog::group(id) else { continue }; + let (rect, resp) = ui.allocate_exact_size(vec2(190.0, 104.0), Sense::click()); + let fill = if resp.hovered() { t.hover } else { t.card }; + ui.painter().rect(rect, CornerRadius::same(10), fill, Stroke::new(1.0, t.divider), egui::StrokeKind::Inside); + let color = egui::Color32::from_rgb(g.hue[0], g.hue[1], g.hue[2]); + icons::paint(ui, Rect::from_min_size(rect.min + vec2(14.0, 14.0), vec2(22.0, 22.0)), g.icon, 21.0, color); + ui.painter().text(rect.min + vec2(44.0, 25.0), Align2::LEFT_CENTER, g.label, theme::semibold(13.5), t.text); + let blurb = g + .sections + .first() + .map(|s| s.items.iter().take(3).map(|i| i.label).collect::>().join(" · ")) + .unwrap_or_default(); + let galley = ui.fonts_mut(|f| f.layout(blurb, theme::regular(11.5), t.text_muted, rect.width() - 28.0)); + ui.painter().galley(rect.min + vec2(14.0, 46.0), galley, t.text_muted); + ui.painter().text( + rect.left_bottom() + vec2(14.0, -14.0), + Align2::LEFT_CENTER, + "Use now", + theme::medium(12.0), + t.accent_text, + ); + if resp.clicked() { + app.left = LeftPanel::Tool(g.id); + app.left_open = true; + } + } + let (rect, resp) = ui.allocate_exact_size(vec2(170.0, 104.0), Sense::click()); + ui.painter().rect( + rect, + CornerRadius::same(10), + if resp.hovered() { t.hover } else { t.pasteboard }, + Stroke::new(1.0, t.divider), + egui::StrokeKind::Inside, + ); + icons::paint(ui, Rect::from_center_size(rect.center() - vec2(0.0, 16.0), vec2(28.0, 28.0)), "folder-open", 26.0, t.icon); + ui.painter().text(rect.center() + vec2(0.0, 22.0), Align2::CENTER_CENTER, "Open file", theme::semibold(13.0), t.text); + if resp.clicked() { + app.open_dialog(); + } + }); + }); + + ui.add_space(26.0); + ui.label(egui::RichText::new("Recent").font(theme::semibold(17.0))); + ui.add_space(8.0); + if app.recent.is_empty() { + ui.label(egui::RichText::new("Files you open in PrintCraft appear here. Drop a PDF anywhere to open it.").color(t.text_muted)); + } + let mut open = None; + for r in &app.recent { + let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 46.0), Sense::click()); + if resp.hovered() { + ui.painter().rect_filled(rect, CornerRadius::same(8), t.hover); + } + icons::paint( + ui, + Rect::from_min_size(rect.min + vec2(10.0, 11.0), vec2(24.0, 24.0)), + "file-text", + 22.0, + egui::Color32::from_rgb(0xE0, 0x3E, 0x3E), + ); + ui.painter().text(rect.min + vec2(46.0, 15.0), Align2::LEFT_CENTER, &r.name, theme::medium(13.5), t.text); + ui.painter().text(rect.min + vec2(46.0, 32.0), Align2::LEFT_CENTER, &r.path, theme::regular(11.0), t.text_faint); + ui.painter().text( + rect.right_center() - vec2(12.0, 0.0), + Align2::RIGHT_CENTER, + format!("{} pages · {}", r.pages, human_size(r.size)), + theme::regular(12.0), + t.text_muted, + ); + if resp.clicked() { + open = Some(r.path.clone()); + } + } + if let Some(p) = open { + if let Some(i) = app.views.iter().position(|v| app.session.get(v.id).and_then(|d| d.path.as_deref()) == Some(p.as_str())) { + app.active = Some(i); + } else { + #[cfg(not(target_arch = "wasm32"))] + app.open_path(&p); + } + } + ui.add_space(20.0); + widgets::section_title(ui, "Privacy"); + ui.label( + egui::RichText::new("PrintCraft works offline. No telemetry, no account, and no cloud processing unless you add a provider.") + .color(t.text_muted), + ); + }); + }); +} diff --git a/crates/ui-egui/src/icon_data.rs b/crates/ui-egui/src/icon_data.rs new file mode 100644 index 00000000..0cb3a23d --- /dev/null +++ b/crates/ui-egui/src/icon_data.rs @@ -0,0 +1,164 @@ +//! Lucide icons (ISC licence, see assets/icons/LICENSE-lucide.txt), embedded and tinted at runtime. +//! Generated from the contents of assets/icons/. + +pub static ICONS: &[(&str, &[u8])] = &[ + ("accessibility", include_bytes!("../../../assets/icons/accessibility.svg")), + ("align-left", include_bytes!("../../../assets/icons/align-left.svg")), + ("app-window", include_bytes!("../../../assets/icons/app-window.svg")), + ("arrow-left-right", include_bytes!("../../../assets/icons/arrow-left-right.svg")), + ("arrow-up-down", include_bytes!("../../../assets/icons/arrow-up-down.svg")), + ("arrow-up-right", include_bytes!("../../../assets/icons/arrow-up-right.svg")), + ("badge-check", include_bytes!("../../../assets/icons/badge-check.svg")), + ("bandage", include_bytes!("../../../assets/icons/bandage.svg")), + ("bell", include_bytes!("../../../assets/icons/bell.svg")), + ("blend", include_bytes!("../../../assets/icons/blend.svg")), + ("bold", include_bytes!("../../../assets/icons/bold.svg")), + ("book-open", include_bytes!("../../../assets/icons/book-open.svg")), + ("bookmark", include_bytes!("../../../assets/icons/bookmark.svg")), + ("braces", include_bytes!("../../../assets/icons/braces.svg")), + ("brush-cleaning", include_bytes!("../../../assets/icons/brush-cleaning.svg")), + ("brush", include_bytes!("../../../assets/icons/brush.svg")), + ("camera", include_bytes!("../../../assets/icons/camera.svg")), + ("check-circle-2", include_bytes!("../../../assets/icons/check-circle-2.svg")), + ("check", include_bytes!("../../../assets/icons/check.svg")), + ("chevron-down", include_bytes!("../../../assets/icons/chevron-down.svg")), + ("chevron-left", include_bytes!("../../../assets/icons/chevron-left.svg")), + ("chevron-right", include_bytes!("../../../assets/icons/chevron-right.svg")), + ("chevron-up", include_bytes!("../../../assets/icons/chevron-up.svg")), + ("chevrons-left", include_bytes!("../../../assets/icons/chevrons-left.svg")), + ("chevrons-right", include_bytes!("../../../assets/icons/chevrons-right.svg")), + ("circle-check", include_bytes!("../../../assets/icons/circle-check.svg")), + ("circle-dashed", include_bytes!("../../../assets/icons/circle-dashed.svg")), + ("circle-dot", include_bytes!("../../../assets/icons/circle-dot.svg")), + ("circle-help", include_bytes!("../../../assets/icons/circle-help.svg")), + ("circle", include_bytes!("../../../assets/icons/circle.svg")), + ("clock-3", include_bytes!("../../../assets/icons/clock-3.svg")), + ("clock", include_bytes!("../../../assets/icons/clock.svg")), + ("cloud-upload", include_bytes!("../../../assets/icons/cloud-upload.svg")), + ("cloud", include_bytes!("../../../assets/icons/cloud.svg")), + ("columns-2", include_bytes!("../../../assets/icons/columns-2.svg")), + ("compass", include_bytes!("../../../assets/icons/compass.svg")), + ("contrast", include_bytes!("../../../assets/icons/contrast.svg")), + ("copy-plus", include_bytes!("../../../assets/icons/copy-plus.svg")), + ("copy", include_bytes!("../../../assets/icons/copy.svg")), + ("crop", include_bytes!("../../../assets/icons/crop.svg")), + ("droplet", include_bytes!("../../../assets/icons/droplet.svg")), + ("ellipsis-vertical", include_bytes!("../../../assets/icons/ellipsis-vertical.svg")), + ("ellipsis", include_bytes!("../../../assets/icons/ellipsis.svg")), + ("eraser", include_bytes!("../../../assets/icons/eraser.svg")), + ("eye-off", include_bytes!("../../../assets/icons/eye-off.svg")), + ("eye", include_bytes!("../../../assets/icons/eye.svg")), + ("file-check", include_bytes!("../../../assets/icons/file-check.svg")), + ("file-down", include_bytes!("../../../assets/icons/file-down.svg")), + ("file-input", include_bytes!("../../../assets/icons/file-input.svg")), + ("file-lock-2", include_bytes!("../../../assets/icons/file-lock-2.svg")), + ("file-minus", include_bytes!("../../../assets/icons/file-minus.svg")), + ("file-output", include_bytes!("../../../assets/icons/file-output.svg")), + ("file-pen-line", include_bytes!("../../../assets/icons/file-pen-line.svg")), + ("file-plus-2", include_bytes!("../../../assets/icons/file-plus-2.svg")), + ("file-plus", include_bytes!("../../../assets/icons/file-plus.svg")), + ("file-scan", include_bytes!("../../../assets/icons/file-scan.svg")), + ("file-search", include_bytes!("../../../assets/icons/file-search.svg")), + ("file-symlink", include_bytes!("../../../assets/icons/file-symlink.svg")), + ("file-text", include_bytes!("../../../assets/icons/file-text.svg")), + ("file-x", include_bytes!("../../../assets/icons/file-x.svg")), + ("files", include_bytes!("../../../assets/icons/files.svg")), + ("film", include_bytes!("../../../assets/icons/film.svg")), + ("filter", include_bytes!("../../../assets/icons/filter.svg")), + ("folder-open", include_bytes!("../../../assets/icons/folder-open.svg")), + ("folder-plus", include_bytes!("../../../assets/icons/folder-plus.svg")), + ("folder", include_bytes!("../../../assets/icons/folder.svg")), + ("gauge", include_bytes!("../../../assets/icons/gauge.svg")), + ("git-compare", include_bytes!("../../../assets/icons/git-compare.svg")), + ("grid-3x3", include_bytes!("../../../assets/icons/grid-3x3.svg")), + ("hand", include_bytes!("../../../assets/icons/hand.svg")), + ("hash", include_bytes!("../../../assets/icons/hash.svg")), + ("heading", include_bytes!("../../../assets/icons/heading.svg")), + ("highlighter", include_bytes!("../../../assets/icons/highlighter.svg")), + ("house", include_bytes!("../../../assets/icons/house.svg")), + ("image-plus", include_bytes!("../../../assets/icons/image-plus.svg")), + ("image", include_bytes!("../../../assets/icons/image.svg")), + ("info", include_bytes!("../../../assets/icons/info.svg")), + ("italic", include_bytes!("../../../assets/icons/italic.svg")), + ("languages", include_bytes!("../../../assets/icons/languages.svg")), + ("lasso", include_bytes!("../../../assets/icons/lasso.svg")), + ("layers", include_bytes!("../../../assets/icons/layers.svg")), + ("layout-grid", include_bytes!("../../../assets/icons/layout-grid.svg")), + ("link-2", include_bytes!("../../../assets/icons/link-2.svg")), + ("link", include_bytes!("../../../assets/icons/link.svg")), + ("list-checks", include_bytes!("../../../assets/icons/list-checks.svg")), + ("list-ordered", include_bytes!("../../../assets/icons/list-ordered.svg")), + ("list", include_bytes!("../../../assets/icons/list.svg")), + ("lock-open", include_bytes!("../../../assets/icons/lock-open.svg")), + ("lock", include_bytes!("../../../assets/icons/lock.svg")), + ("maximize-2", include_bytes!("../../../assets/icons/maximize-2.svg")), + ("maximize", include_bytes!("../../../assets/icons/maximize.svg")), + ("message-circle-reply", include_bytes!("../../../assets/icons/message-circle-reply.svg")), + ("message-square-text", include_bytes!("../../../assets/icons/message-square-text.svg")), + ("message-square", include_bytes!("../../../assets/icons/message-square.svg")), + ("mic", include_bytes!("../../../assets/icons/mic.svg")), + ("minimize", include_bytes!("../../../assets/icons/minimize.svg")), + ("minus", include_bytes!("../../../assets/icons/minus.svg")), + ("moon", include_bytes!("../../../assets/icons/moon.svg")), + ("mouse-pointer-2", include_bytes!("../../../assets/icons/mouse-pointer-2.svg")), + ("move", include_bytes!("../../../assets/icons/move.svg")), + ("navigation", include_bytes!("../../../assets/icons/navigation.svg")), + ("paint-bucket", include_bytes!("../../../assets/icons/paint-bucket.svg")), + ("palette", include_bytes!("../../../assets/icons/palette.svg")), + ("panel-left", include_bytes!("../../../assets/icons/panel-left.svg")), + ("panel-right", include_bytes!("../../../assets/icons/panel-right.svg")), + ("panels-top-left", include_bytes!("../../../assets/icons/panels-top-left.svg")), + ("paperclip", include_bytes!("../../../assets/icons/paperclip.svg")), + ("pen-line", include_bytes!("../../../assets/icons/pen-line.svg")), + ("pen-tool", include_bytes!("../../../assets/icons/pen-tool.svg")), + ("pencil", include_bytes!("../../../assets/icons/pencil.svg")), + ("pipette", include_bytes!("../../../assets/icons/pipette.svg")), + ("plus", include_bytes!("../../../assets/icons/plus.svg")), + ("presentation", include_bytes!("../../../assets/icons/presentation.svg")), + ("printer", include_bytes!("../../../assets/icons/printer.svg")), + ("rectangle-horizontal", include_bytes!("../../../assets/icons/rectangle-horizontal.svg")), + ("redo-2", include_bytes!("../../../assets/icons/redo-2.svg")), + ("replace", include_bytes!("../../../assets/icons/replace.svg")), + ("rotate-ccw", include_bytes!("../../../assets/icons/rotate-ccw.svg")), + ("rotate-cw", include_bytes!("../../../assets/icons/rotate-cw.svg")), + ("ruler", include_bytes!("../../../assets/icons/ruler.svg")), + ("save", include_bytes!("../../../assets/icons/save.svg")), + ("scan-text", include_bytes!("../../../assets/icons/scan-text.svg")), + ("scan", include_bytes!("../../../assets/icons/scan.svg")), + ("scissors", include_bytes!("../../../assets/icons/scissors.svg")), + ("search", include_bytes!("../../../assets/icons/search.svg")), + ("send", include_bytes!("../../../assets/icons/send.svg")), + ("settings-2", include_bytes!("../../../assets/icons/settings-2.svg")), + ("settings", include_bytes!("../../../assets/icons/settings.svg")), + ("share-2", include_bytes!("../../../assets/icons/share-2.svg")), + ("shield-check", include_bytes!("../../../assets/icons/shield-check.svg")), + ("signature", include_bytes!("../../../assets/icons/signature.svg")), + ("sliders-horizontal", include_bytes!("../../../assets/icons/sliders-horizontal.svg")), + ("sparkles", include_bytes!("../../../assets/icons/sparkles.svg")), + ("spline", include_bytes!("../../../assets/icons/spline.svg")), + ("square-dashed-mouse-pointer", include_bytes!("../../../assets/icons/square-dashed-mouse-pointer.svg")), + ("square-dashed", include_bytes!("../../../assets/icons/square-dashed.svg")), + ("square", include_bytes!("../../../assets/icons/square.svg")), + ("squares-subtract", include_bytes!("../../../assets/icons/squares-subtract.svg")), + ("stamp", include_bytes!("../../../assets/icons/stamp.svg")), + ("star", include_bytes!("../../../assets/icons/star.svg")), + ("sticky-note", include_bytes!("../../../assets/icons/sticky-note.svg")), + ("strikethrough", include_bytes!("../../../assets/icons/strikethrough.svg")), + ("sun", include_bytes!("../../../assets/icons/sun.svg")), + ("swatch-book", include_bytes!("../../../assets/icons/swatch-book.svg")), + ("tag", include_bytes!("../../../assets/icons/tag.svg")), + ("text-cursor-input", include_bytes!("../../../assets/icons/text-cursor-input.svg")), + ("text-select", include_bytes!("../../../assets/icons/text-select.svg")), + ("trash-2", include_bytes!("../../../assets/icons/trash-2.svg")), + ("trash", include_bytes!("../../../assets/icons/trash.svg")), + ("triangle-alert", include_bytes!("../../../assets/icons/triangle-alert.svg")), + ("type", include_bytes!("../../../assets/icons/type.svg")), + ("underline", include_bytes!("../../../assets/icons/underline.svg")), + ("undo-2", include_bytes!("../../../assets/icons/undo-2.svg")), + ("user-round", include_bytes!("../../../assets/icons/user-round.svg")), + ("wand-sparkles", include_bytes!("../../../assets/icons/wand-sparkles.svg")), + ("wrench", include_bytes!("../../../assets/icons/wrench.svg")), + ("x", include_bytes!("../../../assets/icons/x.svg")), + ("zoom-in", include_bytes!("../../../assets/icons/zoom-in.svg")), + ("zoom-out", include_bytes!("../../../assets/icons/zoom-out.svg")), +]; diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs new file mode 100644 index 00000000..949a4111 --- /dev/null +++ b/crates/ui-egui/src/icons.rs @@ -0,0 +1,71 @@ +//! Vector icons: embedded Lucide SVGs, recoloured to white, rasterized by egui_extras at the exact +//! on-screen pixel size, and tinted per use (PhotoCraft lesson: never use Unicode glyphs as icons). + +use std::collections::HashMap; +use std::sync::{Arc, OnceLock}; + +use egui::{Color32, Rect, Response, Sense, Vec2}; + +use crate::icon_data::ICONS; +use crate::theme::Tokens; + +fn white_icons() -> &'static HashMap<&'static str, Arc<[u8]>> { + static MAP: OnceLock>> = OnceLock::new(); + MAP.get_or_init(|| { + ICONS + .iter() + .map(|(name, bytes)| { + let svg = String::from_utf8_lossy(bytes).replace("currentColor", "#ffffff").replace("stroke-width=\"2\"", "stroke-width=\"1.75\""); + (*name, Arc::from(svg.into_bytes().into_boxed_slice())) + }) + .collect() + }) +} + +pub fn exists(name: &str) -> bool { + white_icons().contains_key(name) +} + +pub fn image(name: &str, size: f32, tint: Color32) -> egui::Image<'static> { + let (key, bytes) = match white_icons().get(name) { + Some(b) => (name, b.clone()), + None => ("square", white_icons()["square"].clone()), + }; + egui::Image::from_bytes(format!("bytes://icons/{key}.svg"), egui::load::Bytes::Shared(bytes)).fit_to_exact_size(Vec2::splat(size)).tint(tint) +} + +pub fn paint(ui: &egui::Ui, rect: Rect, name: &str, size: f32, tint: Color32) { + image(name, size, tint).paint_at(ui, Rect::from_center_size(rect.center(), Vec2::splat(size))); +} + +/// Square icon button: transparent until hovered; `selected` gets the accent treatment. +pub fn button(ui: &mut egui::Ui, name: &str, box_size: f32, selected: bool, tooltip: &str) -> Response { + let t = Tokens::get(ui.ctx()); + let (rect, resp) = ui.allocate_exact_size(Vec2::splat(box_size), Sense::click()); + let hovered = resp.hovered(); + if selected { + ui.painter().rect_filled(rect, t.radius, t.accent_soft); + } else if resp.is_pointer_button_down_on() { + ui.painter().rect_filled(rect, t.radius, t.pressed); + } else if hovered { + ui.painter().rect_filled(rect, t.radius, t.hover); + } + let tint = if selected { t.accent_text } else { t.icon }; + paint(ui, rect, name, (box_size * 0.5).round(), tint); + if tooltip.is_empty() { resp } else { resp.on_hover_text(tooltip) } +} + +#[cfg(test)] +mod tests { + #[test] + fn every_catalog_icon_exists() { + for g in printcraft_engine::catalog::TOOL_GROUPS { + assert!(super::exists(g.icon), "missing icon {}", g.icon); + for s in g.sections { + for i in s.items { + assert!(super::exists(i.icon), "missing icon {}", i.icon); + } + } + } + } +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs new file mode 100644 index 00000000..b5c1f33a --- /dev/null +++ b/crates/ui-egui/src/lib.rs @@ -0,0 +1,395 @@ +//! printcraft-ui-egui — the first PrintCraft shell (L7). +//! +//! Layout grammar follows plan/acrobat/02-ui-ux.md §1: tab strip, mode bar, left tool panel, +//! floating quick-action bar, document area, right panel + right rail with page navigation. +//! Everything here is presentation: documents, rendering and the tool catalogue live in +//! `printcraft-engine`. + +mod canvas; +mod chrome; +mod dialogs; +mod home; +mod icon_data; +pub mod icons; +mod palette; +mod panels; +pub mod theme; +mod widgets; + +use printcraft_engine::{DocId, Session}; + +pub use canvas::DocView; +use theme::ThemeKind; + +/// Top-level workspace modes (Acrobat's mode bar). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Mode { + AllTools, + Read, + Edit, + Convert, + Sign, +} + +/// What the left panel shows. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LeftPanel { + AllTools, + Tool(&'static str), +} + +/// Right-hand panels, opened from the rail. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RightPanel { + Comments, + Bookmarks, + Pages, + Fields, + Layers, + Attachments, +} + +/// Quick-action bar tools (the vertical floating strip). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum QuickTool { + Select, + Hand, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Dialog { + Properties(PropsTab), + About, + Shortcuts, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PropsTab { + Description, + Security, + Advanced, +} + +pub struct RecentFile { + pub name: String, + pub path: String, + pub pages: usize, + pub size: usize, +} + +pub struct PrintCraftApp { + pub session: Session, + pub views: Vec, + /// `None` shows the Home tab. + pub active: Option, + pub mode: Mode, + pub left: LeftPanel, + pub left_open: bool, + pub right: Option, + pub quick_tool: QuickTool, + pub theme: ThemeKind, + pub dialog: Option, + pub palette_open: bool, + pub palette_query: String, + pub all_tools_expanded: bool, + pub recent: Vec, + pub toast: Option<(String, f64)>, + /// Whether the macOS title bar is drawn by us (traffic lights over our tab strip). + pub integrated_titlebar: bool, + pending_theme: Option, + styled: bool, + fonts_ready: bool, +} + +impl Default for PrintCraftApp { + fn default() -> Self { + Self::new() + } +} + +impl PrintCraftApp { + pub fn new() -> Self { + Self { + session: Session::new(), + views: Vec::new(), + active: None, + mode: Mode::AllTools, + left: LeftPanel::AllTools, + left_open: true, + right: None, + quick_tool: QuickTool::Select, + theme: ThemeKind::Light, + dialog: None, + palette_open: false, + palette_query: String::new(), + all_tools_expanded: false, + recent: Vec::new(), + toast: None, + integrated_titlebar: false, + pending_theme: None, + styled: false, + fonts_ready: false, + } + } + + /// Open a document and make it the active tab. + pub fn open_bytes(&mut self, name: &str, path: Option, bytes: Vec) -> Result<(), String> { + let size = bytes.len(); + let id = self.session.open(name, path.clone(), bytes).map_err(|e| e.to_string())?; + let doc = self.session.get(id).expect("just opened"); + let pages = doc.info.pages.len(); + // Acrobat opens straight to the Comments panel when a document has comments. + if self.right.is_none() { + self.right = if !doc.info.annotations.is_empty() { + Some(RightPanel::Comments) + } else if !doc.info.outline.is_empty() { + Some(RightPanel::Bookmarks) + } else { + None + }; + } + self.views.push(DocView::new(id, &doc.info)); + self.active = Some(self.views.len() - 1); + if let Some(p) = path { + self.recent.retain(|r| r.path != p); + self.recent.insert(0, RecentFile { name: name.to_string(), path: p, pages, size }); + self.recent.truncate(12); + } + Ok(()) + } + + #[cfg(not(target_arch = "wasm32"))] + pub fn open_path(&mut self, path: &str) { + let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| path.to_string()); + match std::fs::read(path) { + Ok(bytes) => { + if let Err(e) = self.open_bytes(&name, Some(path.to_string()), bytes) { + self.notify(format!("Couldn't open {name}: {e}")); + } + } + Err(e) => self.notify(format!("Couldn't read {name}: {e}")), + } + } + + pub fn open_dialog(&mut self) { + #[cfg(not(target_arch = "wasm32"))] + if let Some(p) = rfd::FileDialog::new().add_filter("PDF", &["pdf"]).pick_file() { + self.open_path(&p.to_string_lossy()); + } + } + + pub fn close_tab(&mut self, index: usize) { + if index >= self.views.len() { + return; + } + let id = self.views.remove(index).id; + self.session.close(id); + self.active = match self.active { + _ if self.views.is_empty() => None, + Some(a) if a >= self.views.len() => Some(self.views.len() - 1), + other => other, + }; + } + + pub fn active_ids(&self) -> Option<(usize, DocId)> { + self.active.and_then(|i| self.views.get(i).map(|v| (i, v.id))) + } + + pub fn notify(&mut self, msg: impl Into) { + self.toast = Some((msg.into(), 0.0)); + } + + pub fn set_theme(&mut self, ctx: &egui::Context, kind: ThemeKind) { + self.theme = kind; + theme::apply(ctx, kind); + } + + /// Run a catalogue command. Commands that aren't implemented yet say which milestone ships them. + pub fn run_command(&mut self, command: &str) { + match command { + "page.organize" => { + if let Some(i) = self.active { + self.views[i].organize = !self.views[i].organize; + } else { + self.notify("Open a document first"); + } + } + "comment.list" => self.right = Some(RightPanel::Comments), + "form.fields" => self.right = Some(RightPanel::Fields), + "protect.properties" => self.dialog = Some(Dialog::Properties(PropsTab::Security)), + other => { + let when = printcraft_engine::catalog::TOOL_GROUPS + .iter() + .flat_map(|g| g.sections.iter().flat_map(|s| s.items.iter())) + .find(|i| i.command == other) + .map(|i| match i.availability { + printcraft_engine::catalog::Availability::Planned(m) => format!("ships in milestone {m}"), + printcraft_engine::catalog::Availability::Provider => "needs an AI provider (off by default)".to_string(), + printcraft_engine::catalog::Availability::Ready => "is available".to_string(), + }) + .unwrap_or_else(|| "is not available yet".into()); + self.notify(format!("`{other}` {when}")); + } + } + } + + /// Apply a named view option (`--page 3`, `--panel bookmarks`, `--theme dark`, …). + /// + /// This is the seed of the UI control channel (M3.9): the same verbs become `ui.set` calls. + pub fn set_option(&mut self, key: &str, value: &str) -> Result<(), String> { + let view = self.active.and_then(|i| self.views.get_mut(i)); + match (key, view) { + ("theme", _) => self.pending_theme = Some(if value == "dark" { ThemeKind::Dark } else { ThemeKind::Light }), + ("panel", _) => { + self.right = match value { + "comments" => Some(RightPanel::Comments), + "bookmarks" => Some(RightPanel::Bookmarks), + "pages" => Some(RightPanel::Pages), + "fields" => Some(RightPanel::Fields), + "layers" => Some(RightPanel::Layers), + "attachments" => Some(RightPanel::Attachments), + "none" => None, + other => return Err(format!("unknown panel {other}")), + } + } + ("mode", _) => { + self.mode = match value { + "read" => Mode::Read, + "edit" => Mode::Edit, + "convert" => Mode::Convert, + "sign" => Mode::Sign, + _ => Mode::AllTools, + } + } + ("tool", _) => { + let g = printcraft_engine::catalog::group(value).ok_or_else(|| format!("unknown tool {value}"))?; + self.left = LeftPanel::Tool(g.id); + self.left_open = true; + } + ("left", _) => self.left_open = value != "closed", + ("home", _) => self.active = None, + ("dialog", _) => { + self.dialog = match value { + "properties" => Some(Dialog::Properties(PropsTab::Description)), + "shortcuts" => Some(Dialog::Shortcuts), + _ => Some(Dialog::About), + } + } + ("palette", _) => { + self.palette_open = true; + self.palette_query = value.to_string(); + } + ("page", Some(v)) => v.go_to_page(value.parse::().map_err(|e| e.to_string())?.saturating_sub(1)), + ("zoom", Some(v)) => v.set_zoom(value.trim_end_matches('%').parse::().map_err(|e| e.to_string())? / 100.0), + ("layout", Some(v)) => { + v.layout = match value { + "two-up" => canvas::PageLayout::TwoUp, + "single" => canvas::PageLayout::Single, + _ => canvas::PageLayout::Continuous, + } + } + ("organize", Some(v)) => v.organize = value != "off", + ("fields", Some(v)) => v.highlight_fields = value != "off", + (k, None) if ["page", "zoom", "layout", "organize", "fields"].contains(&k) => return Err(format!("`{k}` needs an open document")), + (other, _) => return Err(format!("unknown option {other}")), + } + Ok(()) + } + + fn shortcuts(&mut self, ctx: &egui::Context) { + use egui::{Key, KeyboardShortcut, Modifiers}; + let cmd = |k| KeyboardShortcut::new(Modifiers::COMMAND, k); + let pressed = |s: KeyboardShortcut| ctx.input_mut(|i| i.consume_shortcut(&s)); + if pressed(cmd(Key::O)) { + self.open_dialog(); + } + if pressed(cmd(Key::K)) { + self.palette_open = !self.palette_open; + } + if pressed(cmd(Key::D)) && self.active.is_some() { + self.dialog = Some(Dialog::Properties(PropsTab::Description)); + } + if pressed(cmd(Key::W)) + && let Some(i) = self.active + { + self.close_tab(i); + } + if pressed(KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::CTRL, Key::H)) { + self.mode = if self.mode == Mode::Read { Mode::AllTools } else { Mode::Read }; + } + if let Some(i) = self.active { + canvas::shortcuts(&mut self.views[i], ctx); + } + } +} + +impl eframe::App for PrintCraftApp { + fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + if !self.styled { + egui_extras::install_image_loaders(ctx); + theme::install_fonts(ctx); + theme::apply(ctx, self.theme); + self.styled = true; + } else { + self.fonts_ready = true; + } + if let Some(k) = self.pending_theme.take() { + self.set_theme(ctx, k); + } + let dropped = ctx.input(|i| i.raw.dropped_files.clone()); + for f in dropped { + #[cfg(not(target_arch = "wasm32"))] + { + let p = f.path().to_string_lossy().into_owned(); + if !p.is_empty() { + self.open_path(&p); + continue; + } + } + let name = f.path().file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| "dropped.pdf".into()); + match f.bytes() { + Ok(bytes) => { + if let Err(e) = self.open_bytes(&name, None, bytes) { + self.notify(format!("Couldn't open {name}: {e}")); + } + } + Err(e) => self.notify(format!("Couldn't read {name}: {e}")), + } + } + self.shortcuts(ctx); + // Pull finished renders into textures for every open document. + for view in &mut self.views { + if let Some(doc) = self.session.get(view.id) { + view.receive(ctx, &doc.renderer); + } + } + } + + fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { + let ctx = ui.ctx().clone(); + // Fonts registered via set_fonts only take effect next frame; named families would panic now. + if !self.fonts_ready { + ctx.request_repaint(); + return; + } + chrome::tab_strip(self, ui); + chrome::mode_bar(self, ui); + if self.active.is_some() { + chrome::right_rail(self, ui); + if self.right.is_some() && self.mode != Mode::Read { + panels::right_panel(self, ui); + } + } + if self.left_open && self.mode != Mode::Read { + panels::left_panel(self, ui); + } + let t = theme::Tokens::get(&ctx); + egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.pasteboard)).show(ui, |ui| match self.active { + None => home::show(self, ui), + Some(i) => canvas::document_area(self, i, ui), + }); + palette::show(self, &ctx); + dialogs::show(self, &ctx); + widgets::toast(self, &ctx); + } +} diff --git a/crates/ui-egui/src/palette.rs b/crates/ui-egui/src/palette.rs new file mode 100644 index 00000000..9b34cbd2 --- /dev/null +++ b/crates/ui-egui/src/palette.rs @@ -0,0 +1,132 @@ +//! ⌘K command palette over the tool catalogue (fuzzy-ish substring match on tool and item names). + +use egui::{Align2, CornerRadius, Rect, Sense, Stroke, vec2}; +use printcraft_engine::catalog::{Availability, TOOL_GROUPS}; + +use crate::theme::{self, Tokens}; +use crate::{LeftPanel, PrintCraftApp, icons}; + +struct Hit { + group: &'static str, + label: String, + detail: &'static str, + icon: &'static str, + command: Option<&'static str>, + ready: bool, +} + +fn score(hay: &str, needle: &str) -> Option { + if needle.is_empty() { + return Some(0); + } + let h = hay.to_lowercase(); + if let Some(p) = h.find(needle) { + return Some(p); + } + // Subsequence match as a fallback. + let mut it = h.chars(); + needle.chars().all(|c| it.any(|x| x == c)).then_some(100) +} + +pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { + if !app.palette_open { + return; + } + let t = Tokens::get(ctx); + if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { + app.palette_open = false; + return; + } + let q = app.palette_query.trim().to_lowercase(); + let mut hits: Vec<(usize, Hit)> = Vec::new(); + for g in TOOL_GROUPS { + if let Some(s) = score(g.label, &q) { + hits.push(( + s, + Hit { + group: g.id, + label: g.label.to_string(), + detail: "Tool", + icon: g.icon, + command: None, + ready: g.availability == Availability::Ready, + }, + )); + } + for sec in g.sections { + for i in sec.items { + if let Some(s) = score(i.label, &q).or_else(|| score(i.command, &q).map(|s| s + 50)) { + hits.push(( + s + 1, + Hit { + group: g.id, + label: i.label.to_string(), + detail: g.label, + icon: i.icon, + command: Some(i.command), + ready: i.availability == Availability::Ready, + }, + )); + } + } + } + } + hits.sort_by_key(|(s, h)| (*s, !h.ready)); + hits.truncate(12); + + let screen = ctx.content_rect(); + let mut chosen: Option<(Option<&'static str>, &'static str)> = None; + egui::Area::new(egui::Id::new("palette")) + .order(egui::Order::Foreground) + .pivot(Align2::CENTER_TOP) + .fixed_pos(egui::pos2(screen.center().x, screen.top() + 96.0)) + .show(ctx, |ui| { + egui::Frame::popup(ui.style()).corner_radius(CornerRadius::same(12)).inner_margin(egui::Margin::same(10)).show(ui, |ui| { + ui.set_width(560.0); + ui.horizontal(|ui| { + ui.add(icons::image("search", 18.0, t.text_muted)); + let r = ui.add( + egui::TextEdit::singleline(&mut app.palette_query) + .hint_text("Search tools and commands…") + .frame(egui::Frame::NONE) + .font(theme::regular(15.0)) + .desired_width(f32::INFINITY), + ); + r.request_focus(); + if r.lost_focus() + && ui.input(|i| i.key_pressed(egui::Key::Enter)) + && let Some((_, h)) = hits.first() + { + chosen = Some((h.command, h.group)); + } + }); + ui.separator(); + for (_, h) in &hits { + let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 36.0), Sense::click()); + if resp.hovered() { + ui.painter().rect_filled(rect, CornerRadius::same(6), t.hover); + } + icons::paint(ui, Rect::from_min_size(rect.min + vec2(8.0, 9.0), vec2(18.0, 18.0)), h.icon, 17.0, t.icon); + ui.painter().text(rect.left_center() + vec2(36.0, 0.0), Align2::LEFT_CENTER, &h.label, theme::regular(13.5), t.text); + ui.painter().text(rect.right_center() - vec2(10.0, 0.0), Align2::RIGHT_CENTER, h.detail, theme::regular(12.0), t.text_faint); + if resp.clicked() { + chosen = Some((h.command, h.group)); + } + } + if hits.is_empty() { + ui.label(egui::RichText::new("No matching tools").color(t.text_muted)); + } + ui.add_space(2.0); + let _ = Stroke::NONE; + }); + }); + if let Some((command, group)) = chosen { + app.palette_open = false; + app.palette_query.clear(); + app.left_open = true; + app.left = LeftPanel::Tool(group); + if let Some(c) = command { + app.run_command(c); + } + } +} diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs new file mode 100644 index 00000000..b302f849 --- /dev/null +++ b/crates/ui-egui/src/panels.rs @@ -0,0 +1,509 @@ +//! Left tool panel (All tools + tool sub-panels, generated from the engine catalogue) and the +//! right-hand panels (Comments, Bookmarks, Pages, Fields, Layers, Attachments). + +use egui::{Align, Align2, Color32, CornerRadius, Layout, Rect, Sense, Stroke, pos2, vec2}; +use printcraft_engine::catalog::{self, Availability, TOOL_GROUPS, ToolGroup}; +use printcraft_render::{Annotation, DocInfo, FieldKind, OutlineItem}; + +use crate::theme::{self, Tokens}; +use crate::{LeftPanel, PrintCraftApp, RightPanel, icons, widgets}; + +const COLLAPSED_TOOLS: usize = 14; + +fn hue(g: &ToolGroup) -> Color32 { + Color32::from_rgb(g.hue[0], g.hue[1], g.hue[2]) +} + +pub fn left_panel(app: &mut PrintCraftApp, ui: &mut egui::Ui) { + let t = Tokens::get(ui.ctx()); + egui::Panel::left("tool_panel") + .resizable(false) + .exact_size(272.0) + .frame( + egui::Frame::NONE + .fill(t.panel) + .inner_margin(egui::Margin { left: 14, right: 10, top: 12, bottom: 10 }) + .stroke(Stroke::new(1.0, t.divider)), + ) + .show(ui, |ui| match app.left { + LeftPanel::AllTools => all_tools(app, ui, &t), + LeftPanel::Tool(id) => match catalog::group(id) { + Some(g) => tool_detail(app, ui, &t, g), + None => app.left = LeftPanel::AllTools, + }, + }); +} + +fn panel_header(ui: &mut egui::Ui, t: &Tokens, title: &str, back: bool) -> (bool, bool) { + let mut go_back = false; + let mut close = false; + ui.horizontal(|ui| { + if back && icons::button(ui, "chevron-left", 26.0, false, "Back to all tools").clicked() { + go_back = true; + } + ui.label(egui::RichText::new(title).font(theme::semibold(15.5)).color(t.text)); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if icons::button(ui, "x", 26.0, false, "Close panel").clicked() { + close = true; + } + }); + }); + ui.add_space(6.0); + (go_back, close) +} + +fn all_tools(app: &mut PrintCraftApp, ui: &mut egui::Ui, t: &Tokens) { + let (_, close) = panel_header(ui, t, "All tools", false); + if close { + app.left_open = false; + } + egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { + let shown = if app.all_tools_expanded { TOOL_GROUPS.len() } else { COLLAPSED_TOOLS.min(TOOL_GROUPS.len()) }; + for g in &TOOL_GROUPS[..shown] { + if tool_row(ui, t, g).clicked() { + app.left = LeftPanel::Tool(g.id); + } + } + ui.add_space(4.0); + let more = if app.all_tools_expanded { "View less" } else { "View more" }; + if ui.add(egui::Label::new(egui::RichText::new(more).color(t.accent_text).font(theme::medium(13.0))).sense(Sense::click())).clicked() { + app.all_tools_expanded = !app.all_tools_expanded; + } + }); +} + +fn tool_row(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup) -> egui::Response { + let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::click()); + if resp.hovered() { + ui.painter().rect_filled(rect, CornerRadius::same(6), t.hover); + } + icons::paint(ui, Rect::from_min_size(rect.min + vec2(6.0, 7.0), vec2(20.0, 20.0)), g.icon, 19.0, hue(g)); + ui.painter().text(rect.left_center() + vec2(36.0, 0.0), Align2::LEFT_CENTER, g.label, theme::regular(13.5), t.text); + let (chip, fill) = match (g.badge, g.availability) { + (Some(b), _) => (Some(b.to_string()), t.badge_new), + (None, Availability::Planned(m)) => (Some(m.to_string()), t.pressed), + _ => (None, t.pressed), + }; + if let Some(c) = chip { + let font = theme::semibold(9.5); + let fg = if fill == t.badge_new { Color32::WHITE } else { t.text_muted }; + let w = ui.fonts_mut(|f| f.layout_no_wrap(c.clone(), font.clone(), fg).size().x); + let r = Rect::from_center_size(rect.right_center() - vec2(w / 2.0 + 10.0, 0.0), vec2(w + 10.0, 16.0)); + ui.painter().rect_filled(r, CornerRadius::same(4), fill); + ui.painter().text(r.center(), Align2::CENTER_CENTER, c, font, fg); + } + let tip = match g.availability { + Availability::Ready => "Available".to_string(), + Availability::Planned(m) => format!("Planned for milestone {m} — open to see what it will include"), + Availability::Provider => "Optional: needs an AI provider you configure".into(), + }; + resp.on_hover_text(tip) +} + +fn tool_detail(app: &mut PrintCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static ToolGroup) { + let (back, close) = panel_header(ui, t, g.label, true); + if back { + app.left = LeftPanel::AllTools; + app.mode = crate::Mode::AllTools; + } + if close { + app.left_open = false; + } + if let Availability::Planned(m) = g.availability { + egui::Frame::NONE.fill(t.accent_soft).corner_radius(CornerRadius::same(8)).inner_margin(egui::Margin::same(10)).show(ui, |ui| { + ui.set_width(ui.available_width()); + ui.label( + egui::RichText::new(format!("Coming in milestone {m}. Items marked Ready work today.")).color(t.text).font(theme::regular(12.0)), + ); + }); + } + let mut run = None; + egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { + for s in g.sections { + widgets::section_title(ui, s.title); + for item in s.items { + let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::click()); + let ready = item.availability == Availability::Ready; + if resp.hovered() { + ui.painter().rect_filled(rect, CornerRadius::same(6), t.hover); + } + let fg = if ready { t.text } else { t.text_muted }; + icons::paint( + ui, + Rect::from_min_size(rect.min + vec2(6.0, 8.0), vec2(18.0, 18.0)), + item.icon, + 17.0, + if ready { hue(g) } else { t.text_faint }, + ); + ui.painter().text(rect.left_center() + vec2(34.0, 0.0), Align2::LEFT_CENTER, item.label, theme::regular(13.0), fg); + let (chip, fill, cfg) = match item.availability { + Availability::Ready => ("Ready", Color32::from_rgb(0xDD, 0xF3, 0xE4), Color32::from_rgb(0x1E, 0x7B, 0x43)), + Availability::Planned(m) => (m, t.pressed, t.text_muted), + Availability::Provider => ("AI", t.pressed, t.text_muted), + }; + let font = theme::semibold(9.5); + let w = ui.fonts_mut(|f| f.layout_no_wrap(chip.to_string(), font.clone(), cfg).size().x); + let r = Rect::from_center_size(rect.right_center() - vec2(w / 2.0 + 10.0, 0.0), vec2(w + 10.0, 16.0)); + ui.painter().rect_filled(r, CornerRadius::same(4), fill); + ui.painter().text(r.center(), Align2::CENTER_CENTER, chip, font, cfg); + if resp.on_hover_text(item.command).clicked() { + run = Some(item.command); + } + } + } + }); + if let Some(cmd) = run { + app.run_command(cmd); + } +} + +// ───────────────────────────────────────────────────────────────────────────── right panels + +enum Nav { + Page(usize), + Flash(usize, [f32; 4]), +} + +pub fn right_panel(app: &mut PrintCraftApp, ui: &mut egui::Ui) { + let t = Tokens::get(ui.ctx()); + let Some((index, id)) = app.active_ids() else { return }; + let Some(panel) = app.right else { return }; + let mut nav: Option